Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
617 changes: 616 additions & 1 deletion packages/react-grab/e2e/selection.spec.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/react-grab/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export const ARROW_LABEL_MARGIN_PX = 16;
export const ARROW_PANEL_OVERLAP_PX = 1;
export const LABEL_GAP_PX = 4;
export const PREVIEW_TEXT_MAX_LENGTH = 100;
export const TEXT_NODE_LABEL_MAX_LENGTH = 20;
export const PREVIEW_ATTR_VALUE_MAX_LENGTH = 15;
export const PREVIEW_PRIORITY_ATTRS: readonly string[] = [
"id",
Expand Down
13 changes: 13 additions & 0 deletions packages/react-grab/src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,19 @@ export const getElementReferenceContext = async (
return `${getInlineHTMLPreview(element)}${composeElementContext(element, traceContext).replace(/\n\s+/g, " ")}`;
};

export const getTextNodeReferenceContext = async (
textNode: Text,
options: StackContextOptions = {},
): Promise<string> => {
const textContent = textNode.textContent?.replace(/\s+/g, " ").trim() ?? "";
const parentElement = textNode.parentElement;
if (!parentElement) return JSON.stringify(textContent);

const traceContext = await getTraceContext(parentElement, options);
const parentContext = composeElementContext(parentElement, traceContext).replace(/\n\s+/g, " ");
return `${JSON.stringify(textContent)}${parentContext}`;
};
Comment thread
cursor[bot] marked this conversation as resolved.

export const formatElementInfo = async (
element: Element,
options: StackContextOptions = {},
Expand Down
33 changes: 31 additions & 2 deletions packages/react-grab/src/core/copy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { getElementReferenceContext, getStack, getStackContext, resolveSource } from "./context.js";
import {
getElementReferenceContext,
getStack,
getStackContext,
getTextNodeReferenceContext,
resolveSource,
} from "./context.js";
import { copyContent } from "../utils/copy-content.js";
import { normalizeError } from "../utils/normalize-error.js";
import { getTagName } from "../utils/get-tag-name.js";
import { formatTextNodeLabel } from "../utils/format-text-node-label.js";
import { ABORTED_PROMISE_RESULT, racePromiseWithAbort } from "../utils/race-promise-with-abort.js";
import type { StackFrame } from "bippy/source";
import type { ReactGrabEntry, ReactGrabStackFrame } from "../types.js";
Expand All @@ -11,6 +18,7 @@ interface CopyFlowOptions {
componentName?: string;
maxContextLines?: number;
signal?: AbortSignal;
textNode?: Text;
}

interface CopyFlowHooks {
Expand Down Expand Up @@ -68,7 +76,28 @@ const buildElementPayloadEntry = async (
const buildClipboardPayload = async (
elements: Element[],
maxContextLines?: number,
textNode?: Text,
): Promise<CopyPayload | null> => {
const parentElement = elements[0];
if (textNode && parentElement) {
const stackOptions = { maxLines: maxContextLines };
const [referenceContext, stackContext, source, stack] = await Promise.all([
getTextNodeReferenceContext(textNode, stackOptions),
getStackContext(parentElement, stackOptions),
resolveSource(parentElement),
getStack(parentElement),
]);
const entry: ReactGrabEntry = {
tagName: formatTextNodeLabel(textNode),
componentName: source?.componentName ?? undefined,
content: `[${referenceContext}]`,
source,
stackContext,
frames: (stack ?? []).map(formatStackFramePayload),
};
return { content: entry.content, entries: [entry] };
}

const rawEntries = await Promise.all(
elements.map((element) => buildElementPayloadEntry(element, maxContextLines)),
);
Expand Down Expand Up @@ -122,7 +151,7 @@ export const runCopyFlow = async (
try {
const pendingPayload: Promise<CopyPayload | null> = options.getContent
? Promise.resolve(options.getContent(elements)).then((content) => ({ content }))
: buildClipboardPayload(elements, options.maxContextLines);
: buildClipboardPayload(elements, options.maxContextLines, options.textNode);
const payload = await racePromiseWithAbort(pendingPayload, options.signal);
if (payload === ABORTED_PROMISE_RESULT) return CANCELLED_COPY_FLOW_RESULT;
const rawContent = payload?.content;
Expand Down
Loading
Loading