From c7f15acd26a8a1b5c071369a5e6ce82d92916cc5 Mon Sep 17 00:00:00 2001 From: Tu Shaokun <2801884530@qq.com> Date: Fri, 5 Dec 2025 23:22:33 +0800 Subject: [PATCH 1/2] feat: open file in local editor when clicking component tag - Add direct editor URL scheme support (vscode, cursor, windsurf, trae, webstorm, phpstorm, idea, zed, sublime, atom) - Use __PROJECT_ROOT__ from Vite config to build absolute file paths - Use __PREFERRED_EDITOR__ from Vite config to select editor (default: vscode) - Clean up localhost URL prefixes from file paths - Fallback to react-grab.com/open-file when __PROJECT_ROOT__ is not configured --- .../react-grab/src/components/renderer.tsx | 71 +++++++++++++++++-- packages/react-grab/src/core.tsx | 13 ++++ 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/packages/react-grab/src/components/renderer.tsx b/packages/react-grab/src/components/renderer.tsx index 70222472..c47554ea 100644 --- a/packages/react-grab/src/components/renderer.tsx +++ b/packages/react-grab/src/components/renderer.tsx @@ -7,6 +7,36 @@ import { Crosshair } from "./crosshair.js"; import { SelectionCursor } from "./selection-cursor.js"; import { SelectionLabel } from "./selection-label.js"; +// Declare globals which are injected by Vite's define config +declare const __PROJECT_ROOT__: string | undefined; +declare const __PREFERRED_EDITOR__: string | undefined; + +// Editor URL schemes for common frontend editors +const EDITOR_URL_SCHEMES: Record = { + vscode: "vscode://file{file}:{line}:{column}", + cursor: "cursor://file{file}:{line}:{column}", + windsurf: "windsurf://file{file}:{line}:{column}", + trae: "trae://file{file}:{line}:{column}", + webstorm: "webstorm://open?file={file}&line={line}&column={column}", + zed: "zed://file{file}:{line}:{column}", +}; + +function getPreferredEditor(): string { + // Read from Vite's define config (set via REACT_GRAB_EDITOR env var) + if (typeof __PREFERRED_EDITOR__ !== "undefined" && __PREFERRED_EDITOR__ in EDITOR_URL_SCHEMES) { + return __PREFERRED_EDITOR__; + } + return "vscode"; // Default +} + +function buildEditorUrl(editor: string, filePath: string, line: number, column: number): string { + const scheme = EDITOR_URL_SCHEMES[editor] || EDITOR_URL_SCHEMES.vscode; + return scheme + .replace("{file}", filePath) + .replace("{line}", String(line)) + .replace("{column}", String(column)); +} + export const ReactGrabRenderer: Component = (props) => { return ( <> @@ -103,11 +133,42 @@ export const ReactGrabRenderer: Component = (props) => { onToggleExpand={props.onToggleExpand} onOpen={() => { if (props.selectionFilePath) { - const openFileUrl = buildOpenFileUrl( - props.selectionFilePath, - props.selectionLineNumber, - ); - window.open(openFileUrl, "_blank"); + // Clean up the file path - remove localhost URL prefix if present + let cleanPath = props.selectionFilePath; + try { + if (cleanPath.includes("localhost")) { + const url = new URL(cleanPath, window.location.origin); + cleanPath = url.pathname; + } + } catch { + const match = cleanPath.match(/localhost:\d+(.+)/); + if (match) { + cleanPath = match[1]; + } + } + + const projectRoot = typeof __PROJECT_ROOT__ !== "undefined" ? __PROJECT_ROOT__ : ""; + + if (projectRoot) { + // Ensure proper path joining + const normalizedRoot = projectRoot.endsWith("/") ? projectRoot : projectRoot + "/"; + const normalizedPath = cleanPath.startsWith("/") ? cleanPath.slice(1) : cleanPath; + const absolutePath = normalizedRoot + normalizedPath; + const line = props.selectionLineNumber || 1; + const editor = getPreferredEditor(); + const editorUrl = buildEditorUrl(editor, absolutePath, line, 1); + + const link = document.createElement("a"); + link.href = editorUrl; + link.click(); + } else { + // Fallback to react-grab.com/open-file + const openFileUrl = buildOpenFileUrl( + props.selectionFilePath, + props.selectionLineNumber, + ); + window.open(openFileUrl, "_blank"); + } } }} /> diff --git a/packages/react-grab/src/core.tsx b/packages/react-grab/src/core.tsx index 40e1c035..33575338 100644 --- a/packages/react-grab/src/core.tsx +++ b/packages/react-grab/src/core.tsx @@ -790,6 +790,19 @@ export const init = (rawOptions?: Options): ReactGrabAPI => { return; } + // Prefer DOM attributes (added by tools like lovable-tagger) + const componentPath = element.getAttribute("data-component-path"); + const componentLine = element.getAttribute("data-component-line"); + + if (componentPath) { + setSelectionFilePath(componentPath); + setSelectionLineNumber( + componentLine ? parseInt(componentLine, 10) : undefined, + ); + return; + } + + // Fallback to React Fiber stack getStack(element) .then((stack) => { if (!stack) return; From ca94d3fb187f5d4120913985d3ebd20b88d5e5a1 Mon Sep 17 00:00:00 2001 From: Tu Shaokun <2801884530@qq.com> Date: Sat, 6 Dec 2025 09:09:07 +0800 Subject: [PATCH 2/2] feat: add JetBrains Rider support Add Rider to the list of supported editors, using the same URL scheme as WebStorm (rider://open?file={file}&line={line}&column={column}). This addresses Issue #48 request for JetBrains Rider support. --- packages/react-grab/src/components/renderer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-grab/src/components/renderer.tsx b/packages/react-grab/src/components/renderer.tsx index c47554ea..1d3aca73 100644 --- a/packages/react-grab/src/components/renderer.tsx +++ b/packages/react-grab/src/components/renderer.tsx @@ -11,13 +11,14 @@ import { SelectionLabel } from "./selection-label.js"; declare const __PROJECT_ROOT__: string | undefined; declare const __PREFERRED_EDITOR__: string | undefined; -// Editor URL schemes for common frontend editors +// Editor URL schemes for common editors const EDITOR_URL_SCHEMES: Record = { vscode: "vscode://file{file}:{line}:{column}", cursor: "cursor://file{file}:{line}:{column}", windsurf: "windsurf://file{file}:{line}:{column}", trae: "trae://file{file}:{line}:{column}", webstorm: "webstorm://open?file={file}&line={line}&column={column}", + rider: "rider://open?file={file}&line={line}&column={column}", zed: "zed://file{file}:{line}:{column}", };