diff --git a/packages/web/.eslintrc.json b/packages/web/.eslintrc.json
index c381b6b5..6b1e43a1 100644
--- a/packages/web/.eslintrc.json
+++ b/packages/web/.eslintrc.json
@@ -7,7 +7,6 @@
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
- "plugin:react-hooks/recommended",
"next/core-web-vitals"
],
"rules": {
diff --git a/packages/web/package.json b/packages/web/package.json
index 5cd5dfdb..769b0e91 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "next lint",
+ "lint": "cross-env SKIP_ENV_VALIDATION=1 next lint",
"test": "vitest",
"dev:emails": "email dev --dir ./src/emails",
"stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe"
@@ -146,6 +146,7 @@
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
+ "cross-env": "^7.0.3",
"eslint": "^8",
"eslint-config-next": "14.2.6",
"eslint-plugin-react": "^7.35.0",
diff --git a/packages/web/src/app/[domain]/browse/[...path]/page.tsx b/packages/web/src/app/[domain]/browse/[...path]/page.tsx
index 804f306b..89e4e293 100644
--- a/packages/web/src/app/[domain]/browse/[...path]/page.tsx
+++ b/packages/web/src/app/[domain]/browse/[...path]/page.tsx
@@ -1,7 +1,8 @@
import { FileHeader } from "@/app/[domain]/components/fileHeader";
import { TopBar } from "@/app/[domain]/components/topBar";
import { Separator } from '@/components/ui/separator';
-import { getFileSource, listRepositories } from '@/lib/server/searchService';
+import { getFileSource } from '@/features/search/fileSourceApi';
+import { listRepositories } from '@/features/search/listReposApi';
import { base64Decode, isServiceError } from "@/lib/utils";
import { CodePreview } from "./codePreview";
import { ErrorCode } from "@/lib/errorCodes";
@@ -57,7 +58,7 @@ export default async function BrowsePage({
if (isServiceError(reposResponse)) {
throw new ServiceErrorException(reposResponse);
}
- const repo = reposResponse.List.Repos.find(r => r.Repository.Name === repoName);
+ const repo = reposResponse.repos.find(r => r.name === repoName);
if (pathType === 'tree') {
// @todo : proper tree handling
@@ -81,7 +82,7 @@ export default async function BrowsePage({
diff --git a/packages/web/src/app/[domain]/components/fileHeader.tsx b/packages/web/src/app/[domain]/components/fileHeader.tsx
index 6b8630d6..0ff343f8 100644
--- a/packages/web/src/app/[domain]/components/fileHeader.tsx
+++ b/packages/web/src/app/[domain]/components/fileHeader.tsx
@@ -1,4 +1,4 @@
-import { Repository } from "@/lib/types";
+import { Repository } from "@/features/search/types";
import { getRepoCodeHostInfo } from "@/lib/utils";
import { LaptopIcon } from "@radix-ui/react-icons";
import clsx from "clsx";
diff --git a/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx b/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx
index f95a926e..5b8fdac3 100644
--- a/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx
+++ b/packages/web/src/app/[domain]/components/searchBar/searchSuggestionsBox.tsx
@@ -275,6 +275,7 @@ const SearchSuggestionsBox = forwardRef(({
searchHistorySuggestions,
languageSuggestions,
searchContextSuggestions,
+ refineModeSuggestions,
]);
// When the list of suggestions change, reset the highlight index
diff --git a/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeAndQuery.ts b/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeAndQuery.ts
index 6aa4ff9d..42474a17 100644
--- a/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeAndQuery.ts
+++ b/packages/web/src/app/[domain]/components/searchBar/useSuggestionModeAndQuery.ts
@@ -69,7 +69,7 @@ export const useSuggestionModeAndQuery = ({
suggestionQuery: part,
suggestionMode: "refine",
}
- }, [cursorPosition, isSuggestionsEnabled, query, isHistorySearchEnabled]);
+ }, [cursorPosition, isSuggestionsEnabled, query, isHistorySearchEnabled, suggestionModeMappings]);
// Debug logging for tracking mode transitions.
const [prevSuggestionMode, setPrevSuggestionMode] = useState("none");
diff --git a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts
index ac13d4f8..e77c07cf 100644
--- a/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts
+++ b/packages/web/src/app/[domain]/components/searchBar/useSuggestionsData.ts
@@ -5,7 +5,7 @@ import { Suggestion, SuggestionMode } from "./searchSuggestionsBox";
import { getRepos, search } from "@/app/api/(client)/client";
import { getSearchContexts } from "@/actions";
import { useMemo } from "react";
-import { Symbol } from "@/lib/types";
+import { SearchSymbol } from "@/features/search/types";
import { languageMetadataMap } from "@/lib/languageMetadata";
import {
VscSymbolClass,
@@ -40,10 +40,9 @@ export const useSuggestionsData = ({
queryKey: ["repoSuggestions"],
queryFn: () => getRepos(domain),
select: (data): Suggestion[] => {
- return data.List.Repos
- .map(r => r.Repository)
+ return data.repos
.map(r => ({
- value: r.Name
+ value: r.name,
}));
},
enabled: suggestionMode === "repo",
@@ -54,16 +53,17 @@ export const useSuggestionsData = ({
queryKey: ["fileSuggestions", suggestionQuery],
queryFn: () => search({
query: `file:${suggestionQuery}`,
- maxMatchDisplayCount: 15,
+ matches: 15,
+ contextLines: 1,
}, domain),
select: (data): Suggestion[] => {
if (isServiceError(data)) {
return [];
}
- return data.Result.Files?.map((file) => ({
- value: file.FileName
- })) ?? [];
+ return data.files.map((file) => ({
+ value: file.fileName.text,
+ }));
},
enabled: suggestionMode === "file"
});
@@ -73,22 +73,23 @@ export const useSuggestionsData = ({
queryKey: ["symbolSuggestions", suggestionQuery],
queryFn: () => search({
query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`,
- maxMatchDisplayCount: 15,
+ matches: 15,
+ contextLines: 1,
}, domain),
select: (data): Suggestion[] => {
if (isServiceError(data)) {
return [];
}
- const symbols = data.Result.Files?.flatMap((file) => file.ChunkMatches).flatMap((chunk) => chunk.SymbolInfo ?? []);
+ const symbols = data.files.flatMap((file) => file.chunks).flatMap((chunk) => chunk.symbols ?? []);
if (!symbols) {
return [];
}
// De-duplicate on symbol name & kind.
- const symbolMap = new Map(symbols.map((symbol: Symbol) => [`${symbol.Kind}.${symbol.Sym}`, symbol]));
+ const symbolMap = new Map(symbols.map((symbol: SearchSymbol) => [`${symbol.kind}.${symbol.symbol}`, symbol]));
const suggestions = Array.from(symbolMap.values()).map((symbol) => ({
- value: symbol.Sym,
+ value: symbol.symbol,
Icon: getSymbolIcon(symbol),
} satisfies Suggestion));
@@ -157,8 +158,8 @@ export const useSuggestionsData = ({
}
}
-const getSymbolIcon = (symbol: Symbol) => {
- switch (symbol.Kind) {
+const getSymbolIcon = (symbol: SearchSymbol) => {
+ switch (symbol.kind) {
case "methodSpec":
case "method":
case "function":
diff --git a/packages/web/src/app/[domain]/connections/[id]/page.tsx b/packages/web/src/app/[domain]/connections/[id]/page.tsx
index 93a53e9d..04fc1512 100644
--- a/packages/web/src/app/[domain]/connections/[id]/page.tsx
+++ b/packages/web/src/app/[domain]/connections/[id]/page.tsx
@@ -22,7 +22,6 @@ import { isServiceError } from "@/lib/utils"
import { notFound } from "next/navigation"
import { OrgRole } from "@sourcebot/db"
import { CodeHostType } from "@/lib/utils"
-import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type"
interface ConnectionManagementPageProps {
params: {
diff --git a/packages/web/src/app/[domain]/connections/quickActions.tsx b/packages/web/src/app/[domain]/connections/quickActions.tsx
index d0e7736d..af0b4f05 100644
--- a/packages/web/src/app/[domain]/connections/quickActions.tsx
+++ b/packages/web/src/app/[domain]/connections/quickActions.tsx
@@ -403,7 +403,7 @@ export const bitbucketCloudQuickActions: QuickAction[
selectionText: "username",
description: (
- Username to use for authentication. This is only required if you're using an App Password (stored in token) for authentication.
+ Username to use for authentication. This is only required if you're using an App Password (stored in token) for authentication.
)
},
diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx b/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx
index 0681d503..6658d9c7 100644
--- a/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx
+++ b/packages/web/src/app/[domain]/search/components/codePreviewPanel/codePreview.tsx
@@ -3,12 +3,12 @@
import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
+import { SearchResultChunk } from "@/features/search/types";
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
import { useSyntaxHighlightingExtension } from "@/hooks/useSyntaxHighlightingExtension";
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
-import { SearchResultFileMatch } from "@/lib/types";
import { search } from "@codemirror/search";
import { EditorView } from "@codemirror/view";
import { Cross1Icon, FileIcon } from "@radix-ui/react-icons";
@@ -22,7 +22,7 @@ export interface CodePreviewFile {
content: string;
filepath: string;
link?: string;
- matches: SearchResultFileMatch[];
+ matches: SearchResultChunk[];
language: string;
revision: string;
}
@@ -84,7 +84,7 @@ export const CodePreview = ({
}
return file.matches.flatMap((match) => {
- return match.Ranges;
+ return match.matchRanges;
})
}, [file]);
diff --git a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx
index dc55b84d..97218b3d 100644
--- a/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx
+++ b/packages/web/src/app/[domain]/search/components/codePreviewPanel/index.tsx
@@ -4,9 +4,10 @@ import { fetchFileSource } from "@/app/api/(client)/client";
import { base64Decode } from "@/lib/utils";
import { useQuery } from "@tanstack/react-query";
import { CodePreview, CodePreviewFile } from "./codePreview";
-import { SearchResultFile } from "@/lib/types";
+import { SearchResultFile } from "@/features/search/types";
import { useDomain } from "@/hooks/useDomain";
import { SymbolIcon } from "@radix-ui/react-icons";
+
interface CodePreviewPanelProps {
fileMatch?: SearchResultFile;
onClose: () => void;
@@ -25,7 +26,7 @@ export const CodePreviewPanel = ({
const domain = useDomain();
const { data: file, isLoading } = useQuery({
- queryKey: ["source", fileMatch?.FileName, fileMatch?.Repository, fileMatch?.Branches],
+ queryKey: ["source", fileMatch?.fileName, fileMatch?.repository, fileMatch?.branches],
queryFn: async (): Promise => {
if (!fileMatch) {
return undefined;
@@ -33,16 +34,16 @@ export const CodePreviewPanel = ({
// If there are multiple branches pointing to the same revision of this file, it doesn't
// matter which branch we use here, so use the first one.
- const branch = fileMatch.Branches && fileMatch.Branches.length > 0 ? fileMatch.Branches[0] : undefined;
+ const branch = fileMatch.branches && fileMatch.branches.length > 0 ? fileMatch.branches[0] : undefined;
return fetchFileSource({
- fileName: fileMatch.FileName,
- repository: fileMatch.Repository,
+ fileName: fileMatch.fileName.text,
+ repository: fileMatch.repository,
branch,
}, domain)
.then(({ source }) => {
const link = (() => {
- const template = repoUrlTemplates[fileMatch.Repository];
+ const template = repoUrlTemplates[fileMatch.repository];
// This is a hacky parser for templates generated by
// the go text/template package. Example template:
@@ -55,7 +56,7 @@ export const CodePreviewPanel = ({
const url =
template.substring("{{URLJoinPath ".length,template.indexOf("}}"))
.replace(".Version", branch ?? "HEAD")
- .replace(".Path", fileMatch.FileName)
+ .replace(".Path", fileMatch.fileName.text)
.split(" ")
.map((part) => {
// remove wrapping quotes
@@ -68,24 +69,19 @@ export const CodePreviewPanel = ({
const optionalQueryParams =
template.substring(template.indexOf("}}") + 2)
.replace("{{.Version}}", branch ?? "HEAD")
- .replace("{{.Path}}", fileMatch.FileName);
+ .replace("{{.Path}}", fileMatch.fileName.text);
return url + optionalQueryParams;
})();
const decodedSource = base64Decode(source);
- // Filter out filename matches
- const filteredMatches = fileMatch.ChunkMatches.filter((match) => {
- return !match.FileName;
- });
-
return {
content: decodedSource,
- filepath: fileMatch.FileName,
- matches: filteredMatches,
+ filepath: fileMatch.fileName.text,
+ matches: fileMatch.chunks,
link: link,
- language: fileMatch.Language,
+ language: fileMatch.language,
revision: branch ?? "HEAD",
};
});
@@ -103,7 +99,7 @@ export const CodePreviewPanel = ({
return (
{
+ const getSelectedFromQuery = useCallback((param: string) => {
const value = searchParams.get(param);
return value ? new Set(value.split(',')) : new Set();
- };
+ }, [searchParams]);
const repos = useMemo(() => {
const selectedRepos = getSelectedFromQuery(REPOS_QUERY_PARAM);
return aggregateMatches(
- "Repository",
+ "repository",
matches,
(key) => {
const repo: Repository | undefined = repoMetadata[key];
@@ -60,12 +60,12 @@ export const FilterPanel = ({
};
}
)
- }, [searchParams]);
+ }, [getSelectedFromQuery, matches, repoMetadata]);
const languages = useMemo(() => {
const selectedLanguages = getSelectedFromQuery(LANGUAGES_QUERY_PARAM);
return aggregateMatches(
- "Language",
+ "language",
matches,
(key) => {
const Icon = (
@@ -81,7 +81,7 @@ export const FilterPanel = ({
} satisfies Entry;
}
);
- }, [searchParams]);
+ }, [getSelectedFromQuery, matches]);
// Calls `onFilterChanged` with the filtered list of matches
// whenever the filter state changes.
@@ -91,8 +91,8 @@ export const FilterPanel = ({
const filteredMatches = matches.filter((match) =>
(
- (selectedRepos.size === 0 ? true : selectedRepos.has(match.Repository)) &&
- (selectedLanguages.size === 0 ? true : selectedLanguages.has(match.Language))
+ (selectedRepos.size === 0 ? true : selectedRepos.has(match.repository)) &&
+ (selectedLanguages.size === 0 ? true : selectedLanguages.has(match.language))
)
);
onFilterChanged(filteredMatches);
@@ -166,7 +166,7 @@ export const FilterPanel = ({
* }
*/
const aggregateMatches = (
- propName: 'Repository' | 'Language',
+ propName: 'repository' | 'language',
matches: SearchResultFile[],
createEntry: (key: string) => Entry
) => {
diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/codePreview.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/codePreview.tsx
index d87cde69..47cb2678 100644
--- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/codePreview.tsx
+++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/codePreview.tsx
@@ -2,7 +2,7 @@
import { getCodemirrorLanguage } from "@/lib/codemirrorLanguage";
import { lineOffsetExtension } from "@/lib/extensions/lineOffsetExtension";
-import { SearchResultRange } from "@/lib/types";
+import { SearchResultRange } from "@/features/search/types";
import { EditorState, StateField, Transaction } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView, lineNumbers } from "@codemirror/view";
import { useMemo, useRef } from "react";
@@ -43,11 +43,11 @@ export const CodePreview = ({
const decorations = ranges
.sort((a, b) => {
- return a.Start.ByteOffset - b.Start.ByteOffset;
+ return a.start.byteOffset - b.start.byteOffset;
})
- .filter(({ Start, End }) => {
- const startLine = Start.LineNumber - lineOffset;
- const endLine = End.LineNumber - lineOffset;
+ .filter(({ start, end }) => {
+ const startLine = start.lineNumber - lineOffset;
+ const endLine = end.lineNumber - lineOffset;
if (
startLine < 1 ||
@@ -59,12 +59,12 @@ export const CodePreview = ({
}
return true;
})
- .map(({ Start, End }) => {
- const startLine = Start.LineNumber - lineOffset;
- const endLine = End.LineNumber - lineOffset;
+ .map(({ start, end }) => {
+ const startLine = start.lineNumber - lineOffset;
+ const endLine = end.lineNumber - lineOffset;
- const from = document.line(startLine).from + Start.Column - 1;
- const to = document.line(endLine).from + End.Column - 1;
+ const from = document.line(startLine).from + start.column - 1;
+ const to = document.line(endLine).from + end.column - 1;
return markDecoration.range(from, to);
})
.sort((a, b) => a.from - b.from);
diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx
index 981ad746..aaefe1a6 100644
--- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx
+++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatch.tsx
@@ -2,12 +2,12 @@
import { useMemo } from "react";
import { CodePreview } from "./codePreview";
-import { SearchResultFile, SearchResultFileMatch } from "@/lib/types";
+import { SearchResultFile, SearchResultChunk } from "@/features/search/types";
import { base64Decode } from "@/lib/utils";
interface FileMatchProps {
- match: SearchResultFileMatch;
+ match: SearchResultChunk;
file: SearchResultFile;
onOpen: () => void;
}
@@ -18,11 +18,11 @@ export const FileMatch = ({
onOpen,
}: FileMatchProps) => {
const content = useMemo(() => {
- return base64Decode(match.Content);
- }, [match.Content]);
+ return base64Decode(match.content);
+ }, [match.content]);
// If it's just the title, don't show a code preview
- if (match.FileName) {
+ if (match.matchRanges.length === 0) {
return null;
}
@@ -40,9 +40,9 @@ export const FileMatch = ({
>
);
diff --git a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx
index 0a8e74a6..2efddcc7 100644
--- a/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx
+++ b/packages/web/src/app/[domain]/search/components/searchResultsPanel/fileMatchContainer.tsx
@@ -2,10 +2,10 @@
import { FileHeader } from "@/app/[domain]/components/fileHeader";
import { Separator } from "@/components/ui/separator";
-import { Repository, SearchResultFile } from "@/lib/types";
import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons";
import { useCallback, useMemo } from "react";
import { FileMatch } from "./fileMatch";
+import { Repository, SearchResultFile } from "@/features/search/types";
export const MAX_MATCHES_TO_PREVIEW = 3;
@@ -32,12 +32,12 @@ export const FileMatchContainer = ({
}: FileMatchContainerProps) => {
const matchCount = useMemo(() => {
- return file.ChunkMatches.length;
+ return file.chunks.length;
}, [file]);
const matches = useMemo(() => {
- const sortedMatches = file.ChunkMatches.sort((a, b) => {
- return a.ContentStart.LineNumber - b.ContentStart.LineNumber;
+ const sortedMatches = file.chunks.sort((a, b) => {
+ return a.contentStart.lineNumber - b.contentStart.lineNumber;
});
if (!showAllMatches) {
@@ -48,18 +48,16 @@ export const FileMatchContainer = ({
}, [file, showAllMatches]);
const fileNameRange = useMemo(() => {
- for (const match of matches) {
- if (match.FileName && match.Ranges.length > 0) {
- const range = match.Ranges[0];
- return {
- from: range.Start.Column - 1,
- to: range.End.Column - 1,
- }
+ if (file.fileName.matchRanges.length > 0) {
+ const range = file.fileName.matchRanges[0];
+ return {
+ from: range.start.column - 1,
+ to: range.end.column - 1,
}
}
return undefined;
- }, [matches]);
+ }, [file.fileName.matchRanges]);
const isMoreContentButtonVisible = useMemo(() => {
return matchCount > MAX_MATCHES_TO_PREVIEW;
@@ -67,19 +65,19 @@ export const FileMatchContainer = ({
const onOpenMatch = useCallback((index: number) => {
const matchIndex = matches.slice(0, index).reduce((acc, match) => {
- return acc + match.Ranges.length;
+ return acc + match.matchRanges.length;
}, 0);
onOpenFile();
onMatchIndexChanged(matchIndex);
}, [matches, onMatchIndexChanged, onOpenFile]);
const branches = useMemo(() => {
- if (!file.Branches) {
+ if (!file.branches) {
return [];
}
- return file.Branches;
- }, [file.Branches]);
+ return file.branches;
+ }, [file.branches]);
const branchDisplayName = useMemo(() => {
if (!isBranchFilteringEnabled || branches.length === 0) {
@@ -103,8 +101,8 @@ export const FileMatchContainer = ({
}}
>
!match.FileName)
- .slice(0, showAllMatches ? fileMatch.ChunkMatches.length : MAX_MATCHES_TO_PREVIEW)
+ const numCodeCells = fileMatch.chunks
+ .slice(0, showAllMatches ? fileMatch.chunks.length : MAX_MATCHES_TO_PREVIEW)
.length;
const estimatedSize =
diff --git a/packages/web/src/app/[domain]/search/page.tsx b/packages/web/src/app/[domain]/search/page.tsx
index aab869fa..f3aa90c5 100644
--- a/packages/web/src/app/[domain]/search/page.tsx
+++ b/packages/web/src/app/[domain]/search/page.tsx
@@ -9,7 +9,7 @@ import { Separator } from "@/components/ui/separator";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
import { useSearchHistory } from "@/hooks/useSearchHistory";
-import { Repository, SearchQueryParams, SearchResultFile } from "@/lib/types";
+import { SearchQueryParams } from "@/lib/types";
import { createPathWithQueryParams, measure, unwrapServiceError } from "@/lib/utils";
import { InfoCircledIcon, SymbolIcon } from "@radix-ui/react-icons";
import { useQuery } from "@tanstack/react-query";
@@ -23,8 +23,9 @@ import { FilterPanel } from "./components/filterPanel";
import { SearchResultsPanel } from "./components/searchResultsPanel";
import { useDomain } from "@/hooks/useDomain";
import { useToast } from "@/components/hooks/use-toast";
+import { Repository, SearchResultFile } from "@/features/search/types";
-const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 10000;
+const DEFAULT_MATCH_COUNT = 10000;
export default function SearchPage() {
// We need a suspense boundary here since we are accessing query params
@@ -40,18 +41,20 @@ export default function SearchPage() {
const SearchPageInternal = () => {
const router = useRouter();
const searchQuery = useNonEmptyQueryParam(SearchQueryParams.query) ?? "";
- const _maxMatchDisplayCount = parseInt(useNonEmptyQueryParam(SearchQueryParams.maxMatchDisplayCount) ?? `${DEFAULT_MAX_MATCH_DISPLAY_COUNT}`);
- const maxMatchDisplayCount = isNaN(_maxMatchDisplayCount) ? DEFAULT_MAX_MATCH_DISPLAY_COUNT : _maxMatchDisplayCount;
+ const _matches = parseInt(useNonEmptyQueryParam(SearchQueryParams.matches) ?? `${DEFAULT_MATCH_COUNT}`);
+ const matches = isNaN(_matches) ? DEFAULT_MATCH_COUNT : _matches;
const { setSearchHistory } = useSearchHistory();
const captureEvent = useCaptureEvent();
const domain = useDomain();
const { toast } = useToast();
const { data: searchResponse, isLoading: isSearchLoading, error } = useQuery({
- queryKey: ["search", searchQuery, maxMatchDisplayCount],
+ queryKey: ["search", searchQuery, matches],
queryFn: () => measure(() => unwrapServiceError(search({
query: searchQuery,
- maxMatchDisplayCount,
+ matches,
+ contextLines: 3,
+ whole: false,
}, domain)), "client.search"),
select: ({ data, durationMs }) => ({
...data,
@@ -95,12 +98,11 @@ const SearchPageInternal = () => {
queryKey: ["repos"],
queryFn: () => getRepos(domain),
select: (data): Record =>
- data.List.Repos
- .map(r => r.Repository)
+ data.repos
.reduce(
(acc, repo) => ({
...acc,
- [repo.Name]: repo,
+ [repo.name]: repo,
}),
{},
),
@@ -112,29 +114,29 @@ const SearchPageInternal = () => {
return;
}
- const fileLanguages = searchResponse.Result.Files?.map(file => file.Language) || [];
+ const fileLanguages = searchResponse.files?.map(file => file.language) || [];
captureEvent("search_finished", {
- contentBytesLoaded: searchResponse.Result.ContentBytesLoaded,
- indexBytesLoaded: searchResponse.Result.IndexBytesLoaded,
- crashes: searchResponse.Result.Crashes,
- durationMs: searchResponse.Result.Duration / 1000000,
- fileCount: searchResponse.Result.FileCount,
- shardFilesConsidered: searchResponse.Result.ShardFilesConsidered,
- filesConsidered: searchResponse.Result.FilesConsidered,
- filesLoaded: searchResponse.Result.FilesLoaded,
- filesSkipped: searchResponse.Result.FilesSkipped,
- shardsScanned: searchResponse.Result.ShardsScanned,
- shardsSkipped: searchResponse.Result.ShardsSkipped,
- shardsSkippedFilter: searchResponse.Result.ShardsSkippedFilter,
- matchCount: searchResponse.Result.MatchCount,
- ngramMatches: searchResponse.Result.NgramMatches,
- ngramLookups: searchResponse.Result.NgramLookups,
- wait: searchResponse.Result.Wait,
- matchTreeConstruction: searchResponse.Result.MatchTreeConstruction,
- matchTreeSearch: searchResponse.Result.MatchTreeSearch,
- regexpsConsidered: searchResponse.Result.RegexpsConsidered,
- flushReason: searchResponse.Result.FlushReason,
+ durationMs: searchResponse.durationMs,
+ fileCount: searchResponse.zoektStats.fileCount,
+ matchCount: searchResponse.zoektStats.matchCount,
+ filesSkipped: searchResponse.zoektStats.filesSkipped,
+ contentBytesLoaded: searchResponse.zoektStats.contentBytesLoaded,
+ indexBytesLoaded: searchResponse.zoektStats.indexBytesLoaded,
+ crashes: searchResponse.zoektStats.crashes,
+ shardFilesConsidered: searchResponse.zoektStats.shardFilesConsidered,
+ filesConsidered: searchResponse.zoektStats.filesConsidered,
+ filesLoaded: searchResponse.zoektStats.filesLoaded,
+ shardsScanned: searchResponse.zoektStats.shardsScanned,
+ shardsSkipped: searchResponse.zoektStats.shardsSkipped,
+ shardsSkippedFilter: searchResponse.zoektStats.shardsSkippedFilter,
+ ngramMatches: searchResponse.zoektStats.ngramMatches,
+ ngramLookups: searchResponse.zoektStats.ngramLookups,
+ wait: searchResponse.zoektStats.wait,
+ matchTreeConstruction: searchResponse.zoektStats.matchTreeConstruction,
+ matchTreeSearch: searchResponse.zoektStats.matchTreeSearch,
+ regexpsConsidered: searchResponse.zoektStats.regexpsConsidered,
+ flushReason: searchResponse.zoektStats.flushReason,
fileLanguages,
});
}, [captureEvent, searchQuery, searchResponse]);
@@ -151,24 +153,24 @@ const SearchPageInternal = () => {
}
return {
- fileMatches: searchResponse.Result.Files ?? [],
+ fileMatches: searchResponse.files ?? [],
searchDurationMs: Math.round(searchResponse.durationMs),
- totalMatchCount: searchResponse.Result.MatchCount,
+ totalMatchCount: searchResponse.zoektStats.matchCount,
isBranchFilteringEnabled: searchResponse.isBranchFilteringEnabled,
- repoUrlTemplates: searchResponse.Result.RepoURLs,
+ repoUrlTemplates: searchResponse.repoUrlTemplates,
}
}, [searchResponse]);
const isMoreResultsButtonVisible = useMemo(() => {
- return totalMatchCount > maxMatchDisplayCount;
- }, [totalMatchCount, maxMatchDisplayCount]);
+ return totalMatchCount > matches;
+ }, [totalMatchCount, matches]);
const numMatches = useMemo(() => {
// Accumualtes the number of matches across all files
return fileMatches.reduce(
(acc, file) =>
- acc + file.ChunkMatches.reduce(
- (acc, chunk) => acc + chunk.Ranges.length,
+ acc + file.chunks.reduce(
+ (acc, chunk) => acc + chunk.matchRanges.length,
0,
),
0,
@@ -178,10 +180,10 @@ const SearchPageInternal = () => {
const onLoadMoreResults = useCallback(() => {
const url = createPathWithQueryParams(`/${domain}/search`,
[SearchQueryParams.query, searchQuery],
- [SearchQueryParams.maxMatchDisplayCount, `${maxMatchDisplayCount * 2}`],
+ [SearchQueryParams.matches, `${matches * 2}`],
)
router.push(url);
- }, [maxMatchDisplayCount, router, searchQuery, domain]);
+ }, [matches, router, searchQuery, domain]);
return (
diff --git a/packages/web/src/app/api/(client)/client.ts b/packages/web/src/app/api/(client)/client.ts
index 44349cb6..c23d0cde 100644
--- a/packages/web/src/app/api/(client)/client.ts
+++ b/packages/web/src/app/api/(client)/client.ts
@@ -1,9 +1,21 @@
'use client';
-import { fileSourceResponseSchema, getVersionResponseSchema, listRepositoriesResponseSchema, searchResponseSchema } from "@/lib/schemas";
+import { getVersionResponseSchema } from "@/lib/schemas";
import { ServiceError } from "@/lib/serviceError";
-import { FileSourceRequest, FileSourceResponse, GetVersionResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "@/lib/types";
+import { GetVersionResponse } from "@/lib/types";
import { isServiceError } from "@/lib/utils";
+import {
+ FileSourceResponse,
+ FileSourceRequest,
+ ListRepositoriesResponse,
+ SearchRequest,
+ SearchResponse,
+} from "@/features/search/types";
+import {
+ fileSourceResponseSchema,
+ listRepositoriesResponseSchema,
+ searchResponseSchema,
+} from "@/features/search/schemas";
export const search = async (body: SearchRequest, domain: string): Promise
=> {
const result = await fetch("/api/search", {
diff --git a/packages/web/src/app/api/(server)/repos/route.ts b/packages/web/src/app/api/(server)/repos/route.ts
index 829d4f7c..893c3c49 100644
--- a/packages/web/src/app/api/(server)/repos/route.ts
+++ b/packages/web/src/app/api/(server)/repos/route.ts
@@ -1,6 +1,6 @@
'use server';
-import { listRepositories } from "@/lib/server/searchService";
+import { listRepositories } from "@/features/search/listReposApi";
import { NextRequest } from "next/server";
import { sew, withAuth, withOrgMembership } from "@/actions";
import { isServiceError } from "@/lib/utils";
diff --git a/packages/web/src/app/api/(server)/search/route.ts b/packages/web/src/app/api/(server)/search/route.ts
index 54161b0d..d04279a3 100644
--- a/packages/web/src/app/api/(server)/search/route.ts
+++ b/packages/web/src/app/api/(server)/search/route.ts
@@ -1,12 +1,12 @@
'use server';
-import { search } from "@/lib/server/searchService";
-import { searchRequestSchema } from "@/lib/schemas";
+import { search } from "@/features/search/searchApi";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { sew, withAuth, withOrgMembership } from "@/actions";
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
-import { SearchRequest } from "@/lib/types";
+import { searchRequestSchema } from "@/features/search/schemas";
+import { SearchRequest } from "@/features/search/types";
export const POST = async (request: NextRequest) => {
const domain = request.headers.get("X-Org-Domain")!;
diff --git a/packages/web/src/app/api/(server)/source/route.ts b/packages/web/src/app/api/(server)/source/route.ts
index 19962d50..78522f11 100644
--- a/packages/web/src/app/api/(server)/source/route.ts
+++ b/packages/web/src/app/api/(server)/source/route.ts
@@ -1,12 +1,12 @@
'use server';
-import { fileSourceRequestSchema } from "@/lib/schemas";
-import { getFileSource } from "@/lib/server/searchService";
+import { getFileSource } from "@/features/search/fileSourceApi";
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { NextRequest } from "next/server";
import { sew, withAuth, withOrgMembership } from "@/actions";
-import { FileSourceRequest } from "@/lib/types";
+import { fileSourceRequestSchema } from "@/features/search/schemas";
+import { FileSourceRequest } from "@/features/search/types";
export const POST = async (request: NextRequest) => {
const body = await request.json();
diff --git a/packages/web/src/features/entitlements/constants.ts b/packages/web/src/features/entitlements/constants.ts
index 1701913a..665b1f17 100644
--- a/packages/web/src/features/entitlements/constants.ts
+++ b/packages/web/src/features/entitlements/constants.ts
@@ -1,4 +1,5 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
const planLabels = {
oss: "OSS",
"cloud:team": "Team",
@@ -7,6 +8,7 @@ const planLabels = {
export type Plan = keyof typeof planLabels;
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
const entitlements = [
"search-contexts",
"billing"
diff --git a/packages/web/src/features/search/fileSourceApi.ts b/packages/web/src/features/search/fileSourceApi.ts
new file mode 100644
index 00000000..87ef4b0a
--- /dev/null
+++ b/packages/web/src/features/search/fileSourceApi.ts
@@ -0,0 +1,42 @@
+import escapeStringRegexp from "escape-string-regexp";
+import { fileNotFound, ServiceError } from "../../lib/serviceError";
+import { FileSourceRequest, FileSourceResponse } from "./types";
+import { isServiceError } from "../../lib/utils";
+import { search } from "./searchApi";
+
+// @todo (bkellam) : We should really be using `git show :` to fetch file contents here.
+// This will allow us to support permalinks to files at a specific revision that may not be indexed
+// by zoekt.
+export const getFileSource = async ({ fileName, repository, branch }: FileSourceRequest, orgId: number): Promise => {
+ const escapedFileName = escapeStringRegexp(fileName);
+ const escapedRepository = escapeStringRegexp(repository);
+
+ let query = `file:${escapedFileName} repo:^${escapedRepository}$`;
+ if (branch) {
+ query = query.concat(` branch:${branch}`);
+ }
+
+ const searchResponse = await search({
+ query,
+ matches: 1,
+ whole: true,
+ }, orgId);
+
+ if (isServiceError(searchResponse)) {
+ return searchResponse;
+ }
+
+ const files = searchResponse.files;
+
+ if (!files || files.length === 0) {
+ return fileNotFound(fileName, repository);
+ }
+
+ const file = files[0];
+ const source = file.content ?? '';
+ const language = file.language;
+ return {
+ source,
+ language,
+ } satisfies FileSourceResponse;
+}
\ No newline at end of file
diff --git a/packages/web/src/features/search/listReposApi.ts b/packages/web/src/features/search/listReposApi.ts
new file mode 100644
index 00000000..7baae79c
--- /dev/null
+++ b/packages/web/src/features/search/listReposApi.ts
@@ -0,0 +1,44 @@
+import { invalidZoektResponse, ServiceError } from "../../lib/serviceError";
+import { ListRepositoriesResponse } from "./types";
+import { zoektFetch } from "./zoektClient";
+import { zoektListRepositoriesResponseSchema } from "./zoektSchema";
+
+
+export const listRepositories = async (orgId: number): Promise => {
+ const body = JSON.stringify({
+ opts: {
+ Field: 0,
+ }
+ });
+
+ let header: Record = {};
+ header = {
+ "X-Tenant-ID": orgId.toString()
+ };
+
+ const listResponse = await zoektFetch({
+ path: "/api/list",
+ body,
+ header,
+ method: "POST",
+ cache: "no-store",
+ });
+
+ if (!listResponse.ok) {
+ return invalidZoektResponse(listResponse);
+ }
+
+ const listBody = await listResponse.json();
+
+ const parser = zoektListRepositoriesResponseSchema.transform(({ List }) => ({
+ repos: List.Repos.map((repo) => ({
+ name: repo.Repository.Name,
+ url: repo.Repository.URL,
+ source: repo.Repository.Source,
+ branches: repo.Repository.Branches?.map((branch) => branch.Name) ?? [],
+ rawConfig: repo.Repository.RawConfig ?? undefined,
+ }))
+ } satisfies ListRepositoriesResponse));
+
+ return parser.parse(listBody);
+}
\ No newline at end of file
diff --git a/packages/web/src/features/search/schemas.ts b/packages/web/src/features/search/schemas.ts
new file mode 100644
index 00000000..0b087cb0
--- /dev/null
+++ b/packages/web/src/features/search/schemas.ts
@@ -0,0 +1,104 @@
+import { z } from "zod";
+
+export const locationSchema = z.object({
+ // 0-based byte offset from the beginning of the file
+ byteOffset: z.number(),
+ // 1-based line number from the beginning of the file
+ lineNumber: z.number(),
+ // 1-based column number (in runes) from the beginning of line
+ column: z.number(),
+});
+
+export const rangeSchema = z.object({
+ start: locationSchema,
+ end: locationSchema,
+});
+
+export const symbolSchema = z.object({
+ symbol: z.string(),
+ kind: z.string(),
+});
+
+export const searchRequestSchema = z.object({
+ // The zoekt query to execute.
+ query: z.string(),
+ // The number of matches to return.
+ matches: z.number(),
+ // The number of context lines to return.
+ contextLines: z.number().optional(),
+ // Whether to return the whole file as part of the response.
+ whole: z.boolean().optional(),
+});
+
+export const searchResponseSchema = z.object({
+ zoektStats: z.object({
+ // The duration (in nanoseconds) of the search.
+ duration: z.number(),
+ fileCount: z.number(),
+ matchCount: z.number(),
+ filesSkipped: z.number(),
+ contentBytesLoaded: z.number(),
+ indexBytesLoaded: z.number(),
+ crashes: z.number(),
+ shardFilesConsidered: z.number(),
+ filesConsidered: z.number(),
+ filesLoaded: z.number(),
+ shardsScanned: z.number(),
+ shardsSkipped: z.number(),
+ shardsSkippedFilter: z.number(),
+ ngramMatches: z.number(),
+ ngramLookups: z.number(),
+ wait: z.number(),
+ matchTreeConstruction: z.number(),
+ matchTreeSearch: z.number(),
+ regexpsConsidered: z.number(),
+ flushReason: z.number(),
+ }),
+ files: z.array(z.object({
+ fileName: z.object({
+ // The name of the file
+ text: z.string(),
+ // Any matching ranges
+ matchRanges: z.array(rangeSchema),
+ }),
+ repository: z.string(),
+ language: z.string(),
+ chunks: z.array(z.object({
+ content: z.string(),
+ matchRanges: z.array(rangeSchema),
+ contentStart: locationSchema,
+ symbols: z.array(z.object({
+ ...symbolSchema.shape,
+ parent: symbolSchema.optional(),
+ })).optional(),
+ })),
+ branches: z.array(z.string()).optional(),
+ // Set if `whole` is true.
+ content: z.string().optional(),
+ })),
+ repoUrlTemplates: z.record(z.string(), z.string()),
+ isBranchFilteringEnabled: z.boolean(),
+});
+
+export const repositorySchema = z.object({
+ name: z.string(),
+ url: z.string(),
+ source: z.string(),
+ branches: z.array(z.string()),
+ rawConfig: z.record(z.string(), z.string()).optional(),
+});
+
+export const listRepositoriesResponseSchema = z.object({
+ repos: z.array(repositorySchema),
+});
+
+export const fileSourceRequestSchema = z.object({
+ fileName: z.string(),
+ repository: z.string(),
+ branch: z.string().optional(),
+});
+
+export const fileSourceResponseSchema = z.object({
+ source: z.string(),
+ language: z.string(),
+});
\ No newline at end of file
diff --git a/packages/web/src/features/search/searchApi.ts b/packages/web/src/features/search/searchApi.ts
new file mode 100644
index 00000000..eb055b50
--- /dev/null
+++ b/packages/web/src/features/search/searchApi.ts
@@ -0,0 +1,230 @@
+import { env } from "@/env.mjs";
+import { invalidZoektResponse, ServiceError } from "../../lib/serviceError";
+import { isServiceError } from "../../lib/utils";
+import { zoektFetch } from "./zoektClient";
+import { prisma } from "@/prisma";
+import { ErrorCode } from "../../lib/errorCodes";
+import { StatusCodes } from "http-status-codes";
+import { zoektSearchResponseSchema } from "./zoektSchema";
+import { SearchRequest, SearchResponse, SearchResultRange } from "./types";
+
+// List of supported query prefixes in zoekt.
+// @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417
+enum zoektPrefixes {
+ archived = "archived:",
+ branchShort = "b:",
+ branch = "branch:",
+ caseShort = "c:",
+ case = "case:",
+ content = "content:",
+ fileShort = "f:",
+ file = "file:",
+ fork = "fork:",
+ public = "public:",
+ repoShort = "r:",
+ repo = "repo:",
+ regex = "regex:",
+ lang = "lang:",
+ sym = "sym:",
+ typeShort = "t:",
+ type = "type:",
+ reposet = "reposet:",
+}
+
+const transformZoektQuery = async (query: string, orgId: number): Promise => {
+ const prevQueryParts = query.split(" ");
+ const newQueryParts = [];
+
+ for (const part of prevQueryParts) {
+
+ // Handle mapping `rev:` and `revision:` to `branch:`
+ if (part.match(/^-?(rev|revision):.+$/)) {
+ const isNegated = part.startsWith("-");
+ let revisionName = part.slice(part.indexOf(":") + 1);
+
+ // Special case: `*` -> search all revisions.
+ // In zoekt, providing a blank string will match all branches.
+ // @see: https://github.com/sourcebot-dev/zoekt/blob/main/eval.go#L560-L562
+ if (revisionName === "*") {
+ revisionName = "";
+ }
+ newQueryParts.push(`${isNegated ? "-" : ""}${zoektPrefixes.branch}${revisionName}`);
+ }
+
+ // Expand `context:` into `reposet:` atom.
+ else if (part.match(/^-?context:.+$/)) {
+ const isNegated = part.startsWith("-");
+ const contextName = part.slice(part.indexOf(":") + 1);
+
+ const context = await prisma.searchContext.findUnique({
+ where: {
+ name_orgId: {
+ name: contextName,
+ orgId,
+ }
+ },
+ include: {
+ repos: true,
+ }
+ });
+
+ // If the context doesn't exist, return an error.
+ if (!context) {
+ return {
+ errorCode: ErrorCode.SEARCH_CONTEXT_NOT_FOUND,
+ message: `Search context "${contextName}" not found`,
+ statusCode: StatusCodes.NOT_FOUND,
+ } satisfies ServiceError;
+ }
+
+ const names = context.repos.map((repo) => repo.name);
+ newQueryParts.push(`${isNegated ? "-" : ""}${zoektPrefixes.reposet}${names.join(",")}`);
+ }
+
+ // no-op: add the original part to the new query parts.
+ else {
+ newQueryParts.push(part);
+ }
+ }
+
+ return newQueryParts.join(" ");
+}
+
+export const search = async ({ query, matches, contextLines, whole }: SearchRequest, orgId: number) => {
+ const transformedQuery = await transformZoektQuery(query, orgId);
+ if (isServiceError(transformedQuery)) {
+ return transformedQuery;
+ }
+ query = transformedQuery;
+
+ const isBranchFilteringEnabled = (
+ query.includes(zoektPrefixes.branch) ||
+ query.includes(zoektPrefixes.branchShort)
+ );
+
+ // We only want to show matches for the default branch when
+ // the user isn't explicitly filtering by branch.
+ if (!isBranchFilteringEnabled) {
+ query = query.concat(` branch:HEAD`);
+ }
+
+ const body = JSON.stringify({
+ q: query,
+ // @see: https://github.com/sourcebot-dev/zoekt/blob/main/api.go#L892
+ opts: {
+ ChunkMatches: true,
+ MaxMatchDisplayCount: matches,
+ NumContextLines: contextLines,
+ Whole: !!whole,
+ TotalMaxMatchCount: env.TOTAL_MAX_MATCH_COUNT,
+ ShardMaxMatchCount: env.SHARD_MAX_MATCH_COUNT,
+ MaxWallTime: env.ZOEKT_MAX_WALL_TIME_MS * 1000 * 1000, // zoekt expects a duration in nanoseconds
+ }
+ });
+
+ let header: Record = {};
+ header = {
+ "X-Tenant-ID": orgId.toString()
+ };
+
+ const searchResponse = await zoektFetch({
+ path: "/api/search",
+ body,
+ header,
+ method: "POST",
+ });
+
+ if (!searchResponse.ok) {
+ return invalidZoektResponse(searchResponse);
+ }
+
+ const searchBody = await searchResponse.json();
+
+ const parser = zoektSearchResponseSchema.transform(({ Result }) => ({
+ zoektStats: {
+ duration: Result.Duration,
+ fileCount: Result.FileCount,
+ matchCount: Result.MatchCount,
+ filesSkipped: Result.FilesSkipped,
+ contentBytesLoaded: Result.ContentBytesLoaded,
+ indexBytesLoaded: Result.IndexBytesLoaded,
+ crashes: Result.Crashes,
+ shardFilesConsidered: Result.ShardFilesConsidered,
+ filesConsidered: Result.FilesConsidered,
+ filesLoaded: Result.FilesLoaded,
+ shardsScanned: Result.ShardsScanned,
+ shardsSkipped: Result.ShardsSkipped,
+ shardsSkippedFilter: Result.ShardsSkippedFilter,
+ ngramMatches: Result.NgramMatches,
+ ngramLookups: Result.NgramLookups,
+ wait: Result.Wait,
+ matchTreeConstruction: Result.MatchTreeConstruction,
+ matchTreeSearch: Result.MatchTreeSearch,
+ regexpsConsidered: Result.RegexpsConsidered,
+ flushReason: Result.FlushReason,
+ },
+ files: Result.Files?.map((file) => {
+ const fileNameChunks = file.ChunkMatches.filter((chunk) => chunk.FileName);
+ return {
+ fileName: {
+ text: file.FileName,
+ matchRanges: fileNameChunks.length === 1 ? fileNameChunks[0].Ranges.map((range) => ({
+ start: {
+ byteOffset: range.Start.ByteOffset,
+ column: range.Start.Column,
+ lineNumber: range.Start.LineNumber,
+ },
+ end: {
+ byteOffset: range.End.ByteOffset,
+ column: range.End.Column,
+ lineNumber: range.End.LineNumber,
+ }
+ })) : [],
+ },
+ repository: file.Repository,
+ language: file.Language,
+ chunks: file.ChunkMatches
+ .filter((chunk) => !chunk.FileName) // Filter out filename chunks.
+ .map((chunk) => {
+ return {
+ content: chunk.Content,
+ matchRanges: chunk.Ranges.map((range) => ({
+ start: {
+ byteOffset: range.Start.ByteOffset,
+ column: range.Start.Column,
+ lineNumber: range.Start.LineNumber,
+ },
+ end: {
+ byteOffset: range.End.ByteOffset,
+ column: range.End.Column,
+ lineNumber: range.End.LineNumber,
+ }
+ }) satisfies SearchResultRange),
+ contentStart: {
+ byteOffset: chunk.ContentStart.ByteOffset,
+ column: chunk.ContentStart.Column,
+ lineNumber: chunk.ContentStart.LineNumber,
+ },
+ symbols: chunk.SymbolInfo?.map((symbol) => {
+ return {
+ symbol: symbol.Sym,
+ kind: symbol.Kind,
+ parent: symbol.Parent.length > 0 ? {
+ symbol: symbol.Parent,
+ kind: symbol.ParentKind,
+ } : undefined,
+ }
+ }) ?? undefined,
+ }
+ }),
+ branches: file.Branches,
+ content: file.Content,
+ }
+ }
+ ) ?? [],
+ repoUrlTemplates: Result.RepoURLs,
+ isBranchFilteringEnabled: isBranchFilteringEnabled,
+ } satisfies SearchResponse));
+
+ return parser.parse(searchBody);
+}
diff --git a/packages/web/src/features/search/types.ts b/packages/web/src/features/search/types.ts
new file mode 100644
index 00000000..1f652ee4
--- /dev/null
+++ b/packages/web/src/features/search/types.ts
@@ -0,0 +1,25 @@
+import {
+ fileSourceResponseSchema,
+ listRepositoriesResponseSchema,
+ locationSchema,
+ searchRequestSchema,
+ searchResponseSchema,
+ rangeSchema,
+ fileSourceRequestSchema,
+ symbolSchema,
+} from "./schemas";
+import { z } from "zod";
+
+export type SearchRequest = z.infer;
+export type SearchResponse = z.infer;
+export type SearchResultRange = z.infer;
+export type SearchResultLocation = z.infer;
+export type SearchResultFile = SearchResponse["files"][number];
+export type SearchResultChunk = SearchResultFile["chunks"][number];
+export type SearchSymbol = z.infer;
+
+export type ListRepositoriesResponse = z.infer;
+export type Repository = ListRepositoriesResponse["repos"][number];
+
+export type FileSourceRequest = z.infer;
+export type FileSourceResponse = z.infer;
\ No newline at end of file
diff --git a/packages/web/src/lib/server/zoektClient.ts b/packages/web/src/features/search/zoektClient.ts
similarity index 100%
rename from packages/web/src/lib/server/zoektClient.ts
rename to packages/web/src/features/search/zoektClient.ts
diff --git a/packages/web/src/features/search/zoektSchema.ts b/packages/web/src/features/search/zoektSchema.ts
new file mode 100644
index 00000000..d4091fb8
--- /dev/null
+++ b/packages/web/src/features/search/zoektSchema.ts
@@ -0,0 +1,132 @@
+
+import { z } from "zod";
+
+// @see : https://github.com/sourcebot-dev/zoekt/blob/main/api.go#L212
+export const zoektLocationSchema = z.object({
+ // 0-based byte offset from the beginning of the file
+ ByteOffset: z.number(),
+ // 1-based line number from the beginning of the file
+ LineNumber: z.number(),
+ // 1-based column number (in runes) from the beginning of line
+ Column: z.number(),
+});
+
+export const zoektRangeSchema = z.object({
+ Start: zoektLocationSchema,
+ End: zoektLocationSchema,
+});
+
+// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L350
+export const zoektSearchResponseStats = {
+ ContentBytesLoaded: z.number(),
+ IndexBytesLoaded: z.number(),
+ Crashes: z.number(),
+ Duration: z.number(),
+ FileCount: z.number(),
+ ShardFilesConsidered: z.number(),
+ FilesConsidered: z.number(),
+ FilesLoaded: z.number(),
+ FilesSkipped: z.number(),
+ ShardsScanned: z.number(),
+ ShardsSkipped: z.number(),
+ ShardsSkippedFilter: z.number(),
+ MatchCount: z.number(),
+ NgramMatches: z.number(),
+ NgramLookups: z.number(),
+ Wait: z.number(),
+ MatchTreeConstruction: z.number(),
+ MatchTreeSearch: z.number(),
+ RegexpsConsidered: z.number(),
+ FlushReason: z.number(),
+}
+
+export const zoektSymbolSchema = z.object({
+ Sym: z.string(),
+ Kind: z.string(),
+ Parent: z.string(),
+ ParentKind: z.string(),
+});
+
+// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L497
+export const zoektSearchResponseSchema = z.object({
+ Result: z.object({
+ ...zoektSearchResponseStats,
+ Files: z.array(z.object({
+ FileName: z.string(),
+ Repository: z.string(),
+ Version: z.string().optional(),
+ Language: z.string(),
+ Branches: z.array(z.string()).optional(),
+ ChunkMatches: z.array(z.object({
+ Content: z.string(),
+ Ranges: z.array(zoektRangeSchema),
+ FileName: z.boolean(),
+ ContentStart: zoektLocationSchema,
+ Score: z.number(),
+ SymbolInfo: z.array(zoektSymbolSchema).nullable(),
+ })),
+ Checksum: z.string(),
+ Score: z.number(),
+ // Set if `whole` is true.
+ Content: z.string().optional(),
+ })).nullable(),
+ RepoURLs: z.record(z.string(), z.string()),
+ }),
+});
+
+// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728
+const zoektRepoStatsSchema = z.object({
+ Repos: z.number(),
+ Shards: z.number(),
+ Documents: z.number(),
+ IndexBytes: z.number(),
+ ContentBytes: z.number(),
+ NewLinesCount: z.number(),
+ DefaultBranchNewLinesCount: z.number(),
+ OtherBranchesNewLinesCount: z.number(),
+});
+
+// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L716
+const zoektIndexMetadataSchema = z.object({
+ IndexFormatVersion: z.number(),
+ IndexFeatureVersion: z.number(),
+ IndexMinReaderVersion: z.number(),
+ IndexTime: z.string(),
+ PlainASCII: z.boolean(),
+ LanguageMap: z.record(z.string(), z.number()),
+ ZoektVersion: z.string(),
+ ID: z.string(),
+});
+
+
+// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L555
+export const zoektRepositorySchema = z.object({
+ Name: z.string(),
+ URL: z.string(),
+ Source: z.string(),
+ Branches: z.array(z.object({
+ Name: z.string(),
+ Version: z.string(),
+ })).nullable(),
+ CommitURLTemplate: z.string(),
+ FileURLTemplate: z.string(),
+ LineFragmentTemplate: z.string(),
+ RawConfig: z.record(z.string(), z.string()).nullable(),
+ Rank: z.number(),
+ IndexOptions: z.string(),
+ HasSymbols: z.boolean(),
+ Tombstone: z.boolean(),
+ LatestCommitDate: z.string(),
+ FileTombstones: z.string().optional(),
+});
+
+export const zoektListRepositoriesResponseSchema = z.object({
+ List: z.object({
+ Repos: z.array(z.object({
+ Repository: zoektRepositorySchema,
+ IndexMetadata: zoektIndexMetadataSchema,
+ Stats: zoektRepoStatsSchema,
+ })),
+ Stats: zoektRepoStatsSchema,
+ })
+});
\ No newline at end of file
diff --git a/packages/web/src/lib/extensions/searchResultHighlightExtension.ts b/packages/web/src/lib/extensions/searchResultHighlightExtension.ts
index de2844f8..56325af3 100644
--- a/packages/web/src/lib/extensions/searchResultHighlightExtension.ts
+++ b/packages/web/src/lib/extensions/searchResultHighlightExtension.ts
@@ -1,6 +1,6 @@
import { EditorSelection, Extension, StateEffect, StateField, Text, Transaction } from "@codemirror/state";
import { Decoration, DecorationSet, EditorView } from "@codemirror/view";
-import { SearchResultRange } from "../types";
+import { SearchResultRange } from "@/features/search/types";
const setMatchState = StateEffect.define<{
selectedMatchIndex: number,
@@ -8,9 +8,9 @@ const setMatchState = StateEffect.define<{
}>();
const convertToCodeMirrorRange = (range: SearchResultRange, document: Text) => {
- const { Start, End } = range;
- const from = document.line(Start.LineNumber).from + Start.Column - 1;
- const to = document.line(End.LineNumber).from + End.Column - 1;
+ const { start, end } = range;
+ const from = document.line(start.lineNumber).from + start.column - 1;
+ const to = document.line(end.lineNumber).from + end.column - 1;
return { from, to };
}
@@ -28,7 +28,7 @@ const matchHighlighter = StateField.define({
const decorations = ranges
.sort((a, b) => {
- return a.Start.ByteOffset - b.Start.ByteOffset;
+ return a.start.byteOffset - b.start.byteOffset;
})
.map((range, index) => {
const { from, to } = convertToCodeMirrorRange(range, transaction.newDoc);
diff --git a/packages/web/src/lib/schemas.ts b/packages/web/src/lib/schemas.ts
index a0fc7975..09bdae70 100644
--- a/packages/web/src/lib/schemas.ts
+++ b/packages/web/src/lib/schemas.ts
@@ -3,103 +3,6 @@ import { RepoIndexingStatus } from "@sourcebot/db";
import { z } from "zod";
import { isServiceError } from "./utils";
-export const searchRequestSchema = z.object({
- query: z.string(),
- maxMatchDisplayCount: z.number(),
- whole: z.boolean().optional(),
-});
-
-
-// @see : https://github.com/sourcebot-dev/zoekt/blob/main/api.go#L212
-export const locationSchema = z.object({
- // 0-based byte offset from the beginning of the file
- ByteOffset: z.number(),
- // 1-based line number from the beginning of the file
- LineNumber: z.number(),
- // 1-based column number (in runes) from the beginning of line
- Column: z.number(),
-});
-
-export const rangeSchema = z.object({
- Start: locationSchema,
- End: locationSchema,
-});
-
-// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L350
-export const searchResponseStats = {
- ContentBytesLoaded: z.number(),
- IndexBytesLoaded: z.number(),
- Crashes: z.number(),
- Duration: z.number(),
- FileCount: z.number(),
- ShardFilesConsidered: z.number(),
- FilesConsidered: z.number(),
- FilesLoaded: z.number(),
- FilesSkipped: z.number(),
- ShardsScanned: z.number(),
- ShardsSkipped: z.number(),
- ShardsSkippedFilter: z.number(),
- MatchCount: z.number(),
- NgramMatches: z.number(),
- NgramLookups: z.number(),
- Wait: z.number(),
- MatchTreeConstruction: z.number(),
- MatchTreeSearch: z.number(),
- RegexpsConsidered: z.number(),
- FlushReason: z.number(),
-}
-
-export const symbolSchema = z.object({
- Sym: z.string(),
- Kind: z.string(),
- Parent: z.string(),
- ParentKind: z.string(),
-});
-
-// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L497
-export const zoektSearchResponseSchema = z.object({
- Result: z.object({
- ...searchResponseStats,
- Files: z.array(z.object({
- FileName: z.string(),
- Repository: z.string(),
- Version: z.string().optional(),
- Language: z.string(),
- Branches: z.array(z.string()).optional(),
- ChunkMatches: z.array(z.object({
- Content: z.string(),
- Ranges: z.array(rangeSchema),
- FileName: z.boolean(),
- ContentStart: locationSchema,
- Score: z.number(),
- SymbolInfo: z.array(symbolSchema).nullable(),
- })),
- Checksum: z.string(),
- Score: z.number(),
- // Set if `whole` is true.
- Content: z.string().optional(),
- })).nullable(),
- RepoURLs: z.record(z.string(), z.string()),
- }),
-});
-
-export const searchResponseSchema = z.object({
- ...zoektSearchResponseSchema.shape,
- // Flag when a branch filter was used (e.g., `branch:`, `revision:`, etc.).
- isBranchFilteringEnabled: z.boolean(),
-});
-
-export const fileSourceRequestSchema = z.object({
- fileName: z.string(),
- repository: z.string(),
- branch: z.string().optional(),
-});
-
-export const fileSourceResponseSchema = z.object({
- source: z.string(),
- language: z.string(),
-});
-
export const secretCreateRequestSchema = z.object({
key: z.string(),
value: z.string(),
@@ -109,62 +12,6 @@ export const secreteDeleteRequestSchema = z.object({
key: z.string(),
});
-
-// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L728
-const repoStatsSchema = z.object({
- Repos: z.number(),
- Shards: z.number(),
- Documents: z.number(),
- IndexBytes: z.number(),
- ContentBytes: z.number(),
- NewLinesCount: z.number(),
- DefaultBranchNewLinesCount: z.number(),
- OtherBranchesNewLinesCount: z.number(),
-});
-
-// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L716
-const indexMetadataSchema = z.object({
- IndexFormatVersion: z.number(),
- IndexFeatureVersion: z.number(),
- IndexMinReaderVersion: z.number(),
- IndexTime: z.string(),
- PlainASCII: z.boolean(),
- LanguageMap: z.record(z.string(), z.number()),
- ZoektVersion: z.string(),
- ID: z.string(),
-});
-
-// @see : https://github.com/sourcebot-dev/zoekt/blob/3780e68cdb537d5a7ed2c84d9b3784f80c7c5d04/api.go#L555
-export const repositorySchema = z.object({
- Name: z.string(),
- URL: z.string(),
- Source: z.string(),
- Branches: z.array(z.object({
- Name: z.string(),
- Version: z.string(),
- })).nullable(),
- CommitURLTemplate: z.string(),
- FileURLTemplate: z.string(),
- LineFragmentTemplate: z.string(),
- RawConfig: z.record(z.string(), z.string()).nullable(),
- Rank: z.number(),
- IndexOptions: z.string(),
- HasSymbols: z.boolean(),
- Tombstone: z.boolean(),
- LatestCommitDate: z.string(),
- FileTombstones: z.string().optional(),
-});
-
-export const listRepositoriesResponseSchema = z.object({
- List: z.object({
- Repos: z.array(z.object({
- Repository: repositorySchema,
- IndexMetadata: indexMetadataSchema,
- Stats: repoStatsSchema,
- })),
- Stats: repoStatsSchema,
- })
-});
export const repositoryQuerySchema = z.object({
codeHostType: z.string(),
repoId: z.number(),
diff --git a/packages/web/src/lib/server/searchService.ts b/packages/web/src/lib/server/searchService.ts
deleted file mode 100644
index 6b978087..00000000
--- a/packages/web/src/lib/server/searchService.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import escapeStringRegexp from "escape-string-regexp";
-import { env } from "@/env.mjs";
-import { listRepositoriesResponseSchema, zoektSearchResponseSchema } from "../schemas";
-import { FileSourceRequest, FileSourceResponse, ListRepositoriesResponse, SearchRequest, SearchResponse } from "../types";
-import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } from "../serviceError";
-import { isServiceError } from "../utils";
-import { zoektFetch } from "./zoektClient";
-import { prisma } from "@/prisma";
-import { ErrorCode } from "../errorCodes";
-import { StatusCodes } from "http-status-codes";
-
-// List of supported query prefixes in zoekt.
-// @see : https://github.com/sourcebot-dev/zoekt/blob/main/query/parse.go#L417
-enum zoektPrefixes {
- archived = "archived:",
- branchShort = "b:",
- branch = "branch:",
- caseShort = "c:",
- case = "case:",
- content = "content:",
- fileShort = "f:",
- file = "file:",
- fork = "fork:",
- public = "public:",
- repoShort = "r:",
- repo = "repo:",
- regex = "regex:",
- lang = "lang:",
- sym = "sym:",
- typeShort = "t:",
- type = "type:",
- reposet = "reposet:",
-}
-
-const transformZoektQuery = async (query: string, orgId: number): Promise => {
- const prevQueryParts = query.split(" ");
- const newQueryParts = [];
-
- for (const part of prevQueryParts) {
-
- // Handle mapping `rev:` and `revision:` to `branch:`
- if (part.match(/^-?(rev|revision):.+$/)) {
- const isNegated = part.startsWith("-");
- let revisionName = part.slice(part.indexOf(":") + 1);
-
- // Special case: `*` -> search all revisions.
- // In zoekt, providing a blank string will match all branches.
- // @see: https://github.com/sourcebot-dev/zoekt/blob/main/eval.go#L560-L562
- if (revisionName === "*") {
- revisionName = "";
- }
- newQueryParts.push(`${isNegated ? "-" : ""}${zoektPrefixes.branch}${revisionName}`);
- }
-
- // Expand `context:` into `reposet:` atom.
- else if (part.match(/^-?context:.+$/)) {
- const isNegated = part.startsWith("-");
- const contextName = part.slice(part.indexOf(":") + 1);
-
- const context = await prisma.searchContext.findUnique({
- where: {
- name_orgId: {
- name: contextName,
- orgId,
- }
- },
- include: {
- repos: true,
- }
- });
-
- // If the context doesn't exist, return an error.
- if (!context) {
- return {
- errorCode: ErrorCode.SEARCH_CONTEXT_NOT_FOUND,
- message: `Search context "${contextName}" not found`,
- statusCode: StatusCodes.NOT_FOUND,
- } satisfies ServiceError;
- }
-
- const names = context.repos.map((repo) => repo.name);
- newQueryParts.push(`${isNegated ? "-" : ""}${zoektPrefixes.reposet}${names.join(",")}`);
- }
-
- // no-op: add the original part to the new query parts.
- else {
- newQueryParts.push(part);
- }
- }
-
- return newQueryParts.join(" ");
-}
-
-export const search = async ({ query, maxMatchDisplayCount, whole }: SearchRequest, orgId: number): Promise => {
- const transformedQuery = await transformZoektQuery(query, orgId);
- if (isServiceError(transformedQuery)) {
- return transformedQuery;
- }
- query = transformedQuery;
-
- const isBranchFilteringEnabled = (
- query.includes(zoektPrefixes.branch) ||
- query.includes(zoektPrefixes.branchShort)
- );
-
- // We only want to show matches for the default branch when
- // the user isn't explicitly filtering by branch.
- if (!isBranchFilteringEnabled) {
- query = query.concat(` branch:HEAD`);
- }
-
- const body = JSON.stringify({
- q: query,
- // @see: https://github.com/sourcebot-dev/zoekt/blob/main/api.go#L892
- opts: {
- NumContextLines: 2,
- ChunkMatches: true,
- MaxMatchDisplayCount: maxMatchDisplayCount,
- Whole: !!whole,
- ShardMaxMatchCount: env.SHARD_MAX_MATCH_COUNT,
- TotalMaxMatchCount: env.TOTAL_MAX_MATCH_COUNT,
- MaxWallTime: env.ZOEKT_MAX_WALL_TIME_MS * 1000 * 1000, // zoekt expects a duration in nanoseconds
- }
- });
-
- let header: Record = {};
- header = {
- "X-Tenant-ID": orgId.toString()
- };
-
- const searchResponse = await zoektFetch({
- path: "/api/search",
- body,
- header,
- method: "POST",
- });
-
- if (!searchResponse.ok) {
- return invalidZoektResponse(searchResponse);
- }
-
- const searchBody = await searchResponse.json();
- const parsedSearchResponse = zoektSearchResponseSchema.safeParse(searchBody);
- if (!parsedSearchResponse.success) {
- console.error(`Failed to parse zoekt response. Error: ${parsedSearchResponse.error}`);
- return unexpectedError(`Something went wrong while parsing the response from zoekt`);
- }
-
- return {
- ...parsedSearchResponse.data,
- isBranchFilteringEnabled,
- }
-}
-
-// @todo (bkellam) : We should really be using `git show :` to fetch file contents here.
-// This will allow us to support permalinks to files at a specific revision that may not be indexed
-// by zoekt.
-export const getFileSource = async ({ fileName, repository, branch }: FileSourceRequest, orgId: number): Promise => {
- const escapedFileName = escapeStringRegexp(fileName);
- const escapedRepository = escapeStringRegexp(repository);
-
- let query = `file:${escapedFileName} repo:^${escapedRepository}$`;
- if (branch) {
- query = query.concat(` branch:${branch}`);
- }
-
- const searchResponse = await search({
- query,
- maxMatchDisplayCount: 1,
- whole: true,
- }, orgId);
-
- if (isServiceError(searchResponse)) {
- return searchResponse;
- }
-
- const files = searchResponse.Result.Files;
-
- if (!files || files.length === 0) {
- return fileNotFound(fileName, repository);
- }
-
- const file = files[0];
- const source = file.Content ?? '';
- const language = file.Language;
- return {
- source,
- language,
- }
-}
-
-export const listRepositories = async (orgId: number): Promise => {
- const body = JSON.stringify({
- opts: {
- Field: 0,
- }
- });
-
- let header: Record = {};
- header = {
- "X-Tenant-ID": orgId.toString()
- };
-
- const listResponse = await zoektFetch({
- path: "/api/list",
- body,
- header,
- method: "POST",
- cache: "no-store",
- });
-
- if (!listResponse.ok) {
- return invalidZoektResponse(listResponse);
- }
-
- const listBody = await listResponse.json();
- const parsedListResponse = listRepositoriesResponseSchema.safeParse(listBody);
- if (!parsedListResponse.success) {
- console.error(`Failed to parse zoekt response. Error: ${parsedListResponse.error}`);
- return unexpectedError(`Something went wrong while parsing the response from zoekt`);
- }
-
- return parsedListResponse.data;
-}
\ No newline at end of file
diff --git a/packages/web/src/lib/types.ts b/packages/web/src/lib/types.ts
index b4720577..c1176e87 100644
--- a/packages/web/src/lib/types.ts
+++ b/packages/web/src/lib/types.ts
@@ -1,31 +1,15 @@
import { z } from "zod";
-import { fileSourceRequestSchema, fileSourceResponseSchema, listRepositoriesResponseSchema, locationSchema, rangeSchema, repositorySchema, repositoryQuerySchema, searchRequestSchema, searchResponseSchema, symbolSchema, getVersionResponseSchema } from "./schemas";
+import { getVersionResponseSchema, repositoryQuerySchema } from "./schemas";
import { tenancyModeSchema } from "@/env.mjs";
export type KeymapType = "default" | "vim";
-export type SearchRequest = z.infer;
-export type SearchResponse = z.infer;
-
-export type SearchResult = SearchResponse["Result"];
-export type SearchResultFile = NonNullable[number];
-export type SearchResultFileMatch = SearchResultFile["ChunkMatches"][number];
-export type SearchResultRange = z.infer;
-export type SearchResultLocation = z.infer;
-
-export type FileSourceRequest = z.infer;
-export type FileSourceResponse = z.infer;
-
-export type ListRepositoriesResponse = z.infer;
-export type Repository = z.infer;
-export type RepositoryQuery = z.infer;
-export type Symbol = z.infer;
-
export type GetVersionResponse = z.infer;
export enum SearchQueryParams {
query = "query",
- maxMatchDisplayCount = "maxMatchDisplayCount",
+ matches = "matches",
}
-export type TenancyMode = z.infer;
\ No newline at end of file
+export type TenancyMode = z.infer;
+export type RepositoryQuery = z.infer;
\ No newline at end of file
diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts
index 5d1d02a4..3d211ccb 100644
--- a/packages/web/src/lib/utils.ts
+++ b/packages/web/src/lib/utils.ts
@@ -6,7 +6,8 @@ import giteaLogo from "@/public/gitea.svg";
import gerritLogo from "@/public/gerrit.svg";
import bitbucketLogo from "@/public/bitbucket.svg";
import { ServiceError } from "./serviceError";
-import { Repository, RepositoryQuery } from "./types";
+import { RepositoryQuery } from "./types";
+import { Repository } from "@/features/search/types";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -48,15 +49,15 @@ export const getRepoCodeHostInfo = (repo?: Repository): CodeHostInfo | undefined
return undefined;
}
- if (!repo.RawConfig) {
+ if (!repo.rawConfig) {
return undefined;
}
// @todo : use zod to validate config schema
- const webUrlType = repo.RawConfig['web-url-type']!;
- const displayName = repo.RawConfig['display-name'] ?? repo.RawConfig['name']!;
+ const webUrlType = repo.rawConfig['web-url-type']!;
+ const displayName = repo.rawConfig['display-name'] ?? repo.rawConfig['name']!;
- return _getCodeHostInfoInternal(webUrlType, displayName, repo.URL);
+ return _getCodeHostInfoInternal(webUrlType, displayName, repo.url);
}
export const getRepoQueryCodeHostInfo = (repo: RepositoryQuery): CodeHostInfo | undefined => {
diff --git a/yarn.lock b/yarn.lock
index 6824f5fd..af3c85c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5343,6 +5343,7 @@ __metadata:
codemirror-lang-sparql: "npm:^2.0.0"
codemirror-lang-spreadsheet: "npm:^1.3.0"
codemirror-lang-zig: "npm:^0.1.0"
+ cross-env: "npm:^7.0.3"
embla-carousel-auto-scroll: "npm:^8.3.0"
embla-carousel-react: "npm:^8.3.0"
escape-string-regexp: "npm:^5.0.0"