mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Display selected file in code preview
This commit is contained in:
parent
e828d53910
commit
3fc3397f01
8 changed files with 155 additions and 18 deletions
|
|
@ -20,6 +20,7 @@
|
||||||
"@uiw/react-codemirror": "^4.23.0",
|
"@uiw/react-codemirror": "^4.23.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"http-status-codes": "^2.3.0",
|
||||||
"lucide-react": "^0.435.0",
|
"lucide-react": "^0.435.0",
|
||||||
"next": "14.2.6",
|
"next": "14.2.6",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { createPathWithQueryParams } from '@/lib/utils';
|
||||||
import { type NextRequest } from 'next/server'
|
import { type NextRequest } from 'next/server'
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
// @todo: proper error handling
|
||||||
const searchParams = request.nextUrl.searchParams;
|
const searchParams = request.nextUrl.searchParams;
|
||||||
const query = searchParams.get('query');
|
const query = searchParams.get('query');
|
||||||
const numResults = searchParams.get('numResults');
|
const numResults = searchParams.get('numResults');
|
||||||
71
src/app/api/source/route.ts
Normal file
71
src/app/api/source/route.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { ErrorCode } from "@/lib/errorCodes";
|
||||||
|
import { serviceError, missingQueryParam } from "@/lib/serviceError";
|
||||||
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import { GetSourceResponse, pathQueryParamName, repoQueryParamName } from "@/lib/api";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content of a source file at the given path.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* GET /api/source?path=<filepath>&repo=<repo>
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const filepath = searchParams.get(pathQueryParamName);
|
||||||
|
const repo = searchParams.get(repoQueryParamName);
|
||||||
|
|
||||||
|
if (!filepath) {
|
||||||
|
return missingQueryParam(pathQueryParamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!repo) {
|
||||||
|
return missingQueryParam(repoQueryParamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the contents of the path
|
||||||
|
const repoPath = getRepoPath(repo);
|
||||||
|
if (!repoPath) {
|
||||||
|
return serviceError({
|
||||||
|
statusCode: StatusCodes.NOT_FOUND,
|
||||||
|
errorCode: ErrorCode.REPOSITORY_NOT_FOUND,
|
||||||
|
message: `Could not find repository '${repo}'.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(repoPath, filepath);
|
||||||
|
if (!fs.existsSync(fullPath)) {
|
||||||
|
return serviceError({
|
||||||
|
statusCode: StatusCodes.NOT_FOUND,
|
||||||
|
errorCode: ErrorCode.FILE_NOT_FOUND,
|
||||||
|
message: `Could not find file '${filepath}' in repository '${repo}'.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo : some error handling here would be nice
|
||||||
|
const content = fs.readFileSync(fullPath, "utf8");
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
content,
|
||||||
|
} satisfies GetSourceResponse,
|
||||||
|
{
|
||||||
|
status: StatusCodes.OK
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo : we will need to figure out a more sophisticated system for this..
|
||||||
|
const getRepoPath = (repo: string): string | undefined => {
|
||||||
|
switch (repo) {
|
||||||
|
case "monorepo":
|
||||||
|
return "/Users/brendan/monorepo"
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
@ -5,13 +5,13 @@ import { Separator } from "@/components/ui/separator";
|
||||||
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||||
import { defaultKeymap } from "@codemirror/commands";
|
import { defaultKeymap } from "@codemirror/commands";
|
||||||
import { javascript } from "@codemirror/lang-javascript";
|
import { javascript } from "@codemirror/lang-javascript";
|
||||||
import { EditorView, keymap, ViewUpdate } from "@codemirror/view";
|
import { keymap } from "@codemirror/view";
|
||||||
import { SymbolIcon } from "@radix-ui/react-icons";
|
import { SymbolIcon } from "@radix-ui/react-icons";
|
||||||
import { ScrollArea, Scrollbar } from "@radix-ui/react-scroll-area";
|
import { ScrollArea, Scrollbar } from "@radix-ui/react-scroll-area";
|
||||||
import CodeMirror, { StateField } from '@uiw/react-codemirror';
|
import CodeMirror from '@uiw/react-codemirror';
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import logo from "../../public/sb_logo_large_3.png";
|
import logo from "../../public/sb_logo_large_3.png";
|
||||||
|
|
||||||
|
|
@ -20,6 +20,8 @@ import {
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable";
|
} from "@/components/ui/resizable";
|
||||||
|
import { GetSourceResponse, pathQueryParamName, repoQueryParamName } from "@/lib/api";
|
||||||
|
import { createPathWithQueryParams } from "@/lib/utils";
|
||||||
|
|
||||||
interface ZoekMatch {
|
interface ZoekMatch {
|
||||||
URL: string,
|
URL: string,
|
||||||
|
|
@ -61,7 +63,8 @@ export default function Home() {
|
||||||
const [query, setQuery] = useState(defaultQuery);
|
const [query, setQuery] = useState(defaultQuery);
|
||||||
const [numResults, _setNumResults] = useState(defaultNumResults && !isNaN(Number(defaultNumResults)) ? Number(defaultNumResults) : 100);
|
const [numResults, _setNumResults] = useState(defaultNumResults && !isNaN(Number(defaultNumResults)) ? Number(defaultNumResults) : 100);
|
||||||
|
|
||||||
const [isCodePanelOpen, _setIsCodePanelOpen] = useState(true);
|
const [isCodePanelOpen, setIsCodePanelOpen] = useState(false);
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
|
||||||
const [fileMatches, setFileMatches] = useState<ZoekFileMatch[]>([]);
|
const [fileMatches, setFileMatches] = useState<ZoekFileMatch[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
@ -111,8 +114,21 @@ export default function Home() {
|
||||||
<FileMatch
|
<FileMatch
|
||||||
key={index}
|
key={index}
|
||||||
match={match}
|
match={match}
|
||||||
onOpenFile={(filename) => {
|
onOpenFile={() => {
|
||||||
console.log(filename);
|
const url = createPathWithQueryParams(
|
||||||
|
`http://localhost:3000/api/source`,
|
||||||
|
[pathQueryParamName, match.FileName],
|
||||||
|
[repoQueryParamName, match.Repo]
|
||||||
|
);
|
||||||
|
|
||||||
|
// @todo : this query should definitely be cached s.t., when the user is switching between files,
|
||||||
|
// we aren't re-fetching the same file.
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then((body: GetSourceResponse) => {
|
||||||
|
setIsCodePanelOpen(true);
|
||||||
|
setCode(body.content);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -126,7 +142,7 @@ export default function Home() {
|
||||||
minSize={20}
|
minSize={20}
|
||||||
>
|
>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code="hello"
|
code={code}
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
)}
|
)}
|
||||||
|
|
@ -143,6 +159,7 @@ const CodeEditor = ({
|
||||||
code,
|
code,
|
||||||
}: CodeEditorProps) => {
|
}: CodeEditorProps) => {
|
||||||
return (
|
return (
|
||||||
|
<ScrollArea className="h-full overflow-y-auto">
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
editable={false}
|
editable={false}
|
||||||
value={code}
|
value={code}
|
||||||
|
|
@ -151,6 +168,8 @@ const CodeEditor = ({
|
||||||
javascript(),
|
javascript(),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<Scrollbar orientation="vertical" />
|
||||||
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +199,7 @@ const SearchBar = ({
|
||||||
console.log('making query...');
|
console.log('making query...');
|
||||||
|
|
||||||
onLoadingChange(true);
|
onLoadingChange(true);
|
||||||
fetch(`http://localhost:3000/zoekt/search?query=${query}&numResults=${numResults}`)
|
fetch(`http://localhost:3000/api/search?query=${query}&numResults=${numResults}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(({ data }: { data: ZoekSearchResponse }) => {
|
.then(({ data }: { data: ZoekSearchResponse }) => {
|
||||||
onSearchResult(data.result);
|
onSearchResult(data.result);
|
||||||
|
|
@ -213,7 +232,7 @@ const SearchBar = ({
|
||||||
|
|
||||||
interface FileMatchProps {
|
interface FileMatchProps {
|
||||||
match: ZoekFileMatch;
|
match: ZoekFileMatch;
|
||||||
onOpenFile: (filename: string) => void;
|
onOpenFile: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileMatch = ({
|
const FileMatch = ({
|
||||||
|
|
@ -234,7 +253,7 @@ const FileMatch = ({
|
||||||
key={index}
|
key={index}
|
||||||
className="font-mono px-4 py-0.5 text-sm cursor-pointer"
|
className="font-mono px-4 py-0.5 text-sm cursor-pointer"
|
||||||
onClick={() =>{
|
onClick={() =>{
|
||||||
onOpenFile(match.FileName);
|
onOpenFile();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p>{match.LineNum}: {fragment.Pre}<span className="font-bold">{fragment.Match}</span>{fragment.Post}</p>
|
<p>{match.LineNum}: {fragment.Pre}<span className="font-bold">{fragment.Match}</span>{fragment.Post}</p>
|
||||||
|
|
|
||||||
8
src/lib/api.ts
Normal file
8
src/lib/api.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
|
||||||
|
export const pathQueryParamName = "path";
|
||||||
|
export const repoQueryParamName = "repo";
|
||||||
|
|
||||||
|
export type GetSourceResponse = {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
6
src/lib/errorCodes.ts
Normal file
6
src/lib/errorCodes.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
export enum ErrorCode {
|
||||||
|
MISSING_REQUIRED_QUERY_PARAMETER = 'MISSING_REQUIRED_QUERY_PARAMETER',
|
||||||
|
REPOSITORY_NOT_FOUND = 'REPOSITORY_NOT_FOUND',
|
||||||
|
FILE_NOT_FOUND = 'FILE_NOT_FOUND',
|
||||||
|
}
|
||||||
26
src/lib/serviceError.ts
Normal file
26
src/lib/serviceError.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import { ErrorCode } from "./errorCodes";
|
||||||
|
|
||||||
|
export interface ServiceErrorArgs {
|
||||||
|
statusCode: StatusCodes;
|
||||||
|
errorCode: ErrorCode;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serviceError = ({ statusCode, errorCode, message }: ServiceErrorArgs) => {
|
||||||
|
return Response.json({
|
||||||
|
statusCode,
|
||||||
|
errorCode,
|
||||||
|
message,
|
||||||
|
}, {
|
||||||
|
status: statusCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const missingQueryParam = (name: string) => {
|
||||||
|
return serviceError({
|
||||||
|
statusCode: StatusCodes.BAD_REQUEST,
|
||||||
|
errorCode: ErrorCode.MISSING_REQUIRED_QUERY_PARAMETER,
|
||||||
|
message: `Missing required query parameter: ${name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1642,6 +1642,11 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.2"
|
function-bind "^1.1.2"
|
||||||
|
|
||||||
|
http-status-codes@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.3.0.tgz#987fefb28c69f92a43aecc77feec2866349a8bfc"
|
||||||
|
integrity sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==
|
||||||
|
|
||||||
ignore@^5.2.0:
|
ignore@^5.2.0:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue