mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
chore(web): Scope code nav to current repository by default (#647)
Some checks are pending
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Waiting to run
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Waiting to run
Publish to ghcr / merge (push) Blocked by required conditions
Update Roadmap Released / update (push) Waiting to run
Some checks are pending
Publish to ghcr / build (linux/amd64, blacksmith-4vcpu-ubuntu-2404) (push) Waiting to run
Publish to ghcr / build (linux/arm64, blacksmith-8vcpu-ubuntu-2204-arm) (push) Waiting to run
Publish to ghcr / merge (push) Blocked by required conditions
Update Roadmap Released / update (push) Waiting to run
This commit is contained in:
parent
28986f4355
commit
92578881df
16 changed files with 396 additions and 347 deletions
|
|
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Added `ALWAYS_INDEX_FILE_PATTERNS` environment variable to allow specifying a comma seperated list of glob patterns matching file paths that should always be indexed, regardless of size or # of trigrams. [#631](https://github.com/sourcebot-dev/sourcebot/pull/631)
|
- Added `ALWAYS_INDEX_FILE_PATTERNS` environment variable to allow specifying a comma seperated list of glob patterns matching file paths that should always be indexed, regardless of size or # of trigrams. [#631](https://github.com/sourcebot-dev/sourcebot/pull/631)
|
||||||
|
- Added button to explore menu to toggle cross-repository search. [#647](https://github.com/sourcebot-dev/sourcebot/pull/647)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed issue where single quotes could not be used in search queries. [#629](https://github.com/sourcebot-dev/sourcebot/pull/629)
|
- Fixed issue where single quotes could not be used in search queries. [#629](https://github.com/sourcebot-dev/sourcebot/pull/629)
|
||||||
|
|
@ -16,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Fixed Ask performance issues. [#632](https://github.com/sourcebot-dev/sourcebot/pull/632)
|
- Fixed Ask performance issues. [#632](https://github.com/sourcebot-dev/sourcebot/pull/632)
|
||||||
- Fixed regression where creating a new Ask thread when unauthenticated would result in a 404. [#641](https://github.com/sourcebot-dev/sourcebot/pull/641)
|
- Fixed regression where creating a new Ask thread when unauthenticated would result in a 404. [#641](https://github.com/sourcebot-dev/sourcebot/pull/641)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Changed the default behaviour for code nav to scope references & definitions search to the current repository. [#647](https://github.com/sourcebot-dev/sourcebot/pull/647)
|
||||||
|
|
||||||
## [4.10.0] - 2025-11-24
|
## [4.10.0] - 2025-11-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import LicenseKeyRequired from '/snippets/license-key-required.mdx'
|
||||||
| **Go to definition** | Clicking the "go to definition" button in the popover or clicking the symbol name navigates to the symbol's definition. |
|
| **Go to definition** | Clicking the "go to definition" button in the popover or clicking the symbol name navigates to the symbol's definition. |
|
||||||
| **Find references** | Clicking the "find all references" button in the popover lists all references in the explore panel. |
|
| **Find references** | Clicking the "find all references" button in the popover lists all references in the explore panel. |
|
||||||
| **Explore panel** | Lists all references and definitions for the symbol selected in the popover. |
|
| **Explore panel** | Lists all references and definitions for the symbol selected in the popover. |
|
||||||
|
| **Cross-repository navigation** | You can search across all repositories by clicking the globe icon in the explore panel. By default, references and definitions are scoped to the repository where the symbol is being resolved. |
|
||||||
|
|
||||||
## How does it work?
|
## How does it work?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
|
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
|
||||||
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
|
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 { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
|
||||||
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
||||||
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
||||||
|
|
@ -11,14 +10,10 @@ import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
||||||
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||||
import { search } from "@codemirror/search";
|
import { search } from "@codemirror/search";
|
||||||
import CodeMirror, { EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, ViewUpdate } from "@uiw/react-codemirror";
|
import CodeMirror, { EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, ViewUpdate } from "@uiw/react-codemirror";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { EditorContextMenu } from "../../../components/editorContextMenu";
|
import { EditorContextMenu } from "../../../components/editorContextMenu";
|
||||||
import { useBrowseNavigation } from "../../hooks/useBrowseNavigation";
|
|
||||||
import { BrowseHighlightRange, HIGHLIGHT_RANGE_QUERY_PARAM } from "../../hooks/utils";
|
import { BrowseHighlightRange, HIGHLIGHT_RANGE_QUERY_PARAM } from "../../hooks/utils";
|
||||||
import { useBrowseState } from "../../hooks/useBrowseState";
|
|
||||||
import { rangeHighlightingExtension } from "./rangeHighlightingExtension";
|
import { rangeHighlightingExtension } from "./rangeHighlightingExtension";
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
|
||||||
import { createAuditAction } from "@/ee/features/audit/actions";
|
|
||||||
|
|
||||||
interface PureCodePreviewPanelProps {
|
interface PureCodePreviewPanelProps {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -40,9 +35,6 @@ export const PureCodePreviewPanel = ({
|
||||||
const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
|
const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
|
||||||
const keymapExtension = useKeymapExtension(editorRef?.view);
|
const keymapExtension = useKeymapExtension(editorRef?.view);
|
||||||
const hasCodeNavEntitlement = useHasEntitlement("code-nav");
|
const hasCodeNavEntitlement = useHasEntitlement("code-nav");
|
||||||
const { updateBrowseState } = useBrowseState();
|
|
||||||
const { navigateToPath } = useBrowseNavigation();
|
|
||||||
const captureEvent = useCaptureEvent();
|
|
||||||
|
|
||||||
const highlightRangeQuery = useNonEmptyQueryParam(HIGHLIGHT_RANGE_QUERY_PARAM);
|
const highlightRangeQuery = useNonEmptyQueryParam(HIGHLIGHT_RANGE_QUERY_PARAM);
|
||||||
const highlightRange = useMemo((): BrowseHighlightRange | undefined => {
|
const highlightRange = useMemo((): BrowseHighlightRange | undefined => {
|
||||||
|
|
@ -88,7 +80,6 @@ export const PureCodePreviewPanel = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [highlightRangeQuery]);
|
}, [highlightRangeQuery]);
|
||||||
|
|
||||||
const extensions = useMemo(() => {
|
const extensions = useMemo(() => {
|
||||||
|
|
@ -116,90 +107,31 @@ export const PureCodePreviewPanel = ({
|
||||||
|
|
||||||
// Scroll the highlighted range into view.
|
// Scroll the highlighted range into view.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!highlightRange || !editorRef || !editorRef.state) {
|
if (!highlightRange || !editorRef || !editorRef.state || !editorRef.view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = editorRef.state.doc;
|
const doc = editorRef.state.doc;
|
||||||
const { start, end } = highlightRange;
|
const { start, end } = highlightRange;
|
||||||
const selection = EditorSelection.range(
|
|
||||||
doc.line(start.lineNumber).from,
|
|
||||||
doc.line(end.lineNumber).from,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const from = doc.line(start.lineNumber).from;
|
||||||
|
const to = doc.line(end.lineNumber).to;
|
||||||
|
const selection = EditorSelection.range(from, to);
|
||||||
|
|
||||||
|
// When the selection is in view, we don't want to perform any scrolling
|
||||||
|
// as it could be jarring for the user. If it is not in view, scroll to the
|
||||||
|
// center of the viewport.
|
||||||
|
const viewport = editorRef.view.viewport;
|
||||||
|
const isInView = from >= viewport.from && to <= viewport.to;
|
||||||
|
const scrollStrategy = isInView ? "nearest" : "center";
|
||||||
|
|
||||||
editorRef.view?.dispatch({
|
editorRef.view?.dispatch({
|
||||||
effects: [
|
effects: [
|
||||||
EditorView.scrollIntoView(selection, { y: "center" }),
|
EditorView.scrollIntoView(selection, { y: scrollStrategy }),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}, [editorRef, highlightRange]);
|
}, [editorRef, highlightRange]);
|
||||||
|
|
||||||
const onFindReferences = useCallback((symbolName: string) => {
|
|
||||||
captureEvent('wa_find_references_pressed', {
|
|
||||||
source: 'browse',
|
|
||||||
});
|
|
||||||
createAuditAction({
|
|
||||||
action: "user.performed_find_references",
|
|
||||||
metadata: {
|
|
||||||
message: symbolName,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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_goto_definition_pressed', {
|
|
||||||
source: 'browse',
|
|
||||||
});
|
|
||||||
createAuditAction({
|
|
||||||
action: "user.performed_goto_definition",
|
|
||||||
metadata: {
|
|
||||||
message: symbolName,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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();
|
const theme = useCodeMirrorTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -223,11 +155,12 @@ export const PureCodePreviewPanel = ({
|
||||||
)}
|
)}
|
||||||
{editorRef && hasCodeNavEntitlement && (
|
{editorRef && hasCodeNavEntitlement && (
|
||||||
<SymbolHoverPopup
|
<SymbolHoverPopup
|
||||||
|
source="preview"
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
revisionName={revisionName}
|
revisionName={revisionName}
|
||||||
language={language}
|
language={language}
|
||||||
onFindReferences={onFindReferences}
|
fileName={path}
|
||||||
onGotoDefinition={onGotoDefinition}
|
repoName={repoName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CodeMirror>
|
</CodeMirror>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { useBrowseParams } from "./hooks/useBrowseParams";
|
||||||
import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog";
|
import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { SearchBar } from "../components/searchBar";
|
import { SearchBar } from "../components/searchBar";
|
||||||
|
import escapeStringRegexp from "escape-string-regexp";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -30,7 +31,7 @@ export default function Layout({
|
||||||
<SearchBar
|
<SearchBar
|
||||||
size="sm"
|
size="sm"
|
||||||
defaults={{
|
defaults={{
|
||||||
query: `repo:${repoName}${revisionName ? ` rev:${revisionName}` : ''} `,
|
query: `repo:^${escapeStringRegexp(repoName)}$${revisionName ? ` rev:${revisionName}` : ''} `,
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
|
||||||
import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu";
|
import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
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 { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
|
||||||
import { SearchResultChunk } from "@/features/search";
|
import { SearchResultChunk } from "@/features/search";
|
||||||
|
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
||||||
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
||||||
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
||||||
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
|
||||||
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
|
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
|
||||||
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
|
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
|
||||||
import { search } from "@codemirror/search";
|
import { search } from "@codemirror/search";
|
||||||
|
|
@ -16,13 +20,6 @@ import { Scrollbar } from "@radix-ui/react-scroll-area";
|
||||||
import CodeMirror, { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
import CodeMirror, { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
||||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||||
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
|
|
||||||
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
|
|
||||||
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
|
|
||||||
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
|
|
||||||
import { SymbolDefinition } from "@/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo";
|
|
||||||
import { createAuditAction } from "@/ee/features/audit/actions";
|
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
|
||||||
|
|
||||||
export interface CodePreviewFile {
|
export interface CodePreviewFile {
|
||||||
content: string;
|
content: string;
|
||||||
|
|
@ -59,8 +56,6 @@ export const CodePreview = ({
|
||||||
const languageExtension = useCodeMirrorLanguageExtension(file?.language ?? '', editorRef?.view);
|
const languageExtension = useCodeMirrorLanguageExtension(file?.language ?? '', editorRef?.view);
|
||||||
const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
|
const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
|
||||||
|
|
||||||
const captureEvent = useCaptureEvent();
|
|
||||||
|
|
||||||
const extensions = useMemo(() => {
|
const extensions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
keymapExtension,
|
keymapExtension,
|
||||||
|
|
@ -115,81 +110,6 @@ export const CodePreview = ({
|
||||||
onSelectedMatchIndexChange((prev) => prev + 1);
|
onSelectedMatchIndexChange((prev) => prev + 1);
|
||||||
}, [onSelectedMatchIndexChange]);
|
}, [onSelectedMatchIndexChange]);
|
||||||
|
|
||||||
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
|
|
||||||
captureEvent('wa_goto_definition_pressed', {
|
|
||||||
source: 'preview',
|
|
||||||
});
|
|
||||||
createAuditAction({
|
|
||||||
action: "user.performed_goto_definition",
|
|
||||||
metadata: {
|
|
||||||
message: symbolName,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (symbolDefinitions.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (symbolDefinitions.length === 1) {
|
|
||||||
const symbolDefinition = symbolDefinitions[0];
|
|
||||||
const { fileName, repoName } = symbolDefinition;
|
|
||||||
|
|
||||||
navigateToPath({
|
|
||||||
repoName,
|
|
||||||
revisionName: file.revision,
|
|
||||||
path: fileName,
|
|
||||||
pathType: 'blob',
|
|
||||||
highlightRange: symbolDefinition.range,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
navigateToPath({
|
|
||||||
repoName,
|
|
||||||
revisionName: file.revision,
|
|
||||||
path: file.filepath,
|
|
||||||
pathType: 'blob',
|
|
||||||
setBrowseState: {
|
|
||||||
selectedSymbolInfo: {
|
|
||||||
symbolName,
|
|
||||||
repoName,
|
|
||||||
revisionName: file.revision,
|
|
||||||
language: file.language,
|
|
||||||
},
|
|
||||||
activeExploreMenuTab: "definitions",
|
|
||||||
isBottomPanelCollapsed: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [captureEvent, file.filepath, file.language, file.revision, navigateToPath, repoName]);
|
|
||||||
|
|
||||||
const onFindReferences = useCallback((symbolName: string) => {
|
|
||||||
captureEvent('wa_find_references_pressed', {
|
|
||||||
source: 'preview',
|
|
||||||
});
|
|
||||||
createAuditAction({
|
|
||||||
action: "user.performed_find_references",
|
|
||||||
metadata: {
|
|
||||||
message: symbolName,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
navigateToPath({
|
|
||||||
repoName,
|
|
||||||
revisionName: file.revision,
|
|
||||||
path: file.filepath,
|
|
||||||
pathType: 'blob',
|
|
||||||
setBrowseState: {
|
|
||||||
selectedSymbolInfo: {
|
|
||||||
repoName,
|
|
||||||
symbolName,
|
|
||||||
revisionName: file.revision,
|
|
||||||
language: file.language,
|
|
||||||
},
|
|
||||||
activeExploreMenuTab: "references",
|
|
||||||
isBottomPanelCollapsed: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [captureEvent, file.filepath, file.language, file.revision, navigateToPath, repoName]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex flex-row bg-accent items-center justify-between pr-3 py-0.5 mt-7">
|
<div className="flex flex-row bg-accent items-center justify-between pr-3 py-0.5 mt-7">
|
||||||
|
|
@ -286,11 +206,12 @@ export const CodePreview = ({
|
||||||
|
|
||||||
{editorRef && hasCodeNavEntitlement && (
|
{editorRef && hasCodeNavEntitlement && (
|
||||||
<SymbolHoverPopup
|
<SymbolHoverPopup
|
||||||
|
source="preview"
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
language={file.language}
|
language={file.language}
|
||||||
revisionName={file.revision}
|
revisionName={file.revision}
|
||||||
onFindReferences={onFindReferences}
|
fileName={file.filepath}
|
||||||
onGotoDefinition={onGotoDefinition}
|
repoName={repoName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CodeMirror>
|
</CodeMirror>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
interface KeyboardShortcutHintProps {
|
interface KeyboardShortcutHintProps {
|
||||||
shortcut: string
|
shortcut: string
|
||||||
label?: string
|
label?: string
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KeyboardShortcutHint({ shortcut, label }: KeyboardShortcutHintProps) {
|
export function KeyboardShortcutHint({ shortcut, label, className }: KeyboardShortcutHintProps) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center" aria-label={label || `Keyboard shortcut: ${shortcut}`}>
|
<div className={cn("inline-flex items-center", className)} aria-label={label || `Keyboard shortcut: ${shortcut}`}>
|
||||||
<kbd
|
<kbd
|
||||||
className="px-2 py-1 font-semibold font-sans border rounded-md"
|
className="px-2 py-1 font-semibold font-sans border rounded-md"
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const toggleVariants = cva(
|
const toggleVariants = cva(
|
||||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2",
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2 cursor-pointer",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|
@ -16,9 +16,7 @@ const toggleVariants = cva(
|
||||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-10 px-3 min-w-10",
|
default: "h-7 w-7 min-w-7 p-0",
|
||||||
sm: "h-9 px-2.5 min-w-9",
|
|
||||||
lg: "h-11 px-5 min-w-11",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState";
|
import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState";
|
||||||
import { findSearchBasedSymbolReferences, findSearchBasedSymbolDefinitions} from "@/app/api/(client)/client";
|
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "@/app/api/(client)/client";
|
||||||
import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle";
|
import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||||
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { measure, unwrapServiceError } from "@/lib/utils";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Loader2 } from "lucide-react";
|
import { GlobeIcon, Loader2 } from "lucide-react";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { VscSymbolMisc } from "react-icons/vsc";
|
import { VscSymbolMisc } from "react-icons/vsc";
|
||||||
import { ReferenceList } from "./referenceList";
|
import { ReferenceList } from "./referenceList";
|
||||||
|
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
||||||
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
|
|
||||||
interface ExploreMenuProps {
|
interface ExploreMenuProps {
|
||||||
selectedSymbolInfo: {
|
selectedSymbolInfo: {
|
||||||
|
|
@ -27,27 +31,39 @@ interface ExploreMenuProps {
|
||||||
export const ExploreMenu = ({
|
export const ExploreMenu = ({
|
||||||
selectedSymbolInfo,
|
selectedSymbolInfo,
|
||||||
}: ExploreMenuProps) => {
|
}: ExploreMenuProps) => {
|
||||||
|
|
||||||
const domain = useDomain();
|
const domain = useDomain();
|
||||||
|
const captureEvent = useCaptureEvent();
|
||||||
const {
|
const {
|
||||||
state: { activeExploreMenuTab },
|
state: { activeExploreMenuTab },
|
||||||
updateBrowseState,
|
updateBrowseState,
|
||||||
} = useBrowseState();
|
} = useBrowseState();
|
||||||
|
|
||||||
|
const [isGlobalSearchEnabled, setIsGlobalSearchEnabled] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: referencesResponse,
|
data: referencesResponse,
|
||||||
isError: isReferencesResponseError,
|
isError: isReferencesResponseError,
|
||||||
isPending: isReferencesResponsePending,
|
isPending: isReferencesResponsePending,
|
||||||
isLoading: isReferencesResponseLoading,
|
isLoading: isReferencesResponseLoading,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["references", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain],
|
queryKey: ["references", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain, isGlobalSearchEnabled],
|
||||||
queryFn: () => unwrapServiceError(
|
queryFn: async () => {
|
||||||
findSearchBasedSymbolReferences({
|
const response = await measure(() => unwrapServiceError(
|
||||||
symbolName: selectedSymbolInfo.symbolName,
|
findSearchBasedSymbolReferences({
|
||||||
language: selectedSymbolInfo.language,
|
symbolName: selectedSymbolInfo.symbolName,
|
||||||
revisionName: selectedSymbolInfo.revisionName,
|
language: selectedSymbolInfo.language,
|
||||||
|
revisionName: selectedSymbolInfo.revisionName,
|
||||||
|
repoName: isGlobalSearchEnabled ? undefined : selectedSymbolInfo.repoName
|
||||||
|
})
|
||||||
|
), 'findSearchBasedSymbolReferences', false);
|
||||||
|
|
||||||
|
captureEvent('wa_explore_menu_references_loaded', {
|
||||||
|
durationMs: response.durationMs,
|
||||||
|
isGlobalSearchEnabled,
|
||||||
})
|
})
|
||||||
),
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -56,14 +72,32 @@ export const ExploreMenu = ({
|
||||||
isPending: isDefinitionsResponsePending,
|
isPending: isDefinitionsResponsePending,
|
||||||
isLoading: isDefinitionsResponseLoading,
|
isLoading: isDefinitionsResponseLoading,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["definitions", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain],
|
queryKey: ["definitions", selectedSymbolInfo.symbolName, selectedSymbolInfo.repoName, selectedSymbolInfo.revisionName, selectedSymbolInfo.language, domain, isGlobalSearchEnabled],
|
||||||
queryFn: () => unwrapServiceError(
|
queryFn: async () => {
|
||||||
findSearchBasedSymbolDefinitions({
|
const response = await measure(() => unwrapServiceError(
|
||||||
symbolName: selectedSymbolInfo.symbolName,
|
findSearchBasedSymbolDefinitions({
|
||||||
language: selectedSymbolInfo.language,
|
symbolName: selectedSymbolInfo.symbolName,
|
||||||
revisionName: selectedSymbolInfo.revisionName,
|
language: selectedSymbolInfo.language,
|
||||||
|
revisionName: selectedSymbolInfo.revisionName,
|
||||||
|
repoName: isGlobalSearchEnabled ? undefined : selectedSymbolInfo.repoName
|
||||||
|
})
|
||||||
|
), 'findSearchBasedSymbolDefinitions', false);
|
||||||
|
|
||||||
|
captureEvent('wa_explore_menu_definitions_loaded', {
|
||||||
|
durationMs: response.durationMs,
|
||||||
|
isGlobalSearchEnabled,
|
||||||
})
|
})
|
||||||
),
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useHotkeys('shift+a', () => {
|
||||||
|
setIsGlobalSearchEnabled(prev => !prev);
|
||||||
|
}, {
|
||||||
|
enableOnFormTags: true,
|
||||||
|
enableOnContentEditable: true,
|
||||||
|
description: "Search all repositories",
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPending = isReferencesResponsePending || isDefinitionsResponsePending;
|
const isPending = isReferencesResponsePending || isDefinitionsResponsePending;
|
||||||
|
|
@ -98,29 +132,52 @@ export const ExploreMenu = ({
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
minSize={10}
|
minSize={10}
|
||||||
maxSize={20}
|
maxSize={20}
|
||||||
|
className="flex flex-col h-full"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col p-2">
|
<div className="flex flex-col p-2">
|
||||||
<Tooltip
|
<div className="flex flex-row items-center justify-between">
|
||||||
delayDuration={100}
|
|
||||||
>
|
<Tooltip
|
||||||
<TooltipTrigger
|
delayDuration={100}
|
||||||
disabled={true}
|
|
||||||
className="mr-auto"
|
|
||||||
>
|
>
|
||||||
<Badge
|
<TooltipTrigger
|
||||||
variant="outline"
|
disabled={true}
|
||||||
className="w-fit h-fit flex-shrink-0 select-none"
|
className="mr-auto"
|
||||||
>
|
>
|
||||||
Search Based
|
<Badge
|
||||||
</Badge>
|
variant="outline"
|
||||||
</TooltipTrigger>
|
className="w-fit h-fit flex-shrink-0 select-none"
|
||||||
<TooltipContent
|
>
|
||||||
side="top"
|
Search Based
|
||||||
align="start"
|
</Badge>
|
||||||
>
|
</TooltipTrigger>
|
||||||
Symbol references and definitions found using a best-guess search heuristic.
|
<TooltipContent
|
||||||
</TooltipContent>
|
side="top"
|
||||||
</Tooltip>
|
align="center"
|
||||||
|
>
|
||||||
|
Symbol references and definitions found using a best-guess search heuristic.
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span>
|
||||||
|
<Toggle
|
||||||
|
pressed={isGlobalSearchEnabled}
|
||||||
|
onPressedChange={setIsGlobalSearchEnabled}
|
||||||
|
>
|
||||||
|
<GlobeIcon className="w-4 h-4" />
|
||||||
|
</Toggle>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" align="center">
|
||||||
|
Search all repositories
|
||||||
|
<KeyboardShortcutHint
|
||||||
|
shortcut="⇧ A"
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-1 mt-4">
|
<div className="flex flex-col gap-1 mt-4">
|
||||||
<Entry
|
<Entry
|
||||||
name="References"
|
name="References"
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,50 @@
|
||||||
|
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
|
||||||
|
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
||||||
|
import { useToast } from "@/components/hooks/use-toast";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { LoadingButton } from "@/components/ui/loading-button";
|
import { LoadingButton } from "@/components/ui/loading-button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { createAuditAction } from "@/ee/features/audit/actions";
|
||||||
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
|
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
|
||||||
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { SymbolDefinition, useHoveredOverSymbolInfo } from "./useHoveredOverSymbolInfo";
|
|
||||||
import { SymbolDefinitionPreview } from "./symbolDefinitionPreview";
|
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { useToast } from "@/components/hooks/use-toast";
|
import { SymbolDefinitionPreview } from "./symbolDefinitionPreview";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { useHoveredOverSymbolInfo } from "./useHoveredOverSymbolInfo";
|
||||||
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
|
||||||
|
|
||||||
interface SymbolHoverPopupProps {
|
interface SymbolHoverPopupProps {
|
||||||
editorRef: ReactCodeMirrorRef;
|
editorRef: ReactCodeMirrorRef;
|
||||||
language: string;
|
language: string;
|
||||||
revisionName: string;
|
revisionName: string;
|
||||||
onFindReferences: (symbolName: string) => void;
|
repoName: string;
|
||||||
onGotoDefinition: (symbolName: string, symbolDefinitions: SymbolDefinition[]) => void;
|
fileName: string;
|
||||||
|
source: 'browse' | 'preview' | 'chat';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
|
export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
|
||||||
editorRef,
|
editorRef,
|
||||||
revisionName,
|
revisionName,
|
||||||
language,
|
language,
|
||||||
onFindReferences,
|
repoName,
|
||||||
onGotoDefinition: _onGotoDefinition,
|
fileName,
|
||||||
|
source,
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [isSticky, setIsSticky] = useState(false);
|
const [isSticky, setIsSticky] = useState(false);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { navigateToPath } = useBrowseNavigation();
|
||||||
|
const captureEvent = useCaptureEvent();
|
||||||
|
|
||||||
const symbolInfo = useHoveredOverSymbolInfo({
|
const symbolInfo = useHoveredOverSymbolInfo({
|
||||||
editorRef,
|
editorRef,
|
||||||
isSticky,
|
isSticky,
|
||||||
revisionName,
|
revisionName,
|
||||||
language,
|
language,
|
||||||
|
repoName,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Positions the popup relative to the symbol
|
// Positions the popup relative to the symbol
|
||||||
|
|
@ -77,13 +85,118 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
|
||||||
}
|
}
|
||||||
}, [symbolInfo, editorRef]);
|
}, [symbolInfo, editorRef]);
|
||||||
|
|
||||||
|
// Multiple symbol definitions can exist for the same symbol, but we can only navigate
|
||||||
|
// and display a preview of one. If the symbol definition exists in the current file,
|
||||||
|
// then we use that one, otherwise we fallback to the first definition in the list.
|
||||||
|
const previewedSymbolDefinition = useMemo(() => {
|
||||||
|
if (!symbolInfo?.symbolDefinitions || symbolInfo.symbolDefinitions.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingDefinition = symbolInfo.symbolDefinitions.find(
|
||||||
|
(definition) => (
|
||||||
|
definition.fileName === fileName && definition.repoName === repoName
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingDefinition) {
|
||||||
|
return matchingDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return symbolInfo.symbolDefinitions[0];
|
||||||
|
}, [fileName, repoName, symbolInfo?.symbolDefinitions]);
|
||||||
|
|
||||||
const onGotoDefinition = useCallback(() => {
|
const onGotoDefinition = useCallback(() => {
|
||||||
if (!symbolInfo || !symbolInfo.symbolDefinitions) {
|
if (
|
||||||
|
!symbolInfo ||
|
||||||
|
!symbolInfo.symbolDefinitions ||
|
||||||
|
!previewedSymbolDefinition
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onGotoDefinition(symbolInfo.symbolName, symbolInfo.symbolDefinitions);
|
captureEvent('wa_goto_definition_pressed', {
|
||||||
}, [symbolInfo, _onGotoDefinition]);
|
source,
|
||||||
|
});
|
||||||
|
|
||||||
|
createAuditAction({
|
||||||
|
action: "user.performed_goto_definition",
|
||||||
|
metadata: {
|
||||||
|
message: symbolInfo.symbolName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
fileName,
|
||||||
|
repoName,
|
||||||
|
revisionName,
|
||||||
|
language,
|
||||||
|
range: highlightRange,
|
||||||
|
} = previewedSymbolDefinition;
|
||||||
|
|
||||||
|
navigateToPath({
|
||||||
|
// Always navigate to the preview symbol definition.
|
||||||
|
repoName,
|
||||||
|
revisionName,
|
||||||
|
path: fileName,
|
||||||
|
pathType: 'blob',
|
||||||
|
highlightRange,
|
||||||
|
// If there are multiple definitions, we should open the Explore panel with the definitions.
|
||||||
|
...(symbolInfo.symbolDefinitions.length > 1 ? {
|
||||||
|
setBrowseState: {
|
||||||
|
selectedSymbolInfo: {
|
||||||
|
symbolName: symbolInfo.symbolName,
|
||||||
|
repoName,
|
||||||
|
revisionName,
|
||||||
|
language,
|
||||||
|
},
|
||||||
|
activeExploreMenuTab: "definitions",
|
||||||
|
isBottomPanelCollapsed: false,
|
||||||
|
}
|
||||||
|
} : {}),
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
captureEvent,
|
||||||
|
previewedSymbolDefinition,
|
||||||
|
navigateToPath,
|
||||||
|
source,
|
||||||
|
symbolInfo
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onFindReferences = useCallback(() => {
|
||||||
|
if (!symbolInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
captureEvent('wa_find_references_pressed', {
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
|
||||||
|
createAuditAction({
|
||||||
|
action: "user.performed_find_references",
|
||||||
|
metadata: {
|
||||||
|
message: symbolInfo.symbolName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
navigateToPath({
|
||||||
|
repoName,
|
||||||
|
revisionName,
|
||||||
|
path: fileName,
|
||||||
|
pathType: 'blob',
|
||||||
|
highlightRange: symbolInfo.range,
|
||||||
|
setBrowseState: {
|
||||||
|
selectedSymbolInfo: {
|
||||||
|
symbolName: symbolInfo.symbolName,
|
||||||
|
repoName,
|
||||||
|
revisionName,
|
||||||
|
language,
|
||||||
|
},
|
||||||
|
activeExploreMenuTab: "references",
|
||||||
|
isBottomPanelCollapsed: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [captureEvent, fileName, language, navigateToPath, repoName, revisionName, source, symbolInfo]);
|
||||||
|
|
||||||
// @todo: We should probably make the behaviour s.t., the ctrl / cmd key needs to be held
|
// @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
|
// down to navigate to the definition. We should also only show the underline when the key
|
||||||
|
|
@ -100,9 +213,7 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
|
||||||
}, [symbolInfo, onGotoDefinition]);
|
}, [symbolInfo, onGotoDefinition]);
|
||||||
|
|
||||||
useHotkeys('alt+shift+f12', () => {
|
useHotkeys('alt+shift+f12', () => {
|
||||||
if (symbolInfo?.symbolName) {
|
onFindReferences();
|
||||||
onFindReferences(symbolInfo.symbolName);
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
enableOnFormTags: true,
|
enableOnFormTags: true,
|
||||||
enableOnContentEditable: true,
|
enableOnContentEditable: true,
|
||||||
|
|
@ -147,9 +258,9 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
) : symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 0 ? (
|
) : previewedSymbolDefinition ? (
|
||||||
<SymbolDefinitionPreview
|
<SymbolDefinitionPreview
|
||||||
symbolDefinition={symbolInfo.symbolDefinitions[0]}
|
symbolDefinition={previewedSymbolDefinition}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm font-medium text-muted-foreground">No hover info found</p>
|
<p className="text-sm font-medium text-muted-foreground">No hover info found</p>
|
||||||
|
|
@ -160,13 +271,13 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
loading={symbolInfo.isSymbolDefinitionsLoading}
|
loading={symbolInfo.isSymbolDefinitionsLoading}
|
||||||
disabled={!symbolInfo.symbolDefinitions || symbolInfo.symbolDefinitions.length === 0}
|
disabled={!previewedSymbolDefinition}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onGotoDefinition}
|
onClick={onGotoDefinition}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
!symbolInfo.isSymbolDefinitionsLoading && (!symbolInfo.symbolDefinitions || symbolInfo.symbolDefinitions.length === 0) ?
|
!symbolInfo.isSymbolDefinitionsLoading && !previewedSymbolDefinition ?
|
||||||
"No definition found" :
|
"No definition found" :
|
||||||
`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`
|
`Go to ${symbolInfo.symbolDefinitions && symbolInfo.symbolDefinitions.length > 1 ? "definitions" : "definition"}`
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +297,7 @@ export const SymbolHoverPopup: React.FC<SymbolHoverPopupProps> = ({
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onFindReferences(symbolInfo.symbolName)}
|
onClick={onFindReferences}
|
||||||
>
|
>
|
||||||
Find references
|
Find references
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { findSearchBasedSymbolDefinitions } from "@/app/api/(client)/client";
|
import { findSearchBasedSymbolDefinitions } from "@/app/api/(client)/client";
|
||||||
import { SourceRange } from "@/features/search";
|
import { SourceRange } from "@/features/search";
|
||||||
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { measure, unwrapServiceError } from "@/lib/utils";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
@ -12,6 +13,7 @@ interface UseHoveredOverSymbolInfoProps {
|
||||||
isSticky: boolean;
|
isSticky: boolean;
|
||||||
revisionName: string;
|
revisionName: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
repoName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SymbolDefinition = {
|
export type SymbolDefinition = {
|
||||||
|
|
@ -19,12 +21,14 @@ export type SymbolDefinition = {
|
||||||
language: string;
|
language: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
repoName: string;
|
repoName: string;
|
||||||
|
revisionName: string;
|
||||||
range: SourceRange;
|
range: SourceRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HoveredOverSymbolInfo {
|
interface HoveredOverSymbolInfo {
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
symbolName: string;
|
symbolName: string;
|
||||||
|
range: SourceRange;
|
||||||
isSymbolDefinitionsLoading: boolean;
|
isSymbolDefinitionsLoading: boolean;
|
||||||
symbolDefinitions?: SymbolDefinition[];
|
symbolDefinitions?: SymbolDefinition[];
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +41,7 @@ export const useHoveredOverSymbolInfo = ({
|
||||||
isSticky,
|
isSticky,
|
||||||
revisionName,
|
revisionName,
|
||||||
language,
|
language,
|
||||||
|
repoName,
|
||||||
}: UseHoveredOverSymbolInfoProps): HoveredOverSymbolInfo | undefined => {
|
}: UseHoveredOverSymbolInfoProps): HoveredOverSymbolInfo | undefined => {
|
||||||
const mouseOverTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const mouseOverTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const mouseOutTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const mouseOutTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
@ -49,15 +54,26 @@ export const useHoveredOverSymbolInfo = ({
|
||||||
return (symbolElement && symbolElement.textContent) ?? undefined;
|
return (symbolElement && symbolElement.textContent) ?? undefined;
|
||||||
}, [symbolElement]);
|
}, [symbolElement]);
|
||||||
|
|
||||||
|
const captureEvent = useCaptureEvent();
|
||||||
|
|
||||||
const { data: symbolDefinitions, isLoading: isSymbolDefinitionsLoading } = useQuery({
|
const { data: symbolDefinitions, isLoading: isSymbolDefinitionsLoading } = useQuery({
|
||||||
queryKey: ["definitions", symbolName, revisionName, language, domain],
|
queryKey: ["definitions", symbolName, revisionName, language, domain, repoName],
|
||||||
queryFn: () => unwrapServiceError(
|
queryFn: async () => {
|
||||||
findSearchBasedSymbolDefinitions({
|
const response = await measure(() => unwrapServiceError(
|
||||||
symbolName: symbolName!,
|
findSearchBasedSymbolDefinitions({
|
||||||
language,
|
symbolName: symbolName!,
|
||||||
revisionName,
|
language,
|
||||||
})
|
revisionName,
|
||||||
),
|
repoName,
|
||||||
|
})
|
||||||
|
), 'findSearchBasedSymbolDefinitions', false);
|
||||||
|
|
||||||
|
captureEvent('wa_symbol_hover_popup_definitions_loaded', {
|
||||||
|
durationMs: response.durationMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
select: ((data) => {
|
select: ((data) => {
|
||||||
return data.files.flatMap((file) => {
|
return data.files.flatMap((file) => {
|
||||||
return file.matches.map((match) => {
|
return file.matches.map((match) => {
|
||||||
|
|
@ -66,6 +82,7 @@ export const useHoveredOverSymbolInfo = ({
|
||||||
language: file.language,
|
language: file.language,
|
||||||
fileName: file.fileName,
|
fileName: file.fileName,
|
||||||
repoName: file.repository,
|
repoName: file.repository,
|
||||||
|
revisionName: revisionName,
|
||||||
range: match.range,
|
range: match.range,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -107,7 +124,7 @@ export const useHoveredOverSymbolInfo = ({
|
||||||
|
|
||||||
const handleMouseOut = () => {
|
const handleMouseOut = () => {
|
||||||
clearTimers();
|
clearTimers();
|
||||||
|
|
||||||
mouseOutTimerRef.current = setTimeout(() => {
|
mouseOutTimerRef.current = setTimeout(() => {
|
||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
}, SYMBOL_HOVER_POPUP_MOUSE_OUT_TIMEOUT_MS);
|
}, SYMBOL_HOVER_POPUP_MOUSE_OUT_TIMEOUT_MS);
|
||||||
|
|
@ -122,17 +139,64 @@ export const useHoveredOverSymbolInfo = ({
|
||||||
};
|
};
|
||||||
}, [editorRef, domain, clearTimers]);
|
}, [editorRef, domain, clearTimers]);
|
||||||
|
|
||||||
|
// Extract the highlight range of the symbolElement from the editor view.
|
||||||
|
const highlightRange = useMemo((): SourceRange | undefined => {
|
||||||
|
if (!symbolElement || !editorRef.view) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = editorRef.view;
|
||||||
|
const rect = symbolElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Get the start position (left edge, middle vertically)
|
||||||
|
const startPos = view.posAtCoords({
|
||||||
|
x: rect.left,
|
||||||
|
y: rect.top + rect.height / 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the end position (right edge, middle vertically)
|
||||||
|
const endPos = view.posAtCoords({
|
||||||
|
x: rect.right,
|
||||||
|
y: rect.top + rect.height / 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (startPos === null || endPos === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert CodeMirror positions to SourceRange format
|
||||||
|
const startLine = view.state.doc.lineAt(startPos);
|
||||||
|
const endLine = view.state.doc.lineAt(endPos);
|
||||||
|
|
||||||
|
const startColumn = startPos - startLine.from + 1; // 1-based column
|
||||||
|
const endColumn = endPos - endLine.from + 1; // 1-based column
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: {
|
||||||
|
byteOffset: startPos, // 0-based byte offset
|
||||||
|
lineNumber: startLine.number, // 1-based line number
|
||||||
|
column: startColumn, // 1-based column
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
byteOffset: endPos, // 0-based byte offset
|
||||||
|
lineNumber: endLine.number, // 1-based line number
|
||||||
|
column: endColumn, // 1-based column
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [symbolElement, editorRef.view]);
|
||||||
|
|
||||||
if (!isVisible && !isSticky) {
|
if (!isVisible && !isSticky) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!symbolElement || !symbolName) {
|
if (!symbolElement || !symbolName || !highlightRange) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
element: symbolElement,
|
element: symbolElement,
|
||||||
symbolName,
|
symbolName,
|
||||||
|
range: highlightRange,
|
||||||
isSymbolDefinitionsLoading: isSymbolDefinitionsLoading,
|
isSymbolDefinitionsLoading: isSymbolDefinitionsLoading,
|
||||||
symbolDefinitions,
|
symbolDefinitions,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
|
|
||||||
import { PathHeader } from "@/app/[domain]/components/pathHeader";
|
import { PathHeader } from "@/app/[domain]/components/pathHeader";
|
||||||
import { SymbolHoverPopup } from '@/ee/features/codeNav/components/symbolHoverPopup';
|
import { SymbolHoverPopup } from '@/ee/features/codeNav/components/symbolHoverPopup';
|
||||||
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
|
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 { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
|
||||||
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
||||||
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
||||||
|
|
@ -12,15 +10,13 @@ import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Range } from "@codemirror/state";
|
import { Range } from "@codemirror/state";
|
||||||
import { Decoration, DecorationSet, EditorView } from '@codemirror/view';
|
import { Decoration, DecorationSet, EditorView } from '@codemirror/view';
|
||||||
|
import { CodeHostType } from "@sourcebot/db";
|
||||||
import CodeMirror, { ReactCodeMirrorRef, StateField } from '@uiw/react-codemirror';
|
import CodeMirror, { ReactCodeMirrorRef, StateField } from '@uiw/react-codemirror';
|
||||||
|
import isEqual from "fast-deep-equal/react";
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
import { forwardRef, memo, Ref, useCallback, useImperativeHandle, useMemo, useState } from "react";
|
import { forwardRef, memo, Ref, useCallback, useImperativeHandle, useMemo, useState } from "react";
|
||||||
import { FileReference } from "../../types";
|
import { FileReference } from "../../types";
|
||||||
import { createCodeFoldingExtension } from "./codeFoldingExtension";
|
import { createCodeFoldingExtension } from "./codeFoldingExtension";
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
|
||||||
import { CodeHostType } from "@sourcebot/db";
|
|
||||||
import { createAuditAction } from "@/ee/features/audit/actions";
|
|
||||||
import isEqual from "fast-deep-equal/react";
|
|
||||||
|
|
||||||
const lineDecoration = Decoration.line({
|
const lineDecoration = Decoration.line({
|
||||||
attributes: { class: "cm-range-border-radius chat-lineHighlight" },
|
attributes: { class: "cm-range-border-radius chat-lineHighlight" },
|
||||||
|
|
@ -74,7 +70,6 @@ const ReferencedFileSourceListItem = ({
|
||||||
}: ReferencedFileSourceListItemProps, forwardedRef: Ref<ReactCodeMirrorRef>) => {
|
}: ReferencedFileSourceListItemProps, forwardedRef: Ref<ReactCodeMirrorRef>) => {
|
||||||
const theme = useCodeMirrorTheme();
|
const theme = useCodeMirrorTheme();
|
||||||
const [editorRef, setEditorRef] = useState<ReactCodeMirrorRef | null>(null);
|
const [editorRef, setEditorRef] = useState<ReactCodeMirrorRef | null>(null);
|
||||||
const captureEvent = useCaptureEvent();
|
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
|
|
@ -84,7 +79,6 @@ const ReferencedFileSourceListItem = ({
|
||||||
const hasCodeNavEntitlement = useHasEntitlement("code-nav");
|
const hasCodeNavEntitlement = useHasEntitlement("code-nav");
|
||||||
|
|
||||||
const languageExtension = useCodeMirrorLanguageExtension(language, editorRef?.view);
|
const languageExtension = useCodeMirrorLanguageExtension(language, editorRef?.view);
|
||||||
const { navigateToPath } = useBrowseNavigation();
|
|
||||||
|
|
||||||
const getReferenceAtPos = useCallback((x: number, y: number, view: EditorView): FileReference | undefined => {
|
const getReferenceAtPos = useCallback((x: number, y: number, view: EditorView): FileReference | undefined => {
|
||||||
const pos = view.posAtCoords({ x, y });
|
const pos = view.posAtCoords({ x, y });
|
||||||
|
|
@ -217,83 +211,6 @@ const ReferencedFileSourceListItem = ({
|
||||||
codeFoldingExtension,
|
codeFoldingExtension,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
|
|
||||||
if (symbolDefinitions.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
captureEvent('wa_goto_definition_pressed', {
|
|
||||||
source: 'chat',
|
|
||||||
});
|
|
||||||
createAuditAction({
|
|
||||||
action: "user.performed_goto_definition",
|
|
||||||
metadata: {
|
|
||||||
message: symbolName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (symbolDefinitions.length === 1) {
|
|
||||||
const symbolDefinition = symbolDefinitions[0];
|
|
||||||
const { fileName, repoName } = symbolDefinition;
|
|
||||||
|
|
||||||
navigateToPath({
|
|
||||||
repoName,
|
|
||||||
revisionName: revision,
|
|
||||||
path: fileName,
|
|
||||||
pathType: 'blob',
|
|
||||||
highlightRange: symbolDefinition.range,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
navigateToPath({
|
|
||||||
repoName,
|
|
||||||
revisionName: revision,
|
|
||||||
path: fileName,
|
|
||||||
pathType: 'blob',
|
|
||||||
setBrowseState: {
|
|
||||||
selectedSymbolInfo: {
|
|
||||||
symbolName,
|
|
||||||
repoName,
|
|
||||||
revisionName: revision,
|
|
||||||
language: language,
|
|
||||||
},
|
|
||||||
activeExploreMenuTab: "definitions",
|
|
||||||
isBottomPanelCollapsed: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}, [captureEvent, navigateToPath, revision, repoName, fileName, language]);
|
|
||||||
|
|
||||||
const onFindReferences = useCallback((symbolName: string) => {
|
|
||||||
captureEvent('wa_find_references_pressed', {
|
|
||||||
source: 'chat',
|
|
||||||
});
|
|
||||||
createAuditAction({
|
|
||||||
action: "user.performed_find_references",
|
|
||||||
metadata: {
|
|
||||||
message: symbolName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
navigateToPath({
|
|
||||||
repoName,
|
|
||||||
revisionName: revision,
|
|
||||||
path: fileName,
|
|
||||||
pathType: 'blob',
|
|
||||||
setBrowseState: {
|
|
||||||
selectedSymbolInfo: {
|
|
||||||
symbolName,
|
|
||||||
repoName,
|
|
||||||
revisionName: revision,
|
|
||||||
language: language,
|
|
||||||
},
|
|
||||||
activeExploreMenuTab: "references",
|
|
||||||
isBottomPanelCollapsed: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}, [captureEvent, fileName, language, navigateToPath, repoName, revision]);
|
|
||||||
|
|
||||||
const ExpandCollapseIcon = useMemo(() => {
|
const ExpandCollapseIcon = useMemo(() => {
|
||||||
return isExpanded ? ChevronDown : ChevronRight;
|
return isExpanded ? ChevronDown : ChevronRight;
|
||||||
}, [isExpanded]);
|
}, [isExpanded]);
|
||||||
|
|
@ -341,11 +258,12 @@ const ReferencedFileSourceListItem = ({
|
||||||
>
|
>
|
||||||
{editorRef && hasCodeNavEntitlement && (
|
{editorRef && hasCodeNavEntitlement && (
|
||||||
<SymbolHoverPopup
|
<SymbolHoverPopup
|
||||||
|
source="chat"
|
||||||
editorRef={editorRef}
|
editorRef={editorRef}
|
||||||
revisionName={revision}
|
revisionName={revision}
|
||||||
language={language}
|
language={language}
|
||||||
onFindReferences={onFindReferences}
|
repoName={repoName}
|
||||||
onGotoDefinition={onGotoDefinition}
|
fileName={fileName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CodeMirror>
|
</CodeMirror>
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ export const findSymbolReferencesTool = tool({
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
symbol: z.string().describe("The symbol to find references to"),
|
symbol: z.string().describe("The symbol to find references to"),
|
||||||
language: z.string().describe("The programming language of the symbol"),
|
language: z.string().describe("The programming language of the symbol"),
|
||||||
|
repository: z.string().describe("The repository to scope the search to").optional(),
|
||||||
}),
|
}),
|
||||||
execute: async ({ symbol, language }) => {
|
execute: async ({ symbol, language, repository }) => {
|
||||||
// @todo: make revision configurable.
|
// @todo: make revision configurable.
|
||||||
const revision = "HEAD";
|
const revision = "HEAD";
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ export const findSymbolReferencesTool = tool({
|
||||||
symbolName: symbol,
|
symbolName: symbol,
|
||||||
language,
|
language,
|
||||||
revisionName: "HEAD",
|
revisionName: "HEAD",
|
||||||
|
repoName: repository,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isServiceError(response)) {
|
if (isServiceError(response)) {
|
||||||
|
|
@ -63,8 +65,9 @@ export const findSymbolDefinitionsTool = tool({
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
symbol: z.string().describe("The symbol to find definitions of"),
|
symbol: z.string().describe("The symbol to find definitions of"),
|
||||||
language: z.string().describe("The programming language of the symbol"),
|
language: z.string().describe("The programming language of the symbol"),
|
||||||
|
repository: z.string().describe("The repository to scope the search to").optional(),
|
||||||
}),
|
}),
|
||||||
execute: async ({ symbol, language }) => {
|
execute: async ({ symbol, language, repository }) => {
|
||||||
// @todo: make revision configurable.
|
// @todo: make revision configurable.
|
||||||
const revision = "HEAD";
|
const revision = "HEAD";
|
||||||
|
|
||||||
|
|
@ -72,6 +75,7 @@ export const findSymbolDefinitionsTool = tool({
|
||||||
symbolName: symbol,
|
symbolName: symbol,
|
||||||
language,
|
language,
|
||||||
revisionName: revision,
|
revisionName: revision,
|
||||||
|
repoName: repository,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isServiceError(response)) {
|
if (isServiceError(response)) {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { withOptionalAuthV2 } from "@/withAuthV2";
|
||||||
import { SearchResponse } from "../search/types";
|
import { SearchResponse } from "../search/types";
|
||||||
import { FindRelatedSymbolsRequest, FindRelatedSymbolsResponse } from "./types";
|
import { FindRelatedSymbolsRequest, FindRelatedSymbolsResponse } from "./types";
|
||||||
import { QueryIR } from '../search/ir';
|
import { QueryIR } from '../search/ir';
|
||||||
|
import escapeStringRegexp from "escape-string-regexp";
|
||||||
|
|
||||||
// The maximum number of matches to return from the search API.
|
// The maximum number of matches to return from the search API.
|
||||||
const MAX_REFERENCE_COUNT = 1000;
|
const MAX_REFERENCE_COUNT = 1000;
|
||||||
|
|
@ -18,6 +19,7 @@ export const findSearchBasedSymbolReferences = async (props: FindRelatedSymbolsR
|
||||||
symbolName,
|
symbolName,
|
||||||
language,
|
language,
|
||||||
revisionName = "HEAD",
|
revisionName = "HEAD",
|
||||||
|
repoName,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const languageFilter = getExpandedLanguageFilter(language);
|
const languageFilter = getExpandedLanguageFilter(language);
|
||||||
|
|
@ -40,6 +42,11 @@ export const findSearchBasedSymbolReferences = async (props: FindRelatedSymbolsR
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
languageFilter,
|
languageFilter,
|
||||||
|
...(repoName ? [{
|
||||||
|
repo: {
|
||||||
|
regexp: `^${escapeStringRegexp(repoName)}$`,
|
||||||
|
}
|
||||||
|
}]: [])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,6 +74,7 @@ export const findSearchBasedSymbolDefinitions = async (props: FindRelatedSymbols
|
||||||
symbolName,
|
symbolName,
|
||||||
language,
|
language,
|
||||||
revisionName = "HEAD",
|
revisionName = "HEAD",
|
||||||
|
repoName
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const languageFilter = getExpandedLanguageFilter(language);
|
const languageFilter = getExpandedLanguageFilter(language);
|
||||||
|
|
@ -93,6 +101,11 @@ export const findSearchBasedSymbolDefinitions = async (props: FindRelatedSymbols
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
languageFilter,
|
languageFilter,
|
||||||
|
...(repoName ? [{
|
||||||
|
repo: {
|
||||||
|
regexp: `^${escapeStringRegexp(repoName)}$`,
|
||||||
|
}
|
||||||
|
}]: [])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,16 @@ import { rangeSchema, repositoryInfoSchema } from "../search/types";
|
||||||
export const findRelatedSymbolsRequestSchema = z.object({
|
export const findRelatedSymbolsRequestSchema = z.object({
|
||||||
symbolName: z.string(),
|
symbolName: z.string(),
|
||||||
language: z.string(),
|
language: z.string(),
|
||||||
|
/**
|
||||||
|
* Optional revision name to scope search to.
|
||||||
|
* If not provided, the search will be scoped to HEAD.
|
||||||
|
*/
|
||||||
revisionName: z.string().optional(),
|
revisionName: z.string().optional(),
|
||||||
|
/**
|
||||||
|
* Optional repository name to scope search to.
|
||||||
|
* If not provided, the search will be across all repositories.
|
||||||
|
*/
|
||||||
|
repoName: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type FindRelatedSymbolsRequest = z.infer<typeof findRelatedSymbolsRequestSchema>;
|
export type FindRelatedSymbolsRequest = z.infer<typeof findRelatedSymbolsRequestSchema>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { search } from "./searchApi";
|
||||||
import { sew } from "@/actions";
|
import { sew } from "@/actions";
|
||||||
import { withOptionalAuthV2 } from "@/withAuthV2";
|
import { withOptionalAuthV2 } from "@/withAuthV2";
|
||||||
import { QueryIR } from './ir';
|
import { QueryIR } from './ir';
|
||||||
|
import escapeStringRegexp from "escape-string-regexp";
|
||||||
|
|
||||||
// @todo (bkellam) #574 : We should really be using `git show <hash>:<path>` to fetch file contents here.
|
// @todo (bkellam) #574 : We should really be using `git show <hash>:<path>` to fetch file contents here.
|
||||||
// This will allow us to support permalinks to files at a specific revision that may not be indexed
|
// This will allow us to support permalinks to files at a specific revision that may not be indexed
|
||||||
|
|
@ -18,7 +19,7 @@ export const getFileSource = async ({ fileName, repository, branch }: FileSource
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
repo: {
|
repo: {
|
||||||
regexp: `^${repository}$`,
|
regexp: `^${escapeStringRegexp(repository)}$`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -273,15 +273,6 @@ export type PosthogEventMap = {
|
||||||
wa_api_key_created: {},
|
wa_api_key_created: {},
|
||||||
wa_api_key_creation_fail: {},
|
wa_api_key_creation_fail: {},
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
wa_goto_definition_pressed: {
|
|
||||||
source: 'chat' | 'browse' | 'preview',
|
|
||||||
},
|
|
||||||
wa_find_references_pressed: {
|
|
||||||
source: 'chat' | 'browse' | 'preview',
|
|
||||||
},
|
|
||||||
//////////////////////////////////////////////////////////////////
|
|
||||||
wa_explore_menu_reference_clicked: {},
|
|
||||||
//////////////////////////////////////////////////////////////////
|
|
||||||
wa_chat_feedback_submitted: {
|
wa_chat_feedback_submitted: {
|
||||||
feedback: 'like' | 'dislike',
|
feedback: 'like' | 'dislike',
|
||||||
chatId: string,
|
chatId: string,
|
||||||
|
|
@ -302,5 +293,26 @@ export type PosthogEventMap = {
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
wa_github_star_toast_displayed: {},
|
wa_github_star_toast_displayed: {},
|
||||||
wa_github_star_toast_clicked: {},
|
wa_github_star_toast_clicked: {},
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
wa_goto_definition_pressed: {
|
||||||
|
source: 'chat' | 'browse' | 'preview',
|
||||||
|
},
|
||||||
|
wa_find_references_pressed: {
|
||||||
|
source: 'chat' | 'browse' | 'preview',
|
||||||
|
},
|
||||||
|
wa_symbol_hover_popup_definitions_loaded: {
|
||||||
|
durationMs: number,
|
||||||
|
},
|
||||||
|
wa_explore_menu_reference_clicked: {},
|
||||||
|
wa_explore_menu_references_loaded: {
|
||||||
|
durationMs: number,
|
||||||
|
// Whether or not the user is searching all repositories.
|
||||||
|
isGlobalSearchEnabled: boolean,
|
||||||
|
},
|
||||||
|
wa_explore_menu_definitions_loaded: {
|
||||||
|
durationMs: number,
|
||||||
|
// Whether or not the user is searching all repositories.
|
||||||
|
isGlobalSearchEnabled: boolean,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export type PosthogEvent = keyof PosthogEventMap;
|
export type PosthogEvent = keyof PosthogEventMap;
|
||||||
Loading…
Reference in a new issue