mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
fix(web): Fix loading issues with references / definitions list (#617)
This commit is contained in:
parent
341836a2ed
commit
fbe1073d0e
30 changed files with 275 additions and 128 deletions
|
|
@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed spurious infinite loads with explore panel, file tree, and file search command. [#617](https://github.com/sourcebot-dev/sourcebot/pull/617)
|
||||||
|
|
||||||
## [4.9.2] - 2025-11-13
|
## [4.9.2] - 2025-11-13
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { getRepoInfoByName } from "@/actions";
|
import { getRepoInfoByName } from "@/actions";
|
||||||
import { PathHeader } from "@/app/[domain]/components/pathHeader";
|
import { PathHeader } from "@/app/[domain]/components/pathHeader";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { getFileSource } from "@/features/search/fileSourceApi";
|
|
||||||
import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils";
|
import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
|
import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
|
||||||
|
import { getFileSource } from "@/features/search/fileSourceApi";
|
||||||
|
|
||||||
interface CodePreviewPanelProps {
|
interface CodePreviewPanelProps {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { FileTreeItem } from "@/features/fileTree/actions";
|
|
||||||
import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent";
|
import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent";
|
||||||
import { getBrowsePath } from "../../hooks/utils";
|
import { getBrowsePath } from "../../hooks/utils";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useBrowseParams } from "../../hooks/useBrowseParams";
|
import { useBrowseParams } from "../../hooks/useBrowseParams";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
|
import { FileTreeItem } from "@/features/fileTree/types";
|
||||||
|
|
||||||
interface PureTreePreviewPanelProps {
|
interface PureTreePreviewPanelProps {
|
||||||
items: FileTreeItem[];
|
items: FileTreeItem[];
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { getRepoInfoByName } from "@/actions";
|
import { getRepoInfoByName } from "@/actions";
|
||||||
import { PathHeader } from "@/app/[domain]/components/pathHeader";
|
import { PathHeader } from "@/app/[domain]/components/pathHeader";
|
||||||
import { getFolderContents } from "@/features/fileTree/actions";
|
import { getFolderContents } from "@/features/fileTree/api";
|
||||||
import { isServiceError } from "@/lib/utils";
|
import { isServiceError } from "@/lib/utils";
|
||||||
import { PureTreePreviewPanel } from "./pureTreePreviewPanel";
|
import { PureTreePreviewPanel } from "./pureTreePreviewPanel";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { useState, useRef, useMemo, useEffect, useCallback } from "react";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
import { FileTreeItem, getFiles } from "@/features/fileTree/actions";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { useBrowseNavigation } from "../hooks/useBrowseNavigation";
|
import { useBrowseNavigation } from "../hooks/useBrowseNavigation";
|
||||||
import { useBrowseState } from "../hooks/useBrowseState";
|
import { useBrowseState } from "../hooks/useBrowseState";
|
||||||
|
|
@ -13,6 +12,8 @@ import { useBrowseParams } from "../hooks/useBrowseParams";
|
||||||
import { FileTreeItemIcon } from "@/features/fileTree/components/fileTreeItemIcon";
|
import { FileTreeItemIcon } from "@/features/fileTree/components/fileTreeItemIcon";
|
||||||
import { useLocalStorage } from "usehooks-ts";
|
import { useLocalStorage } from "usehooks-ts";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { FileTreeItem } from "@/features/fileTree/types";
|
||||||
|
import { getFiles } from "@/app/api/(client)/client";
|
||||||
|
|
||||||
const MAX_RESULTS = 100;
|
const MAX_RESULTS = 100;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export const useSuggestionsData = ({
|
||||||
query: `file:${suggestionQuery}`,
|
query: `file:${suggestionQuery}`,
|
||||||
matches: 15,
|
matches: 15,
|
||||||
contextLines: 1,
|
contextLines: 1,
|
||||||
}, domain),
|
}),
|
||||||
select: (data): Suggestion[] => {
|
select: (data): Suggestion[] => {
|
||||||
if (isServiceError(data)) {
|
if (isServiceError(data)) {
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -75,7 +75,7 @@ export const useSuggestionsData = ({
|
||||||
query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`,
|
query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`,
|
||||||
matches: 15,
|
matches: 15,
|
||||||
contextLines: 1,
|
contextLines: 1,
|
||||||
}, domain),
|
}),
|
||||||
select: (data): Suggestion[] => {
|
select: (data): Suggestion[] => {
|
||||||
if (isServiceError(data)) {
|
if (isServiceError(data)) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { CodePreview } from "./codePreview";
|
||||||
import { SearchResultFile } from "@/features/search/types";
|
import { SearchResultFile } from "@/features/search/types";
|
||||||
import { SymbolIcon } from "@radix-ui/react-icons";
|
import { SymbolIcon } from "@radix-ui/react-icons";
|
||||||
import { SetStateAction, Dispatch, useMemo } from "react";
|
import { SetStateAction, Dispatch, useMemo } from "react";
|
||||||
import { getFileSource } from "@/features/search/fileSourceApi";
|
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
|
import { getFileSource } from "@/app/api/(client)/client";
|
||||||
|
|
||||||
interface CodePreviewPanelProps {
|
interface CodePreviewPanelProps {
|
||||||
previewedFile: SearchResultFile;
|
previewedFile: SearchResultFile;
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export const SearchResultsPage = ({
|
||||||
matches: maxMatchCount,
|
matches: maxMatchCount,
|
||||||
contextLines: 3,
|
contextLines: 3,
|
||||||
whole: false,
|
whole: false,
|
||||||
}, domain)), "client.search"),
|
})), "client.search"),
|
||||||
select: ({ data, durationMs }) => ({
|
select: ({ data, durationMs }) => ({
|
||||||
...data,
|
...data,
|
||||||
totalClientSearchDurationMs: durationMs,
|
totalClientSearchDurationMs: durationMs,
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,22 @@ import {
|
||||||
SearchRequest,
|
SearchRequest,
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
} from "@/features/search/types";
|
} from "@/features/search/types";
|
||||||
|
import {
|
||||||
|
FindRelatedSymbolsRequest,
|
||||||
|
FindRelatedSymbolsResponse,
|
||||||
|
} from "@/features/codeNav/types";
|
||||||
|
import {
|
||||||
|
GetFilesRequest,
|
||||||
|
GetFilesResponse,
|
||||||
|
GetTreeRequest,
|
||||||
|
GetTreeResponse,
|
||||||
|
} from "@/features/fileTree/types";
|
||||||
|
|
||||||
export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse | ServiceError> => {
|
export const search = async (body: SearchRequest): Promise<SearchResponse | ServiceError> => {
|
||||||
const result = await fetch("/api/search", {
|
const result = await fetch("/api/search", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Org-Domain": domain,
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
}).then(response => response.json());
|
}).then(response => response.json());
|
||||||
|
|
@ -27,12 +36,11 @@ export const search = async (body: SearchRequest, domain: string): Promise<Searc
|
||||||
return result as SearchResponse | ServiceError;
|
return result as SearchResponse | ServiceError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchFileSource = async (body: FileSourceRequest, domain: string): Promise<FileSourceResponse | ServiceError> => {
|
export const getFileSource = async (body: FileSourceRequest): Promise<FileSourceResponse | ServiceError> => {
|
||||||
const result = await fetch("/api/source", {
|
const result = await fetch("/api/source", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Org-Domain": domain,
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
}).then(response => response.json());
|
}).then(response => response.json());
|
||||||
|
|
@ -60,3 +68,35 @@ export const getVersion = async (): Promise<GetVersionResponse> => {
|
||||||
}).then(response => response.json());
|
}).then(response => response.json());
|
||||||
return result as GetVersionResponse;
|
return result as GetVersionResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const findSearchBasedSymbolReferences = async (body: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => {
|
||||||
|
const result = await fetch("/api/find_references", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}).then(response => response.json());
|
||||||
|
return result as FindRelatedSymbolsResponse | ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findSearchBasedSymbolDefinitions = async (body: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => {
|
||||||
|
const result = await fetch("/api/find_definitions", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}).then(response => response.json());
|
||||||
|
return result as FindRelatedSymbolsResponse | ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTree = async (body: GetTreeRequest): Promise<GetTreeResponse | ServiceError> => {
|
||||||
|
const result = await fetch("/api/tree", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}).then(response => response.json());
|
||||||
|
return result as GetTreeResponse | ServiceError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFiles = async (body: GetFilesRequest): Promise<GetFilesResponse | ServiceError> => {
|
||||||
|
const result = await fetch("/api/files", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}).then(response => response.json());
|
||||||
|
return result as GetFilesResponse | ServiceError;
|
||||||
|
}
|
||||||
23
packages/web/src/app/api/(server)/files/route.ts
Normal file
23
packages/web/src/app/api/(server)/files/route.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { getFiles } from "@/features/fileTree/api";
|
||||||
|
import { getFilesRequestSchema } from "@/features/fileTree/types";
|
||||||
|
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export const POST = async (request: NextRequest) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const parsed = await getFilesRequestSchema.safeParseAsync(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getFiles(parsed.data);
|
||||||
|
if (isServiceError(response)) {
|
||||||
|
return serviceErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json(response);
|
||||||
|
}
|
||||||
|
|
||||||
22
packages/web/src/app/api/(server)/find_definitions/route.ts
Normal file
22
packages/web/src/app/api/(server)/find_definitions/route.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { findSearchBasedSymbolDefinitions } from "@/features/codeNav/api";
|
||||||
|
import { findRelatedSymbolsRequestSchema } from "@/features/codeNav/types";
|
||||||
|
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export const POST = async (request: NextRequest) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await findSearchBasedSymbolDefinitions(parsed.data);
|
||||||
|
if (isServiceError(response)) {
|
||||||
|
return serviceErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json(response);
|
||||||
|
}
|
||||||
20
packages/web/src/app/api/(server)/find_references/route.ts
Normal file
20
packages/web/src/app/api/(server)/find_references/route.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { findSearchBasedSymbolReferences } from "@/features/codeNav/api";
|
||||||
|
import { findRelatedSymbolsRequestSchema } from "@/features/codeNav/types";
|
||||||
|
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export const POST = async (request: NextRequest) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await findSearchBasedSymbolReferences(parsed.data);
|
||||||
|
if (isServiceError(response)) {
|
||||||
|
return serviceErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json(response);
|
||||||
|
}
|
||||||
23
packages/web/src/app/api/(server)/tree/route.ts
Normal file
23
packages/web/src/app/api/(server)/tree/route.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
'use server';
|
||||||
|
|
||||||
|
import { getTree } from "@/features/fileTree/api";
|
||||||
|
import { getTreeRequestSchema } from "@/features/fileTree/types";
|
||||||
|
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export const POST = async (request: NextRequest) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const parsed = await getTreeRequestSchema.safeParseAsync(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getTree(parsed.data);
|
||||||
|
if (isServiceError(response)) {
|
||||||
|
return serviceErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
'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 { 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 { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "@/features/codeNav/actions";
|
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
@ -46,7 +46,7 @@ export const ExploreMenu = ({
|
||||||
symbolName: selectedSymbolInfo.symbolName,
|
symbolName: selectedSymbolInfo.symbolName,
|
||||||
language: selectedSymbolInfo.language,
|
language: selectedSymbolInfo.language,
|
||||||
revisionName: selectedSymbolInfo.revisionName,
|
revisionName: selectedSymbolInfo.revisionName,
|
||||||
}, domain)
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ export const ExploreMenu = ({
|
||||||
symbolName: selectedSymbolInfo.symbolName,
|
symbolName: selectedSymbolInfo.symbolName,
|
||||||
language: selectedSymbolInfo.language,
|
language: selectedSymbolInfo.language,
|
||||||
revisionName: selectedSymbolInfo.revisionName,
|
revisionName: selectedSymbolInfo.revisionName,
|
||||||
}, domain)
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { findSearchBasedSymbolDefinitions } from "@/features/codeNav/actions";
|
import { findSearchBasedSymbolDefinitions } from "@/app/api/(client)/client";
|
||||||
import { SourceRange } from "@/features/search/types";
|
import { SourceRange } from "@/features/search/types";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
|
|
@ -56,7 +56,7 @@ export const useHoveredOverSymbolInfo = ({
|
||||||
symbolName: symbolName!,
|
symbolName: symbolName!,
|
||||||
language,
|
language,
|
||||||
revisionName,
|
revisionName,
|
||||||
}, domain)
|
})
|
||||||
),
|
),
|
||||||
select: ((data) => {
|
select: ((data) => {
|
||||||
return data.files.flatMap((file) => {
|
return data.files.flatMap((file) => {
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,6 @@ const resolveFileSource = async ({ path, repo, revision }: FileSource) => {
|
||||||
fileName: path,
|
fileName: path,
|
||||||
repository: repo,
|
repository: repo,
|
||||||
branch: revision,
|
branch: revision,
|
||||||
// @todo: handle multi-tenancy.
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isServiceError(fileSource)) {
|
if (isServiceError(fileSource)) {
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export const useSuggestionsData = ({
|
||||||
query,
|
query,
|
||||||
matches: 10,
|
matches: 10,
|
||||||
contextLines: 1,
|
contextLines: 1,
|
||||||
}, domain))
|
}))
|
||||||
},
|
},
|
||||||
select: (data): FileSuggestion[] => {
|
select: (data): FileSuggestion[] => {
|
||||||
return data.files.map((file) => {
|
return data.files.map((file) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { fetchFileSource } from "@/app/api/(client)/client";
|
import { getFileSource } from "@/app/api/(client)/client";
|
||||||
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
@ -99,11 +99,11 @@ export const ReferencedSourcesListView = ({
|
||||||
const fileSourceQueries = useQueries({
|
const fileSourceQueries = useQueries({
|
||||||
queries: referencedFileSources.map((file) => ({
|
queries: referencedFileSources.map((file) => ({
|
||||||
queryKey: ['fileSource', file.path, file.repo, file.revision, domain],
|
queryKey: ['fileSource', file.path, file.repo, file.revision, domain],
|
||||||
queryFn: () => unwrapServiceError(fetchFileSource({
|
queryFn: () => unwrapServiceError(getFileSource({
|
||||||
fileName: file.path,
|
fileName: file.path,
|
||||||
repository: file.repo,
|
repository: file.repo,
|
||||||
branch: file.revision,
|
branch: file.revision,
|
||||||
}, domain)),
|
})),
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { search } from "@/features/search/searchApi"
|
import { search } from "@/features/search/searchApi"
|
||||||
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
|
|
||||||
import { InferToolInput, InferToolOutput, InferUITool, tool, ToolUIPart } from "ai";
|
import { InferToolInput, InferToolOutput, InferUITool, tool, ToolUIPart } from "ai";
|
||||||
import { isServiceError } from "@/lib/utils";
|
import { isServiceError } from "@/lib/utils";
|
||||||
import { getFileSource } from "../search/fileSourceApi";
|
import { getFileSource } from "../search/fileSourceApi";
|
||||||
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/actions";
|
import { findSearchBasedSymbolDefinitions, findSearchBasedSymbolReferences } from "../codeNav/api";
|
||||||
import { FileSourceResponse } from "../search/types";
|
import { FileSourceResponse } from "../search/types";
|
||||||
import { addLineNumbers, buildSearchQuery } from "./utils";
|
import { addLineNumbers, buildSearchQuery } from "./utils";
|
||||||
import { toolNames } from "./constants";
|
import { toolNames } from "./constants";
|
||||||
|
|
@ -36,8 +35,7 @@ export const findSymbolReferencesTool = tool({
|
||||||
symbolName: symbol,
|
symbolName: symbol,
|
||||||
language,
|
language,
|
||||||
revisionName: "HEAD",
|
revisionName: "HEAD",
|
||||||
// @todo(mt): handle multi-tenancy.
|
});
|
||||||
}, SINGLE_TENANT_ORG_DOMAIN);
|
|
||||||
|
|
||||||
if (isServiceError(response)) {
|
if (isServiceError(response)) {
|
||||||
return response;
|
return response;
|
||||||
|
|
@ -74,8 +72,7 @@ export const findSymbolDefinitionsTool = tool({
|
||||||
symbolName: symbol,
|
symbolName: symbol,
|
||||||
language,
|
language,
|
||||||
revisionName: revision,
|
revisionName: revision,
|
||||||
// @todo(mt): handle multi-tenancy.
|
});
|
||||||
}, SINGLE_TENANT_ORG_DOMAIN);
|
|
||||||
|
|
||||||
if (isServiceError(response)) {
|
if (isServiceError(response)) {
|
||||||
return response;
|
return response;
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,19 @@
|
||||||
'use server';
|
import 'server-only';
|
||||||
|
|
||||||
import { sew, withAuth, withOrgMembership } from "@/actions";
|
import { sew } from "@/actions";
|
||||||
import { searchResponseSchema } from "@/features/search/schemas";
|
import { searchResponseSchema } from "@/features/search/schemas";
|
||||||
import { search } from "@/features/search/searchApi";
|
import { search } from "@/features/search/searchApi";
|
||||||
import { isServiceError } from "@/lib/utils";
|
|
||||||
import { FindRelatedSymbolsResponse } from "./types";
|
|
||||||
import { ServiceError } from "@/lib/serviceError";
|
import { ServiceError } from "@/lib/serviceError";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
import { withOptionalAuthV2 } from "@/withAuthV2";
|
||||||
import { SearchResponse } from "../search/types";
|
import { SearchResponse } from "../search/types";
|
||||||
import { OrgRole } from "@sourcebot/db";
|
import { FindRelatedSymbolsRequest, FindRelatedSymbolsResponse } from "./types";
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
||||||
export const findSearchBasedSymbolReferences = async (
|
export const findSearchBasedSymbolReferences = async (props: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
|
||||||
props: {
|
withOptionalAuthV2(async () => {
|
||||||
symbolName: string,
|
|
||||||
language: string,
|
|
||||||
revisionName?: string,
|
|
||||||
},
|
|
||||||
domain: string,
|
|
||||||
): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
|
|
||||||
withAuth((session) =>
|
|
||||||
withOrgMembership(session, domain, async () => {
|
|
||||||
const {
|
const {
|
||||||
symbolName,
|
symbolName,
|
||||||
language,
|
language,
|
||||||
|
|
@ -41,20 +33,11 @@ export const findSearchBasedSymbolReferences = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseRelatedSymbolsSearchResponse(searchResult);
|
return parseRelatedSymbolsSearchResponse(searchResult);
|
||||||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true)
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
export const findSearchBasedSymbolDefinitions = async (
|
export const findSearchBasedSymbolDefinitions = async (props: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
|
||||||
props: {
|
withOptionalAuthV2(async () => {
|
||||||
symbolName: string,
|
|
||||||
language: string,
|
|
||||||
revisionName?: string,
|
|
||||||
},
|
|
||||||
domain: string,
|
|
||||||
): Promise<FindRelatedSymbolsResponse | ServiceError> => sew(() =>
|
|
||||||
withAuth((session) =>
|
|
||||||
withOrgMembership(session, domain, async () => {
|
|
||||||
const {
|
const {
|
||||||
symbolName,
|
symbolName,
|
||||||
language,
|
language,
|
||||||
|
|
@ -74,8 +57,7 @@ export const findSearchBasedSymbolDefinitions = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseRelatedSymbolsSearchResponse(searchResult);
|
return parseRelatedSymbolsSearchResponse(searchResult);
|
||||||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true)
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
const parseRelatedSymbolsSearchResponse = (searchResult: SearchResponse) => {
|
const parseRelatedSymbolsSearchResponse = (searchResult: SearchResponse) => {
|
||||||
const parser = searchResponseSchema.transform(async ({ files }) => ({
|
const parser = searchResponseSchema.transform(async ({ files }) => ({
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { rangeSchema, repositoryInfoSchema } from "../search/schemas";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const findRelatedSymbolsResponseSchema = z.object({
|
|
||||||
stats: z.object({
|
|
||||||
matchCount: z.number(),
|
|
||||||
}),
|
|
||||||
files: z.array(z.object({
|
|
||||||
fileName: z.string(),
|
|
||||||
repository: z.string(),
|
|
||||||
repositoryId: z.number(),
|
|
||||||
webUrl: z.string().optional(),
|
|
||||||
language: z.string(),
|
|
||||||
matches: z.array(z.object({
|
|
||||||
lineContent: z.string(),
|
|
||||||
range: rangeSchema,
|
|
||||||
}))
|
|
||||||
})),
|
|
||||||
repositoryInfo: z.array(repositoryInfoSchema),
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +1,29 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { findRelatedSymbolsResponseSchema } from "./schemas";
|
import { rangeSchema, repositoryInfoSchema } from "../search/schemas";
|
||||||
|
|
||||||
|
export const findRelatedSymbolsRequestSchema = z.object({
|
||||||
|
symbolName: z.string(),
|
||||||
|
language: z.string(),
|
||||||
|
revisionName: z.string().optional(),
|
||||||
|
});
|
||||||
|
export type FindRelatedSymbolsRequest = z.infer<typeof findRelatedSymbolsRequestSchema>;
|
||||||
|
|
||||||
|
export const findRelatedSymbolsResponseSchema = z.object({
|
||||||
|
stats: z.object({
|
||||||
|
matchCount: z.number(),
|
||||||
|
}),
|
||||||
|
files: z.array(z.object({
|
||||||
|
fileName: z.string(),
|
||||||
|
repository: z.string(),
|
||||||
|
repositoryId: z.number(),
|
||||||
|
webUrl: z.string().optional(),
|
||||||
|
language: z.string(),
|
||||||
|
matches: z.array(z.object({
|
||||||
|
lineContent: z.string(),
|
||||||
|
range: rangeSchema,
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
repositoryInfo: z.array(repositoryInfoSchema),
|
||||||
|
});
|
||||||
|
|
||||||
export type FindRelatedSymbolsResponse = z.infer<typeof findRelatedSymbolsResponseSchema>;
|
export type FindRelatedSymbolsResponse = z.infer<typeof findRelatedSymbolsResponseSchema>;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
'use server';
|
import 'server-only';
|
||||||
|
|
||||||
import { sew } from '@/actions';
|
import { sew } from '@/actions';
|
||||||
import { env } from '@sourcebot/shared';
|
import { env } from '@sourcebot/shared';
|
||||||
|
|
@ -8,19 +8,10 @@ import { Repo } from '@sourcebot/db';
|
||||||
import { createLogger } from '@sourcebot/shared';
|
import { createLogger } from '@sourcebot/shared';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { simpleGit } from 'simple-git';
|
import { simpleGit } from 'simple-git';
|
||||||
|
import { FileTreeItem, FileTreeNode } from './types';
|
||||||
|
|
||||||
const logger = createLogger('file-tree');
|
const logger = createLogger('file-tree');
|
||||||
|
|
||||||
export type FileTreeItem = {
|
|
||||||
type: string;
|
|
||||||
path: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FileTreeNode = FileTreeItem & {
|
|
||||||
children: FileTreeNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the tree of files (blobs) and directories (trees) for a given repository,
|
* Returns the tree of files (blobs) and directories (trees) for a given repository,
|
||||||
* at a given revision.
|
* at a given revision.
|
||||||
|
|
@ -218,7 +209,7 @@ const buildFileTree = (flatList: { type: string, path: string }[]): FileTreeNode
|
||||||
const part = parts[i];
|
const part = parts[i];
|
||||||
const isLeaf = i === parts.length - 1;
|
const isLeaf = i === parts.length - 1;
|
||||||
const nodeType = isLeaf ? item.type : 'tree';
|
const nodeType = isLeaf ? item.type : 'tree';
|
||||||
let next = current.children.find(child => child.name === part && child.type === nodeType);
|
let next = current.children.find((child: FileTreeNode) => child.name === part && child.type === nodeType);
|
||||||
|
|
||||||
if (!next) {
|
if (!next) {
|
||||||
next = {
|
next = {
|
||||||
|
|
@ -240,7 +231,7 @@ const buildFileTree = (flatList: { type: string, path: string }[]): FileTreeNode
|
||||||
|
|
||||||
const sortedChildren = node.children
|
const sortedChildren = node.children
|
||||||
.map(sortTree)
|
.map(sortTree)
|
||||||
.sort((a, b) => {
|
.sort((a: FileTreeNode, b: FileTreeNode) => {
|
||||||
if (a.type !== b.type) {
|
if (a.type !== b.type) {
|
||||||
return a.type === 'tree' ? -1 : 1;
|
return a.type === 'tree' ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { FileTreeItem } from "../actions";
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||||
import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
|
import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
|
||||||
import { FileTreeItemIcon } from "./fileTreeItemIcon";
|
import { FileTreeItemIcon } from "./fileTreeItemIcon";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { FileTreeItem } from "../types";
|
||||||
|
|
||||||
export const FileTreeItemComponent = ({
|
export const FileTreeItemComponent = ({
|
||||||
node,
|
node,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { FileTreeItem } from "../actions";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { VscodeFolderIcon } from "@/app/components/vscodeFolderIcon";
|
import { VscodeFolderIcon } from "@/app/components/vscodeFolderIcon";
|
||||||
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
||||||
|
import { FileTreeItem } from "../types";
|
||||||
|
|
||||||
interface FileTreeItemIconProps {
|
interface FileTreeItemIconProps {
|
||||||
item: FileTreeItem;
|
item: FileTreeItem;
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,25 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { getTree } from "../actions";
|
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { unwrapServiceError } from "@/lib/utils";
|
|
||||||
import { ResizablePanel } from "@/components/ui/resizable";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState";
|
import { useBrowseState } from "@/app/[domain]/browse/hooks/useBrowseState";
|
||||||
import { PureFileTreePanel } from "./pureFileTreePanel";
|
import { getTree } from "@/app/api/(client)/client";
|
||||||
|
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ImperativePanelHandle } from "react-resizable-panels";
|
import { ResizablePanel } from "@/components/ui/resizable";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { unwrapServiceError } from "@/lib/utils";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { SearchIcon } from "lucide-react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import {
|
import {
|
||||||
GoSidebarCollapse as ExpandIcon,
|
GoSidebarExpand as CollapseIcon,
|
||||||
GoSidebarExpand as CollapseIcon
|
GoSidebarCollapse as ExpandIcon
|
||||||
} from "react-icons/go";
|
} from "react-icons/go";
|
||||||
import { Tooltip, TooltipContent } from "@/components/ui/tooltip";
|
import { ImperativePanelHandle } from "react-resizable-panels";
|
||||||
import { TooltipTrigger } from "@/components/ui/tooltip";
|
import { PureFileTreePanel } from "./pureFileTreePanel";
|
||||||
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
|
||||||
import { useBrowseParams } from "@/app/[domain]/browse/hooks/useBrowseParams";
|
|
||||||
import { SearchIcon } from "lucide-react";
|
|
||||||
|
|
||||||
|
|
||||||
interface FileTreePanelProps {
|
interface FileTreePanelProps {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { FileTreeNode as RawFileTreeNode } from "../actions";
|
import { FileTreeNode as RawFileTreeNode } from "../types";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||||
import React, { useCallback, useMemo, useState, useEffect, useRef } from "react";
|
import React, { useCallback, useMemo, useState, useEffect, useRef } from "react";
|
||||||
import { FileTreeItemComponent } from "./fileTreeItemComponent";
|
import { FileTreeItemComponent } from "./fileTreeItemComponent";
|
||||||
|
|
|
||||||
44
packages/web/src/features/fileTree/types.ts
Normal file
44
packages/web/src/features/fileTree/types.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const getTreeRequestSchema = z.object({
|
||||||
|
repoName: z.string(),
|
||||||
|
revisionName: z.string(),
|
||||||
|
});
|
||||||
|
export type GetTreeRequest = z.infer<typeof getTreeRequestSchema>;
|
||||||
|
|
||||||
|
export const getFilesRequestSchema = z.object({
|
||||||
|
repoName: z.string(),
|
||||||
|
revisionName: z.string(),
|
||||||
|
});
|
||||||
|
export type GetFilesRequest = z.infer<typeof getFilesRequestSchema>;
|
||||||
|
|
||||||
|
export const fileTreeItemSchema = z.object({
|
||||||
|
type: z.string(),
|
||||||
|
path: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
export type FileTreeItem = z.infer<typeof fileTreeItemSchema>;
|
||||||
|
|
||||||
|
type FileTreeNodeType = {
|
||||||
|
type: string;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
children: FileTreeNodeType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fileTreeNodeSchema: z.ZodType<FileTreeNodeType> = z.lazy(() => z.object({
|
||||||
|
type: z.string(),
|
||||||
|
path: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
children: z.array(fileTreeNodeSchema),
|
||||||
|
}));
|
||||||
|
export type FileTreeNode = z.infer<typeof fileTreeNodeSchema>;
|
||||||
|
|
||||||
|
export const getTreeResponseSchema = z.object({
|
||||||
|
tree: fileTreeNodeSchema,
|
||||||
|
});
|
||||||
|
export type GetTreeResponse = z.infer<typeof getTreeResponseSchema>;
|
||||||
|
|
||||||
|
export const getFilesResponseSchema = z.array(fileTreeItemSchema);
|
||||||
|
export type GetFilesResponse = z.infer<typeof getFilesResponseSchema>;
|
||||||
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
'use server';
|
import 'server-only';
|
||||||
|
|
||||||
import escapeStringRegexp from "escape-string-regexp";
|
import escapeStringRegexp from "escape-string-regexp";
|
||||||
import { fileNotFound, ServiceError, unexpectedError } from "../../lib/serviceError";
|
import { fileNotFound, ServiceError, unexpectedError } from "../../lib/serviceError";
|
||||||
import { FileSourceRequest, FileSourceResponse } from "./types";
|
import { FileSourceRequest, FileSourceResponse } from "./types";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
'use server';
|
import 'server-only';
|
||||||
|
|
||||||
import { sew } from "@/actions";
|
import { sew } from "@/actions";
|
||||||
import { withOptionalAuthV2 } from "@/withAuthV2";
|
import { withOptionalAuthV2 } from "@/withAuthV2";
|
||||||
import { PrismaClient, Repo } from "@sourcebot/db";
|
import { PrismaClient, Repo } from "@sourcebot/db";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue