sourcebot/packages/web/src/app/[domain]/components/editorContextMenu.tsx
2025-02-12 13:51:44 -08:00

147 lines
No EOL
4.4 KiB
TypeScript

'use client';
import { useToast } from "@/components/hooks/use-toast";
import { Button } from "@/components/ui/button";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { createPathWithQueryParams } from "@/lib/utils";
import { autoPlacement, computePosition, offset, shift, VirtualElement } from "@floating-ui/react";
import { Link2Icon } from "@radix-ui/react-icons";
import { EditorView, SelectionRange } from "@uiw/react-codemirror";
import { useCallback, useEffect, useRef } from "react";
import { resolveServerPath } from "../../api/(client)/client";
interface ContextMenuProps {
view: EditorView;
selection: SelectionRange;
repoName: string;
path: string;
revisionName: string;
}
export const EditorContextMenu = ({
view,
selection,
repoName,
path,
revisionName,
}: ContextMenuProps) => {
const ref = useRef<HTMLDivElement>(null);
const { toast } = useToast();
const captureEvent = useCaptureEvent();
useEffect(() => {
if (selection.empty) {
ref.current?.classList.add('hidden');
} else {
ref.current?.classList.remove('hidden');
}
}, [selection.empty]);
useEffect(() => {
if (selection.empty) {
return;
}
const { from, to } = selection;
const start = view.coordsAtPos(from);
const end = view.coordsAtPos(to);
if (!start || !end) {
return;
}
const selectionElement: VirtualElement = {
getBoundingClientRect: () => {
const { top, left } = start;
const { bottom, right } = end;
return {
x: left,
y: top,
top,
bottom,
left,
right,
width: right - left,
height: bottom - top,
}
}
}
if (ref.current) {
computePosition(selectionElement, ref.current, {
middleware: [
offset(5),
autoPlacement({
boundary: view.dom,
padding: 5,
allowedPlacements: ['bottom'],
}),
shift({
padding: 5
})
],
}).then(({ x, y }) => {
if (ref.current) {
ref.current.style.left = `${x}px`;
ref.current.style.top = `${y}px`;
}
});
}
}, [selection, view]);
const onCopyLinkToSelection = useCallback(() => {
const toLineAndColumn = (pos: number) => {
const lineInfo = view.state.doc.lineAt(pos);
return {
line: lineInfo.number,
column: pos - lineInfo.from + 1,
}
}
const from = toLineAndColumn(selection.from);
const to = toLineAndColumn(selection.to);
// @note: we need to resolve the server path for /browse since
// we aren't using <Link /> (which normally does this for us).
const basePath = `${window.location.origin}${resolveServerPath('/browse')}`;
const url = createPathWithQueryParams(`${basePath}/${repoName}@${revisionName}/-/blob/${path}`,
['highlightRange', `${from?.line}:${from?.column},${to?.line}:${to?.column}`],
);
navigator.clipboard.writeText(url);
toast({
description: "✅ Copied link to selection",
});
captureEvent('share_link_created', {});
// Reset the selection
view.dispatch(
{
selection: {
anchor: selection.to,
head: selection.to,
}
}
)
}, [captureEvent, path, repoName, selection.from, selection.to, toast, view, revisionName]);
return (
<div
ref={ref}
className="absolute z-10 flex flex-col gap-2 bg-background border border-gray-300 dark:border-gray-700 rounded-md shadow-lg p-2"
>
<Button
variant="ghost"
size="sm"
onClick={onCopyLinkToSelection}
>
<Link2Icon className="h-4 w-4 mr-1" />
Share selection
</Button>
</div>
)
}