2025-06-06 19:38:16 +00:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
|
|
|
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
|
|
|
|
|
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
|
|
|
|
|
import { SymbolDefinition } from "@/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo";
|
|
|
|
|
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
|
|
|
|
|
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
|
|
|
|
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
|
|
|
|
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
|
|
|
|
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
|
|
|
|
import { search } from "@codemirror/search";
|
|
|
|
|
import CodeMirror, { EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, ViewUpdate } from "@uiw/react-codemirror";
|
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
|
|
import { EditorContextMenu } from "../../../components/editorContextMenu";
|
|
|
|
|
import { BrowseHighlightRange, HIGHLIGHT_RANGE_QUERY_PARAM, useBrowseNavigation } from "../../hooks/useBrowseNavigation";
|
|
|
|
|
import { useBrowseState } from "../../hooks/useBrowseState";
|
|
|
|
|
import { rangeHighlightingExtension } from "./rangeHighlightingExtension";
|
|
|
|
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
2025-06-20 21:57:05 +00:00
|
|
|
import { createAuditAction } from "@/ee/features/audit/actions";
|
|
|
|
|
import { useDomain } from "@/hooks/useDomain";
|
2025-06-06 19:38:16 +00:00
|
|
|
|
|
|
|
|
interface PureCodePreviewPanelProps {
|
|
|
|
|
path: string;
|
|
|
|
|
repoName: string;
|
|
|
|
|
revisionName: string;
|
|
|
|
|
source: string;
|
|
|
|
|
language: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const PureCodePreviewPanel = ({
|
|
|
|
|
source,
|
|
|
|
|
language,
|
|
|
|
|
path,
|
|
|
|
|
repoName,
|
|
|
|
|
revisionName,
|
|
|
|
|
}: PureCodePreviewPanelProps) => {
|
|
|
|
|
const [editorRef, setEditorRef] = useState<ReactCodeMirrorRef | null>(null);
|
|
|
|
|
const languageExtension = useCodeMirrorLanguageExtension(language, editorRef?.view);
|
|
|
|
|
const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
|
|
|
|
|
const keymapExtension = useKeymapExtension(editorRef?.view);
|
|
|
|
|
const hasCodeNavEntitlement = useHasEntitlement("code-nav");
|
|
|
|
|
const { updateBrowseState } = useBrowseState();
|
|
|
|
|
const { navigateToPath } = useBrowseNavigation();
|
2025-06-20 21:57:05 +00:00
|
|
|
const domain = useDomain();
|
2025-06-06 19:38:16 +00:00
|
|
|
const captureEvent = useCaptureEvent();
|
|
|
|
|
|
|
|
|
|
const highlightRangeQuery = useNonEmptyQueryParam(HIGHLIGHT_RANGE_QUERY_PARAM);
|
|
|
|
|
const highlightRange = useMemo((): BrowseHighlightRange | undefined => {
|
|
|
|
|
if (!highlightRangeQuery) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Highlight ranges can be formatted in two ways:
|
|
|
|
|
// 1. start_line,end_line (no column specified)
|
|
|
|
|
// 2. start_line:start_column,end_line:end_column (column specified)
|
|
|
|
|
const rangeRegex = /^(\d+:\d+,\d+:\d+|\d+,\d+)$/;
|
|
|
|
|
if (!rangeRegex.test(highlightRangeQuery)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [start, end] = highlightRangeQuery.split(',').map((range) => {
|
|
|
|
|
if (range.includes(':')) {
|
|
|
|
|
return range.split(':').map((val) => parseInt(val, 10));
|
|
|
|
|
}
|
|
|
|
|
// For line-only format, use column 1 for start and last column for end
|
|
|
|
|
const line = parseInt(range, 10);
|
|
|
|
|
return [line];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (start.length === 1 || end.length === 1) {
|
|
|
|
|
return {
|
|
|
|
|
start: {
|
|
|
|
|
lineNumber: start[0],
|
|
|
|
|
},
|
|
|
|
|
end: {
|
|
|
|
|
lineNumber: end[0],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return {
|
|
|
|
|
start: {
|
|
|
|
|
lineNumber: start[0],
|
|
|
|
|
column: start[1],
|
|
|
|
|
},
|
|
|
|
|
end: {
|
|
|
|
|
lineNumber: end[0],
|
|
|
|
|
column: end[1],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}, [highlightRangeQuery]);
|
|
|
|
|
|
|
|
|
|
const extensions = useMemo(() => {
|
|
|
|
|
return [
|
|
|
|
|
languageExtension,
|
|
|
|
|
EditorView.lineWrapping,
|
|
|
|
|
keymapExtension,
|
|
|
|
|
search({
|
|
|
|
|
top: true,
|
|
|
|
|
}),
|
|
|
|
|
EditorView.updateListener.of((update: ViewUpdate) => {
|
|
|
|
|
if (update.selectionSet) {
|
|
|
|
|
setCurrentSelection(update.state.selection.main);
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
highlightRange ? rangeHighlightingExtension(highlightRange) : [],
|
|
|
|
|
hasCodeNavEntitlement ? symbolHoverTargetsExtension : [],
|
|
|
|
|
];
|
|
|
|
|
}, [
|
|
|
|
|
keymapExtension,
|
|
|
|
|
languageExtension,
|
|
|
|
|
highlightRange,
|
|
|
|
|
hasCodeNavEntitlement,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Scroll the highlighted range into view.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!highlightRange || !editorRef || !editorRef.state) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const doc = editorRef.state.doc;
|
|
|
|
|
const { start, end } = highlightRange;
|
|
|
|
|
const selection = EditorSelection.range(
|
|
|
|
|
doc.line(start.lineNumber).from,
|
|
|
|
|
doc.line(end.lineNumber).from,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
editorRef.view?.dispatch({
|
|
|
|
|
effects: [
|
|
|
|
|
EditorView.scrollIntoView(selection, { y: "center" }),
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
}, [editorRef, highlightRange]);
|
|
|
|
|
|
|
|
|
|
const onFindReferences = useCallback((symbolName: string) => {
|
2025-07-26 01:34:33 +00:00
|
|
|
captureEvent('wa_find_references_pressed', {
|
|
|
|
|
source: 'browse',
|
|
|
|
|
});
|
2025-06-20 21:57:05 +00:00
|
|
|
createAuditAction({
|
|
|
|
|
action: "user.performed_find_references",
|
|
|
|
|
metadata: {
|
|
|
|
|
message: symbolName,
|
|
|
|
|
},
|
|
|
|
|
}, domain)
|
2025-06-06 19:38:16 +00:00
|
|
|
|
|
|
|
|
updateBrowseState({
|
|
|
|
|
selectedSymbolInfo: {
|
|
|
|
|
repoName,
|
|
|
|
|
symbolName,
|
|
|
|
|
revisionName,
|
|
|
|
|
language,
|
|
|
|
|
},
|
|
|
|
|
isBottomPanelCollapsed: false,
|
|
|
|
|
activeExploreMenuTab: "references",
|
|
|
|
|
})
|
2025-06-20 21:57:05 +00:00
|
|
|
}, [captureEvent, updateBrowseState, repoName, revisionName, language, domain]);
|
2025-06-06 19:38:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// If we resolve multiple matches, instead of navigating to the first match, we should
|
|
|
|
|
// instead popup the bottom sheet with the list of matches.
|
|
|
|
|
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
|
2025-07-26 01:34:33 +00:00
|
|
|
captureEvent('wa_goto_definition_pressed', {
|
|
|
|
|
source: 'browse',
|
|
|
|
|
});
|
2025-06-20 21:57:05 +00:00
|
|
|
createAuditAction({
|
|
|
|
|
action: "user.performed_goto_definition",
|
|
|
|
|
metadata: {
|
|
|
|
|
message: symbolName,
|
|
|
|
|
},
|
|
|
|
|
}, domain)
|
2025-06-06 19:38:16 +00:00
|
|
|
|
|
|
|
|
if (symbolDefinitions.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (symbolDefinitions.length === 1) {
|
|
|
|
|
const symbolDefinition = symbolDefinitions[0];
|
|
|
|
|
const { fileName, repoName } = symbolDefinition;
|
|
|
|
|
|
|
|
|
|
navigateToPath({
|
|
|
|
|
repoName,
|
|
|
|
|
revisionName,
|
|
|
|
|
path: fileName,
|
|
|
|
|
pathType: 'blob',
|
|
|
|
|
highlightRange: symbolDefinition.range,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
updateBrowseState({
|
|
|
|
|
selectedSymbolInfo: {
|
|
|
|
|
symbolName,
|
|
|
|
|
repoName,
|
|
|
|
|
revisionName,
|
|
|
|
|
language,
|
|
|
|
|
},
|
|
|
|
|
activeExploreMenuTab: "definitions",
|
|
|
|
|
isBottomPanelCollapsed: false,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-06-20 21:57:05 +00:00
|
|
|
}, [captureEvent, navigateToPath, revisionName, updateBrowseState, repoName, language, domain]);
|
2025-06-06 19:38:16 +00:00
|
|
|
|
|
|
|
|
const theme = useCodeMirrorTheme();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ScrollArea className="h-full overflow-auto flex-1">
|
|
|
|
|
<CodeMirror
|
|
|
|
|
className="relative"
|
|
|
|
|
ref={setEditorRef}
|
|
|
|
|
value={source}
|
|
|
|
|
extensions={extensions}
|
|
|
|
|
readOnly={true}
|
|
|
|
|
theme={theme}
|
|
|
|
|
>
|
|
|
|
|
{editorRef && editorRef.view && currentSelection && (
|
|
|
|
|
<EditorContextMenu
|
|
|
|
|
view={editorRef.view}
|
|
|
|
|
selection={currentSelection}
|
|
|
|
|
repoName={repoName}
|
|
|
|
|
path={path}
|
|
|
|
|
revisionName={revisionName}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{editorRef && hasCodeNavEntitlement && (
|
|
|
|
|
<SymbolHoverPopup
|
|
|
|
|
editorRef={editorRef}
|
|
|
|
|
revisionName={revisionName}
|
|
|
|
|
language={language}
|
|
|
|
|
onFindReferences={onFindReferences}
|
|
|
|
|
onGotoDefinition={onGotoDefinition}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</CodeMirror>
|
|
|
|
|
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|