sourcebot/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/index.tsx

139 lines
5.2 KiB
TypeScript
Raw Normal View History

V4 (#311) Sourcebot V4 introduces authentication, performance improvements and code navigation. Checkout the [migration guide](https://docs.sourcebot.dev/self-hosting/upgrade/v3-to-v4-guide) for information on upgrading your instance to v4. ### Changed - [**Breaking Change**] Authentication is now required by default. Notes: - When setting up your instance, email / password login will be the default authentication provider. - The first user that logs into the instance is given the `owner` role. ([docs](https://docs.sourcebot.dev/docs/more/roles-and-permissions)). - Subsequent users can request to join the instance. The `owner` can approve / deny requests to join the instance via `Settings` > `Members` > `Pending Requests`. - If a user is approved to join the instance, they are given the `member` role. - Additional login providers, including email links and SSO, can be configured with additional environment variables. ([docs](https://docs.sourcebot.dev/self-hosting/configuration/authentication)). - Clicking on a search result now takes you to the `/browse` view. Files can still be previewed by clicking the "Preview" button or holding `Cmd` / `Ctrl` when clicking on a search result. [#315](https://github.com/sourcebot-dev/sourcebot/pull/315) ### Added - [Sourcebot EE] Added search-based code navigation, allowing you to jump between symbol definition and references when viewing source files. [Read the documentation](https://docs.sourcebot.dev/docs/search/code-navigation). [#315](https://github.com/sourcebot-dev/sourcebot/pull/315) - Added collapsible filter panel. [#315](https://github.com/sourcebot-dev/sourcebot/pull/315) ### Fixed - Improved scroll performance for large numbers of search results. [#315](https://github.com/sourcebot-dev/sourcebot/pull/315)
2025-05-28 23:08:42 +00:00
import { Button } from "@/components/ui/button";
import { LoadingButton } from "@/components/ui/loading-button";
import { Separator } from "@/components/ui/separator";
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { Loader2 } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { SymbolDefinition, useHoveredOverSymbolInfo } from "./useHoveredOverSymbolInfo";
import { SymbolDefinitionPreview } from "./symbolDefinitionPreview";
interface SymbolHoverPopupProps {
editorRef: ReactCodeMirrorRef;
language: string;
revisionName: string;
onFindReferences: (symbolName: string) => void;
onGotoDefinition: (symbolName: string, symbolDefinitions: SymbolDefinition[]) => void;
}
export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
editorRef,
revisionName,
language,
onFindReferences,
onGotoDefinition: _onGotoDefinition,
}) => {
const ref = useRef<HTMLDivElement>(null);
const [isSticky, setIsSticky] = useState(false);
const symbolInfo = useHoveredOverSymbolInfo({
editorRef,
isSticky,
revisionName,
language,
});
// Positions the popup relative to the symbol
useEffect(() => {
if (!symbolInfo) {
return;
}
const virtualElement: VirtualElement = {
getBoundingClientRect: () => {
return symbolInfo.element.getBoundingClientRect();
}
}
if (ref.current) {
computePosition(virtualElement, ref.current, {
placement: 'top',
middleware: [
offset(2),
flip({
mainAxis: true,
crossAxis: false,
fallbackPlacements: ['bottom'],
boundary: editorRef.view?.dom,
}),
shift({
padding: 5,
})
]
}).then(({ x, y }) => {
if (ref.current) {
ref.current.style.left = `${x}px`;
ref.current.style.top = `${y}px`;
}
})
}
}, [symbolInfo, editorRef]);
const onGotoDefinition = useCallback(() => {
if (!symbolInfo || !symbolInfo.symbolDefinitions) {
return;
}
_onGotoDefinition(symbolInfo.symbolName, symbolInfo.symbolDefinitions);
}, [symbolInfo, _onGotoDefinition]);
// @todo: We should probably make the behaviour s.t., the ctrl / cmd key needs to be held
// down to navigate to the definition. We should also only show the underline when the key
// is held, hover is active, and we have found the symbol definition.
useEffect(() => {
if (!symbolInfo || !symbolInfo.symbolDefinitions) {
return;
}
symbolInfo.element.addEventListener("click", onGotoDefinition);
return () => {
symbolInfo.element.removeEventListener("click", onGotoDefinition);
}
}, [symbolInfo, onGotoDefinition]);
return symbolInfo ? (
<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 max-w-3xl"
onMouseOver={() => setIsSticky(true)}
onMouseOut={() => setIsSticky(false)}
>
{symbolInfo.isSymbolDefinitionsLoading ? (
<div className="flex flex-row items-center gap-2 text-sm">
<Loader2 className="w-4 h-4 animate-spin" />
Loading...
</div>
) : symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 0 ? (
<SymbolDefinitionPreview
symbolDefinition={symbolInfo.symbolDefinitions[0]}
/>
) : (
<p className="text-sm font-medium text-muted-foreground">No hover info found</p>
)}
<Separator />
<div className="flex flex-row gap-2 mt-2">
<LoadingButton
loading={symbolInfo.isSymbolDefinitionsLoading}
disabled={!symbolInfo.symbolDefinitions || symbolInfo.symbolDefinitions.length === 0}
variant="outline"
size="sm"
onClick={onGotoDefinition}
>
{
!symbolInfo.isSymbolDefinitionsLoading && (!symbolInfo.symbolDefinitions || symbolInfo.symbolDefinitions.length === 0) ?
"No definition found" :
`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`
}
</LoadingButton>
<Button
variant="outline"
size="sm"
onClick={() => onFindReferences(symbolInfo.symbolName)}
>
Find references
</Button>
</div>
</div>
) : null;
};