diff --git a/package.json b/package.json index c53fc872..a5013018 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@uiw/react-codemirror": "^4.23.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "http-status-codes": "^2.3.0", "lucide-react": "^0.435.0", "next": "14.2.6", "react": "^18", diff --git a/src/app/zoekt/search/route.ts b/src/app/api/search/route.ts similarity index 94% rename from src/app/zoekt/search/route.ts rename to src/app/api/search/route.ts index 3c5cfa11..a968a5a1 100644 --- a/src/app/zoekt/search/route.ts +++ b/src/app/api/search/route.ts @@ -2,6 +2,7 @@ import { createPathWithQueryParams } from '@/lib/utils'; import { type NextRequest } from 'next/server' export async function GET(request: NextRequest) { + // @todo: proper error handling const searchParams = request.nextUrl.searchParams; const query = searchParams.get('query'); const numResults = searchParams.get('numResults'); diff --git a/src/app/api/source/route.ts b/src/app/api/source/route.ts new file mode 100644 index 00000000..3f948a9b --- /dev/null +++ b/src/app/api/source/route.ts @@ -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=&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; +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 0b61a975..7603891d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,13 +5,13 @@ import { Separator } from "@/components/ui/separator"; import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam"; import { defaultKeymap } from "@codemirror/commands"; 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 { 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 { useRouter } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { useDebouncedCallback } from 'use-debounce'; import logo from "../../public/sb_logo_large_3.png"; @@ -20,6 +20,8 @@ import { ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; +import { GetSourceResponse, pathQueryParamName, repoQueryParamName } from "@/lib/api"; +import { createPathWithQueryParams } from "@/lib/utils"; interface ZoekMatch { URL: string, @@ -61,7 +63,8 @@ export default function Home() { const [query, setQuery] = useState(defaultQuery); 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([]); const [isLoading, setIsLoading] = useState(false); @@ -111,8 +114,21 @@ export default function Home() { { - console.log(filename); + onOpenFile={() => { + 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} > )} @@ -143,14 +159,17 @@ const CodeEditor = ({ code, }: CodeEditorProps) => { return ( - + + + + ) } @@ -180,7 +199,7 @@ const SearchBar = ({ console.log('making query...'); 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(({ data }: { data: ZoekSearchResponse }) => { onSearchResult(data.result); @@ -213,7 +232,7 @@ const SearchBar = ({ interface FileMatchProps { match: ZoekFileMatch; - onOpenFile: (filename: string) => void; + onOpenFile: () => void; } const FileMatch = ({ @@ -234,7 +253,7 @@ const FileMatch = ({ key={index} className="font-mono px-4 py-0.5 text-sm cursor-pointer" onClick={() =>{ - onOpenFile(match.FileName); + onOpenFile(); }} >

{match.LineNum}: {fragment.Pre}{fragment.Match}{fragment.Post}

diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 00000000..a2855732 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,8 @@ + + +export const pathQueryParamName = "path"; +export const repoQueryParamName = "repo"; + +export type GetSourceResponse = { + content: string; +} diff --git a/src/lib/errorCodes.ts b/src/lib/errorCodes.ts new file mode 100644 index 00000000..d5f7b2e6 --- /dev/null +++ b/src/lib/errorCodes.ts @@ -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', +} diff --git a/src/lib/serviceError.ts b/src/lib/serviceError.ts new file mode 100644 index 00000000..5b926da6 --- /dev/null +++ b/src/lib/serviceError.ts @@ -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}`, + }); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1ccd4306..c927e1b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1642,6 +1642,11 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: 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: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"