-
Notifications
You must be signed in to change notification settings - Fork 106
feature: File explorer #336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
3b3a393
wip
brendan-kellam b5ff755
tree rendering
brendan-kellam 0af6369
wip
brendan-kellam fc26a47
savepoint: refactored a bunch of stuff to allow us to persist the fil…
brendan-kellam 66ce25b
Collapsible state + more style improvements
brendan-kellam 4250553
make file tree keyboard nav work
brendan-kellam df9243b
further wip: added useBrowseParams improved UX, etc
brendan-kellam 9d5df91
fix scrolling behaviour
brendan-kellam 8db87ef
prefetch source files on hover over reference
brendan-kellam e780223
Add file header to source view
brendan-kellam a46c085
Add file tree preview
brendan-kellam 3402d6d
breadcrumb system
brendan-kellam b6867d2
fix scroll issue. Put re-used prefetching logic into shared hooks
brendan-kellam 4aca32e
fix issue with paths that have spaces
brendan-kellam c2d8ec8
Make breadcumb paths responsive by collapsing long paths with an elli…
brendan-kellam a6b3f2d
changelog
brendan-kellam 2b8fce2
Add news data
brendan-kellam 3b14032
feedback
brendan-kellam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
303 changes: 88 additions & 215 deletions
303
packages/web/src/app/[domain]/browse/[...path]/components/codePreviewPanel.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,226 +1,99 @@ | ||
'use client'; | ||
|
||
import { ResizablePanel } from "@/components/ui/resizable"; | ||
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"; | ||
|
||
interface CodePreviewPanelProps { | ||
path: string; | ||
repoName: string; | ||
revisionName: string; | ||
source: string; | ||
language: string; | ||
} | ||
|
||
export const CodePreviewPanel = ({ | ||
source, | ||
language, | ||
path, | ||
repoName, | ||
revisionName, | ||
}: CodePreviewPanelProps) => { | ||
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(); | ||
const captureEvent = useCaptureEvent(); | ||
|
||
const highlightRangeQuery = useNonEmptyQueryParam(HIGHLIGHT_RANGE_QUERY_PARAM); | ||
const highlightRange = useMemo((): BrowseHighlightRange | undefined => { | ||
if (!highlightRangeQuery) { | ||
return; | ||
import { base64Decode, getCodeHostInfoForRepo, unwrapServiceError } from "@/lib/utils"; | ||
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams"; | ||
import { useQuery } from "@tanstack/react-query"; | ||
import { getFileSource } from "@/features/search/fileSourceApi"; | ||
import { useDomain } from "@/hooks/useDomain"; | ||
import { Loader2 } from "lucide-react"; | ||
import { Separator } from "@/components/ui/separator"; | ||
import { getRepoInfoByName } from "@/actions"; | ||
import { cn } from "@/lib/utils"; | ||
import Image from "next/image"; | ||
import { useMemo } from "react"; | ||
import { PureCodePreviewPanel } from "./pureCodePreviewPanel"; | ||
import { PathHeader } from "@/app/[domain]/components/pathHeader"; | ||
|
||
export const CodePreviewPanel = () => { | ||
const { path, repoName, revisionName } = useBrowseParams(); | ||
const domain = useDomain(); | ||
|
||
const { data: fileSourceResponse, isPending: isFileSourcePending, isError: isFileSourceError } = useQuery({ | ||
queryKey: ['fileSource', repoName, revisionName, path, domain], | ||
queryFn: () => unwrapServiceError(getFileSource({ | ||
fileName: path, | ||
repository: repoName, | ||
branch: revisionName | ||
}, domain)), | ||
}); | ||
|
||
const { data: repoInfoResponse, isPending: isRepoInfoPending, isError: isRepoInfoError } = useQuery({ | ||
queryKey: ['repoInfo', repoName, domain], | ||
queryFn: () => unwrapServiceError(getRepoInfoByName(repoName, domain)), | ||
}); | ||
|
||
const codeHostInfo = useMemo(() => { | ||
if (!repoInfoResponse) { | ||
return undefined; | ||
} | ||
|
||
// 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]; | ||
return getCodeHostInfoForRepo({ | ||
codeHostType: repoInfoResponse.codeHostType, | ||
name: repoInfoResponse.name, | ||
displayName: repoInfoResponse.displayName, | ||
webUrl: repoInfoResponse.webUrl, | ||
}); | ||
}, [repoInfoResponse]); | ||
|
||
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]); | ||
if (isFileSourcePending || isRepoInfoPending) { | ||
return ( | ||
<div className="flex flex-col w-full min-h-full items-center justify-center"> | ||
<Loader2 className="w-4 h-4 animate-spin" /> | ||
Loading... | ||
</div> | ||
) | ||
} | ||
|
||
const onFindReferences = useCallback((symbolName: string) => { | ||
captureEvent('wa_browse_find_references_pressed', {}); | ||
|
||
updateBrowseState({ | ||
selectedSymbolInfo: { | ||
repoName, | ||
symbolName, | ||
revisionName, | ||
language, | ||
}, | ||
isBottomPanelCollapsed: false, | ||
activeExploreMenuTab: "references", | ||
}) | ||
}, [captureEvent, updateBrowseState, repoName, revisionName, language]); | ||
|
||
|
||
// 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[]) => { | ||
captureEvent('wa_browse_goto_definition_pressed', {}); | ||
|
||
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, | ||
}) | ||
} | ||
}, [captureEvent, navigateToPath, revisionName, updateBrowseState, repoName, language]); | ||
|
||
const theme = useCodeMirrorTheme(); | ||
if (isFileSourceError || isRepoInfoError) { | ||
return <div>Error loading file source</div> | ||
} | ||
brendan-kellam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return ( | ||
<ResizablePanel | ||
order={1} | ||
id={"code-preview-panel"} | ||
> | ||
<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} | ||
<> | ||
<div className="flex flex-row py-1 px-2 items-center justify-between"> | ||
<PathHeader | ||
path={path} | ||
repo={{ | ||
name: repoName, | ||
codeHostType: repoInfoResponse.codeHostType, | ||
displayName: repoInfoResponse.displayName, | ||
webUrl: repoInfoResponse.webUrl, | ||
}} | ||
/> | ||
{(fileSourceResponse.webUrl && codeHostInfo) && ( | ||
<a | ||
href={fileSourceResponse.webUrl} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
className="flex flex-row items-center gap-2 px-2 py-0.5 rounded-md flex-shrink-0" | ||
> | ||
<Image | ||
src={codeHostInfo.icon} | ||
alt={codeHostInfo.codeHostName} | ||
className={cn('w-4 h-4 flex-shrink-0', codeHostInfo.iconClassName)} | ||
/> | ||
)} | ||
{editorRef && hasCodeNavEntitlement && ( | ||
<SymbolHoverPopup | ||
editorRef={editorRef} | ||
revisionName={revisionName} | ||
language={language} | ||
onFindReferences={onFindReferences} | ||
onGotoDefinition={onGotoDefinition} | ||
/> | ||
)} | ||
</CodeMirror> | ||
|
||
</ScrollArea> | ||
</ResizablePanel> | ||
<span className="text-sm font-medium">Open in {codeHostInfo.codeHostName}</span> | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</a> | ||
)} | ||
</div> | ||
<Separator /> | ||
<PureCodePreviewPanel | ||
source={base64Decode(fileSourceResponse.source)} | ||
language={fileSourceResponse.language} | ||
repoName={repoName} | ||
path={path} | ||
revisionName={revisionName ?? 'HEAD'} | ||
/> | ||
</> | ||
) | ||
} | ||
|
||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.