sourcebot/packages/web/src/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo.ts

140 lines
4.3 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 { findSearchBasedSymbolDefinitions } from "@/features/codeNav/actions";
import { SourceRange } from "@/features/search/types";
import { useDomain } from "@/hooks/useDomain";
import { unwrapServiceError } from "@/lib/utils";
import { useQuery } from "@tanstack/react-query";
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { SYMBOL_HOVER_TARGET_DATA_ATTRIBUTE } from "./symbolHoverTargetsExtension";
interface UseHoveredOverSymbolInfoProps {
editorRef: ReactCodeMirrorRef;
isSticky: boolean;
revisionName: string;
language: string;
}
export type SymbolDefinition = {
lineContent: string;
language: string;
fileName: string;
repoName: string;
range: SourceRange;
}
interface HoveredOverSymbolInfo {
element: HTMLElement;
symbolName: string;
isSymbolDefinitionsLoading: boolean;
symbolDefinitions?: SymbolDefinition[];
}
const SYMBOL_HOVER_POPUP_MOUSE_OVER_TIMEOUT_MS = 500;
const SYMBOL_HOVER_POPUP_MOUSE_OUT_TIMEOUT_MS = 100;
export const useHoveredOverSymbolInfo = ({
editorRef,
isSticky,
revisionName,
language,
}: UseHoveredOverSymbolInfoProps): HoveredOverSymbolInfo | undefined => {
const mouseOverTimerRef = useRef<NodeJS.Timeout | null>(null);
const mouseOutTimerRef = useRef<NodeJS.Timeout | null>(null);
const domain = useDomain();
const [isVisible, setIsVisible] = useState(false);
const [symbolElement, setSymbolElement] = useState<HTMLElement | null>(null);
const symbolName = useMemo(() => {
return (symbolElement && symbolElement.textContent) ?? undefined;
}, [symbolElement]);
const { data: symbolDefinitions, isLoading: isSymbolDefinitionsLoading } = useQuery({
queryKey: ["definitions", symbolName, revisionName, language, domain],
queryFn: () => unwrapServiceError(
findSearchBasedSymbolDefinitions({
symbolName: symbolName!,
language,
revisionName,
}, domain)
),
select: ((data) => {
return data.files.flatMap((file) => {
return file.matches.map((match) => {
return {
lineContent: match.lineContent,
language: file.language,
fileName: file.fileName,
repoName: file.repository,
range: match.range,
}
})
})
}),
enabled: !!symbolName,
staleTime: Infinity,
})
const clearTimers = useCallback(() => {
if (mouseOverTimerRef.current) {
clearTimeout(mouseOverTimerRef.current);
}
if (mouseOutTimerRef.current) {
clearTimeout(mouseOutTimerRef.current);
}
}, []);
useEffect(() => {
const view = editorRef.view;
if (!view) {
return;
}
const handleMouseOver = (event: MouseEvent) => {
const target = (event.target as HTMLElement).closest(`[${SYMBOL_HOVER_TARGET_DATA_ATTRIBUTE}="true"]`) as HTMLElement;
if (!target) {
return;
}
clearTimers();
setSymbolElement(target);
mouseOverTimerRef.current = setTimeout(() => {
setIsVisible(true);
}, SYMBOL_HOVER_POPUP_MOUSE_OVER_TIMEOUT_MS);
};
const handleMouseOut = () => {
clearTimers();
mouseOutTimerRef.current = setTimeout(() => {
setIsVisible(false);
}, SYMBOL_HOVER_POPUP_MOUSE_OUT_TIMEOUT_MS);
};
view.dom.addEventListener("mouseover", handleMouseOver);
view.dom.addEventListener("mouseout", handleMouseOut);
return () => {
view.dom.removeEventListener("mouseover", handleMouseOver);
view.dom.removeEventListener("mouseout", handleMouseOut);
};
}, [editorRef, domain, clearTimers]);
if (!isVisible && !isSticky) {
return undefined;
}
if (!symbolElement || !symbolName) {
return undefined;
}
return {
element: symbolElement,
symbolName,
isSymbolDefinitionsLoading: isSymbolDefinitionsLoading,
symbolDefinitions,
};
}