Compare commits

...

38 Commits

Author SHA1 Message Date
ef3114e68b chore(docker): remove QEMU setup and platform specification 2025-03-06 10:27:51 +08:00
63fd9a4a45 chore(docker): change default USER to root in Dockerfile and remove user: root in Compose 2025-03-06 00:32:56 +08:00
1bdae1d458 chore(dockerignore): remove env* pattern from .dockerignore 2025-03-05 22:50:36 +08:00
2ac6457f01 chore(docker): update .dockerignore to exclude additional files and folders 2025-03-05 21:30:03 +08:00
0d29c56750 feat(language-server): add logging for URL creation in connectToLanguageServer 2025-03-05 19:21:55 +08:00
f4ce0e594d feat(dockerfile): support bun.lockb file 2025-03-05 16:23:10 +08:00
4e1a185036 docs(readme): improve setup instructions with pre-built Docker images 2025-03-05 16:19:02 +08:00
3e94bf07a6 chore(docker): update docker compose configuration to use pre-built images 2025-03-05 15:58:44 +08:00
7ade2f8e90 fix(docker): update matrix strategy for clearer context and file assignment 2025-03-05 15:44:26 +08:00
bb06fafe78 chore(workflow): refactor Docker build and push workflow with matrix strategy 2025-03-05 15:38:55 +08:00
c29e62b1e0 chore(workflow): add Checkout step to GitHub Actions workflow 2025-03-05 14:54:50 +08:00
922872e3a9 fix(actions): update context path from './' to '.' 2025-03-05 14:50:02 +08:00
3895a2e5b7 fix(actions): correct Dockerfile path parameter to 'file' in GitHub Actions workflow 2025-03-05 14:44:23 +08:00
e903df067b feat(workflow): add Docker build and push automation 2025-03-05 14:15:54 +08:00
5f79671aa5 refactor(code-editor): dynamically fetch lspConfig from LanguageServerConfig 2025-03-05 13:29:17 +08:00
9ef83d99b6 chore(images): replace demo image with new version 2025-03-05 10:30:27 +08:00
353aff9b4d refactor(header, reset-button, useCodeEditorStore): remove unnecessary value prop and refactor related components 2025-03-05 10:18:06 +08:00
3336a9f1d0 feat(config/problem): add TEMP_DEFAULT_EDITOR_VALUE 2025-03-05 09:36:38 +08:00
a33033b48d refactor(config/judge.ts, types/judge.ts): split judge configuration and types 2025-03-05 09:27:00 +08:00
808dd96a50 feat(language-selector): update language selector logic and integrate LSP config 2025-03-05 08:46:44 +08:00
d33f214450 refactor(button): refactor ResetButton to accept value prop 2025-03-05 08:34:54 +08:00
2a47b469b0 feat(language-server): add environment variable support for configuration 2025-03-05 08:33:38 +08:00
470fa306c2 feat(editor): add language icon support 2025-03-05 08:32:33 +08:00
ca82b94b0f refactor: replace old SUPPORTED_LANGUAGES config with new EditorLanguageConfig config 2025-03-05 08:30:58 +08:00
753422ebf4 refactor: remove unused editor and language config files 2025-03-05 08:28:52 +08:00
527c52abbc feat(editor): refactor code editor with LSP support and state management 2025-03-05 00:33:19 +08:00
0c94bb2fa3 refactor(store): restructure code editor store and update configuration 2025-03-05 00:31:57 +08:00
a920cbc4b8 refactor(utils): rename languageServerConfigs to LanguageServerConfig 2025-03-04 21:28:47 +08:00
398928d933 style(editor): format code style for consistency 2025-03-04 21:27:53 +08:00
d89a45daa8 feat(editor): update loading state with padding and refactor dynamic import 2025-03-04 21:26:51 +08:00
0c1ecbcff2 feat(editor): initialize CoreEditorLsp component 2025-03-04 21:05:23 +08:00
598ca75829 feat(editor): add default editor options configuration 2025-03-04 21:02:16 +08:00
af23dd3289 feat(editor): add language server connection via WebSocket 2025-03-04 20:58:06 +08:00
742a827072 feat(language-server): add configuration for C and C++ 2025-03-04 20:53:19 +08:00
1f417fb4e6 feat(shiki): improve Shiki highlighter initialization with dynamic theme and language support 2025-03-04 20:29:21 +08:00
6d9c6701d4 feat(monaco-theme): update Monaco theme configuration with label metadata 2025-03-04 20:29:07 +08:00
ec9f3b2475 feat(editor-language): update language configurations and types for C and C++ 2025-03-04 20:28:41 +08:00
d8fa37dd8a feat(theme): add MonacoTheme enum, configuration and hook 2025-03-04 20:06:33 +08:00
31 changed files with 581 additions and 402 deletions

View File

@ -1,6 +1,11 @@
.git .git
.github
.next .next
node_modules node_modules
.dockerignore .dockerignore
.gitignore .gitignore
Dockerfile **/*compose*
demo.png
**/*Dockerfile*
LICENSE
README.md

View File

@ -0,0 +1,49 @@
name: Docker Build and Push
on:
push:
branches:
- main
jobs:
docker-build-and-push:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- image: monaco-editor-lsp-next
context: .
file: Dockerfile
- image: lsp-c
context: ./docker/lsp/clangd
file: ./docker/lsp/clangd/Dockerfile
- image: lsp-cpp
context: ./docker/lsp/clangd
file: ./docker/lsp/clangd/Dockerfile
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v3
# with:
# platforms: amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push ${{ matrix.image }}
uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
push: true
tags: ${{ vars.DOCKERHUB_USERNAME }}/${{ matrix.image }}:latest
# platforms: linux/amd64

View File

@ -16,7 +16,7 @@ RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \ elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
elif [ -f bun.lock ]; then \ elif [ -f bun.lock ] || [ -f bun.lockb ]; then \
apk add --no-cache curl bash && \ apk add --no-cache curl bash && \
curl -fsSL https://bun.sh/install | bash && \ curl -fsSL https://bun.sh/install | bash && \
export BUN_INSTALL="$HOME/.bun" && \ export BUN_INSTALL="$HOME/.bun" && \
@ -25,7 +25,6 @@ RUN \
else echo "Lockfile not found." && exit 1; \ else echo "Lockfile not found." && exit 1; \
fi fi
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM base AS builder FROM base AS builder
WORKDIR /app WORKDIR /app
@ -41,7 +40,7 @@ RUN \
if [ -f yarn.lock ]; then yarn run build; \ if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \ elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
elif [ -f bun.lock ]; then \ elif [ -f bun.lock ] || [ -f bun.lockb ]; then \
apk add --no-cache curl bash && \ apk add --no-cache curl bash && \
curl -fsSL https://bun.sh/install | bash && \ curl -fsSL https://bun.sh/install | bash && \
export BUN_INSTALL="$HOME/.bun" && \ export BUN_INSTALL="$HOME/.bun" && \
@ -61,9 +60,6 @@ ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime. # Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size # Automatically leverage output traces to reduce image size
@ -71,7 +67,7 @@ COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs USER root
EXPOSE 3000 EXPOSE 3000

View File

@ -26,29 +26,46 @@ Complete these steps before launching the editor for seamless LSP integration!
### 🐳 Docker Deployment (Recommended) ### 🐳 Docker Deployment (Recommended)
Deploy the project quickly using pre-built Docker images:
```sh ```sh
# Clone repository # Clone the repository
git clone https://github.com/cfngc4594/monaco-editor-lsp-next git clone https://github.com/cfngc4594/monaco-editor-lsp-next
cd monaco-editor-lsp-next cd monaco-editor-lsp-next
# Build and launch containers # Start the application with pre-built images
docker compose up -d --build docker compose up -d
``` ```
### 🔨 Manual Installation ### 🔨 Local Manual Build
Build the images locally and start the containers:
```sh ```sh
# Clone repository # Clone the repository
git clone https://github.com/cfngc4594/monaco-editor-lsp-next git clone https://github.com/cfngc4594/monaco-editor-lsp-next
cd monaco-editor-lsp-next cd monaco-editor-lsp-next
# Start core LSP containers # Build and start containers using the local configuration
docker compose up -d --build lsp-c lsp-cpp docker compose -f compose.local.yml up -d --build
```
# Install dependencies (using Bun) ### 🛠 Development Mode
Set up a development environment with hot-reloading:
```sh
# Clone the repository
git clone https://github.com/cfngc4594/monaco-editor-lsp-next
cd monaco-editor-lsp-next
# Start only the LSP containers
docker compose up -d lsp-c lsp-cpp
# Or use the local configuration
# docker compose -f compose.local.yml up -d --build lsp-c lsp-cpp
# Install dependencies and start the development server
bun install bun install
# Launch development server
bun run dev bun run dev
``` ```
@ -97,10 +114,12 @@ bun run dev
Syntax highlighting depends on `rehype-pretty-code`'s deprecated `getHighlighter` API from `shiki@legacy`. Syntax highlighting depends on `rehype-pretty-code`'s deprecated `getHighlighter` API from `shiki@legacy`.
**Key Points**: **Key Points**:
- **Affected File:** `src/components/mdx-preview.tsx` - **Affected File:** `src/components/mdx-preview.tsx`
- **Dependency Chain:** `rehype-pretty-code``shiki@legacy` - **Dependency Chain:** `rehype-pretty-code``shiki@legacy`
- **Version Constraints**: - **Version Constraints**:
```bash
```sh
"shiki": "<=2.5.0" "shiki": "<=2.5.0"
"@shikijs/monaco": "<=2.5.0" "@shikijs/monaco": "<=2.5.0"
``` ```

62
compose.local.yml Normal file
View File

@ -0,0 +1,62 @@
services:
monaco-editor-lsp-next:
build:
context: ./
dockerfile: Dockerfile
image: monaco-editor-lsp-next:latest
container_name: monaco-editor-lsp-next
restart: always
ports:
- "3000:3000"
networks:
- monaco-editor-lsp-next
depends_on:
- lsp-c
- lsp-cpp
healthcheck:
test: [ "CMD-SHELL", "curl --fail http://localhost:3000 || exit 1" ]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
- /var/run/docker.sock:/var/run/docker.sock
lsp-c:
build:
context: ./docker/lsp/clangd
dockerfile: Dockerfile
image: lsp-c:latest
container_name: lsp-c
restart: always
ports:
- "4594:3000"
networks:
- monaco-editor-lsp-next
healthcheck:
test: [ "CMD-SHELL", "nc -zv localhost 3000" ]
interval: 10s
timeout: 5s
retries: 5
lsp-cpp:
build:
context: ./docker/lsp/clangd
dockerfile: Dockerfile
image: lsp-cpp:latest
container_name: lsp-cpp
restart: always
ports:
- "4595:3000"
networks:
- monaco-editor-lsp-next
healthcheck:
test: [ "CMD-SHELL", "nc -zv localhost 3000" ]
interval: 10s
timeout: 5s
retries: 5
networks:
monaco-editor-lsp-next:
name: monaco-editor-lsp-next
driver: bridge

View File

@ -1,9 +1,6 @@
services: services:
monaco-editor-lsp-next: monaco-editor-lsp-next:
build: image: cfngc4594/monaco-editor-lsp-next:latest
context: ./
dockerfile: Dockerfile
image: monaco-editor-lsp-next:latest
container_name: monaco-editor-lsp-next container_name: monaco-editor-lsp-next
restart: always restart: always
ports: ports:
@ -19,15 +16,11 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 10s start_period: 10s
user: root
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
lsp-c: lsp-c:
build: image: cfngc4594/lsp-c:latest
context: ./docker/lsp/clangd
dockerfile: Dockerfile
image: lsp-c:latest
container_name: lsp-c container_name: lsp-c
restart: always restart: always
ports: ports:
@ -41,10 +34,7 @@ services:
retries: 5 retries: 5
lsp-cpp: lsp-cpp:
build: image: cfngc4594/lsp-cpp:latest
context: ./docker/lsp/clangd
dockerfile: Dockerfile
image: lsp-cpp:latest
container_name: lsp-cpp container_name: lsp-cpp
restart: always restart: always
ports: ports:

BIN
demo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 243 KiB

View File

@ -1,10 +1,10 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import LanguageSelector from "./language-selector";
import FormatButton from "./format-button";
import CopyButton from "./copy-button"; import CopyButton from "./copy-button";
import RedoButton from "./redo-button"; import RedoButton from "./redo-button";
import UndoButton from "./undo-button"; import UndoButton from "./undo-button";
import ResetButton from "./reset-button"; import ResetButton from "./reset-button";
import FormatButton from "./format-button";
import LanguageSelector from "./language-selector";
interface WorkspaceEditorHeaderProps { interface WorkspaceEditorHeaderProps {
className?: string; className?: string;

View File

@ -7,28 +7,37 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { getPath } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { EditorLanguage } from "@/types/editor-language";
import LanguageServerConfig from "@/config/language-server";
import { EditorLanguageConfig } from "@/config/editor-language";
import { useCodeEditorStore } from "@/store/useCodeEditorStore"; import { useCodeEditorStore } from "@/store/useCodeEditorStore";
import { SUPPORTED_LANGUAGES } from "@/constants/language";
export default function LanguageSelector() { export default function LanguageSelector() {
const { hydrated, language, setLanguage } = useCodeEditorStore(); const { hydrated, language, setLanguage, setPath, setLspConfig } = useCodeEditorStore();
if (!hydrated) { if (!hydrated) {
return <Skeleton className="h-6 w-16 rounded-2xl" />; return <Skeleton className="h-6 w-16 rounded-2xl" />;
} }
const handleValueChange = (lang: EditorLanguage) => {
setLanguage(lang);
setPath(getPath(lang));
setLspConfig(LanguageServerConfig[lang]);
};
return ( return (
<Select value={language} onValueChange={setLanguage}> <Select value={language} onValueChange={handleValueChange}>
<SelectTrigger className="h-6 px-1.5 py-0.5 border-none focus:ring-0 hover:bg-muted [&>span]:flex [&>span]:items-center [&>span]:gap-2 [&>span_svg]:shrink-0 [&>span_svg]:text-muted-foreground/80"> <SelectTrigger className="h-6 px-1.5 py-0.5 border-none focus:ring-0 hover:bg-muted [&>span]:flex [&>span]:items-center [&>span]:gap-2 [&>span_svg]:shrink-0 [&>span_svg]:text-muted-foreground/80">
<SelectValue placeholder="Select language" /> <SelectValue placeholder="Select language" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="[&_*[role=option]>span>svg]:shrink-0 [&_*[role=option]>span>svg]:text-muted-foreground/80 [&_*[role=option]>span]:end-2 [&_*[role=option]>span]:start-auto [&_*[role=option]>span]:flex [&_*[role=option]>span]:items-center [&_*[role=option]>span]:gap-2 [&_*[role=option]]:pe-8 [&_*[role=option]]:ps-2"> <SelectContent className="[&_*[role=option]>span>svg]:shrink-0 [&_*[role=option]>span>svg]:text-muted-foreground/80 [&_*[role=option]>span]:end-2 [&_*[role=option]>span]:start-auto [&_*[role=option]>span]:flex [&_*[role=option]>span]:items-center [&_*[role=option]>span]:gap-2 [&_*[role=option]]:pe-8 [&_*[role=option]]:ps-2">
{SUPPORTED_LANGUAGES.map((lang) => ( {Object.values(EditorLanguageConfig).map((langConfig) => (
<SelectItem key={lang.id} value={lang.id}> <SelectItem key={langConfig.id} value={langConfig.id}>
{lang.icon} <langConfig.icon size={16} aria-hidden="true" />
<span className="truncate text-sm font-semibold mr-2"> <span className="truncate text-sm font-semibold mr-2">
{lang.label} {langConfig.label}
</span> </span>
</SelectItem> </SelectItem>
))} ))}

View File

@ -1,19 +1,18 @@
"use client"; "use client";
import { RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { DEFAULT_EDITOR_VALUE } from "@/config/editor/value"; import { RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
import { TEMP_DEFAULT_EDITOR_VALUE } from "@/config/problem/value";
export default function ResetButton() { export default function ResetButton() {
const { editor, language } = useCodeEditorStore(); const { editor, language } = useCodeEditorStore();
return ( return (
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@ -24,7 +23,7 @@ export default function ResetButton() {
aria-label="Reset Code" aria-label="Reset Code"
onClick={() => { onClick={() => {
if (editor) { if (editor) {
const value = DEFAULT_EDITOR_VALUE[language]; const value = TEMP_DEFAULT_EDITOR_VALUE[language];
const model = editor.getModel(); const model = editor.getModel();
if (model) { if (model) {
const fullRange = model.getFullModelRange(); const fullRange = model.getFullModelRange();

View File

@ -3,7 +3,9 @@
import tar from "tar-stream"; import tar from "tar-stream";
import Docker from "dockerode"; import Docker from "dockerode";
import { Readable, Writable } from "stream"; import { Readable, Writable } from "stream";
import { ExitCode, JudgeResult, LanguageConfigs } from "@/config/judge"; import { JudgeConfig } from "@/config/judge";
import { EditorLanguage } from "@/types/editor-language";
import { ExitCode, JudgeResultMetadata } from "@/types/judge";
// Docker client initialization // Docker client initialization
const docker = new Docker({ socketPath: "/var/run/docker.sock" }); const docker = new Docker({ socketPath: "/var/run/docker.sock" });
@ -41,10 +43,11 @@ function createTarStream(file: string, value: string) {
} }
export async function judge( export async function judge(
language: string, language: EditorLanguage,
value: string value: string
): Promise<JudgeResult> { ): Promise<JudgeResultMetadata> {
const { fileName, fileExtension, image, tag, workingDir, memoryLimit, timeLimit, compileOutputLimit, runOutputLimit } = LanguageConfigs[language]; const { fileName, fileExtension } = JudgeConfig[language].editorLanguageMetadata;
const { image, tag, workingDir, memoryLimit, timeLimit, compileOutputLimit, runOutputLimit } = JudgeConfig[language].dockerMetadata;
const file = `${fileName}.${fileExtension}`; const file = `${fileName}.${fileExtension}`;
let container: Docker.Container | undefined; let container: Docker.Container | undefined;
@ -70,14 +73,14 @@ export async function judge(
} }
} }
async function compile(container: Docker.Container, file: string, fileName: string, maxOutput: number = 1 * 1024 * 1024): Promise<JudgeResult> { async function compile(container: Docker.Container, file: string, fileName: string, maxOutput: number = 1 * 1024 * 1024): Promise<JudgeResultMetadata> {
const compileExec = await container.exec({ const compileExec = await container.exec({
Cmd: ["gcc", "-O2", file, "-o", fileName], Cmd: ["gcc", "-O2", file, "-o", fileName],
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
}); });
return new Promise<JudgeResult>((resolve, reject) => { return new Promise<JudgeResultMetadata>((resolve, reject) => {
compileExec.start({}, (error, stream) => { compileExec.start({}, (error, stream) => {
if (error || !stream) { if (error || !stream) {
return reject({ output: "System Error", exitCode: ExitCode.SE }); return reject({ output: "System Error", exitCode: ExitCode.SE });
@ -126,7 +129,7 @@ async function compile(container: Docker.Container, file: string, fileName: stri
const stderr = stderrChunks.join(""); const stderr = stderrChunks.join("");
const exitCode = (await compileExec.inspect()).ExitCode; const exitCode = (await compileExec.inspect()).ExitCode;
let result: JudgeResult; let result: JudgeResultMetadata;
if (exitCode !== 0 || stderr) { if (exitCode !== 0 || stderr) {
result = { output: stderr || "Compilation Error", exitCode: ExitCode.CE }; result = { output: stderr || "Compilation Error", exitCode: ExitCode.CE };
@ -145,14 +148,14 @@ async function compile(container: Docker.Container, file: string, fileName: stri
} }
// Run code and implement timeout // Run code and implement timeout
async function run(container: Docker.Container, fileName: string, timeLimit?: number, maxOutput: number = 1 * 1024 * 1024): Promise<JudgeResult> { async function run(container: Docker.Container, fileName: string, timeLimit?: number, maxOutput: number = 1 * 1024 * 1024): Promise<JudgeResultMetadata> {
const runExec = await container.exec({ const runExec = await container.exec({
Cmd: [`./${fileName}`], Cmd: [`./${fileName}`],
AttachStdout: true, AttachStdout: true,
AttachStderr: true, AttachStderr: true,
}); });
return new Promise<JudgeResult>((resolve, reject) => { return new Promise<JudgeResultMetadata>((resolve, reject) => {
const stdoutChunks: string[] = []; const stdoutChunks: string[] = [];
let stdoutLength = 0; let stdoutLength = 0;
const stdoutStream = new Writable({ const stdoutStream = new Writable({
@ -211,7 +214,7 @@ async function run(container: Docker.Container, fileName: string, timeLimit?: nu
const stderr = stderrChunks.join(""); const stderr = stderrChunks.join("");
const exitCode = (await runExec.inspect()).ExitCode; const exitCode = (await runExec.inspect()).ExitCode;
let result: JudgeResult; let result: JudgeResultMetadata;
// Exit code 0 means successful execution // Exit code 0 means successful execution
if (exitCode === 0) { if (exitCode === 0) {

View File

@ -1,240 +1,127 @@
"use client"; "use client";
import {
toSocket,
WebSocketMessageReader,
WebSocketMessageWriter,
} from "vscode-ws-jsonrpc";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useTheme } from "next-themes"; import { Skeleton } from "./ui/skeleton";
import normalizeUrl from "normalize-url";
import { highlighter } from "@/lib/shiki"; import { highlighter } from "@/lib/shiki";
import { useEffect, useRef } from "react"; import type { editor } from "monaco-editor";
import { shikiToMonaco } from "@shikijs/monaco"; import { shikiToMonaco } from "@shikijs/monaco";
import { Skeleton } from "@/components/ui/skeleton"; import type { Monaco } from "@monaco-editor/react";
import { CODE_EDITOR_OPTIONS } from "@/constants/option"; import { useCallback, useEffect, useRef } from "react";
import { DEFAULT_EDITOR_PATH } from "@/config/editor/path"; import { useMonacoTheme } from "@/hooks/use-monaco-theme";
import { DEFAULT_EDITOR_VALUE } from "@/config/editor/value"; import LanguageServerConfig from "@/config/language-server";
import { connectToLanguageServer } from "@/lib/language-server";
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
import type { MonacoLanguageClient } from "monaco-languageclient"; import type { MonacoLanguageClient } from "monaco-languageclient";
import { SUPPORTED_LANGUAGE_SERVERS } from "@/config/lsp/language-server";
import { useCodeEditorOptionStore, useCodeEditorStore } from "@/store/useCodeEditorStore";
// Skeleton component for loading state
const CodeEditorLoadingSkeleton = () => (
<div className="h-full w-full p-2">
<Skeleton className="h-full w-full rounded-3xl" />
</div>
);
// Dynamically import Monaco Editor with SSR disabled
const Editor = dynamic( const Editor = dynamic(
async () => { async () => {
await import("vscode"); await import("vscode");
const monaco = await import("monaco-editor"); const monaco = await import("monaco-editor");
const { loader } = await import("@monaco-editor/react"); const { loader } = await import("@monaco-editor/react");
loader.config({ monaco }); loader.config({ monaco });
return (await import("@monaco-editor/react")).Editor; return (await import("@monaco-editor/react")).Editor;
}, },
{ {
ssr: false, ssr: false,
loading: () => ( loading: () => <CodeEditorLoadingSkeleton />,
<div className="h-full w-full p-4">
<Skeleton className="h-full w-full rounded-3xl" />
</div>
),
} }
); );
type ConnectionHandle = {
client: MonacoLanguageClient | null;
socket: WebSocket | null;
controller: AbortController;
};
export default function CodeEditor() { export default function CodeEditor() {
const { resolvedTheme } = useTheme(); const {
const connectionRef = useRef<ConnectionHandle>({ hydrated,
client: null, language,
socket: null, path,
controller: new AbortController(), value,
}); editorConfig,
const { fontSize, lineHeight } = useCodeEditorOptionStore(); isLspEnabled,
const { language, setEditor } = useCodeEditorStore(); setEditor,
} = useCodeEditorStore();
const { monacoTheme } = useMonacoTheme();
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const monacoLanguageClientRef = useRef<MonacoLanguageClient | null>(null);
useEffect(() => { // Connect to LSP only if enabled
const currentHandle: ConnectionHandle = { const connectLSP = useCallback(async () => {
client: null, if (!(isLspEnabled && language && editorRef.current)) return;
socket: null,
controller: new AbortController(),
};
const signal = currentHandle.controller.signal;
connectionRef.current = currentHandle;
const cleanupConnection = async (handle: ConnectionHandle) => { const lspConfig = LanguageServerConfig[language];
if (!lspConfig) return;
// If there's an existing language client, stop it first
if (monacoLanguageClientRef.current) {
monacoLanguageClientRef.current.stop();
monacoLanguageClientRef.current = null;
}
// Create a new language client
try { try {
// Cleanup Language Client const monacoLanguageClient = await connectToLanguageServer(
if (handle.client) { lspConfig.protocol,
console.log("Stopping language client..."); lspConfig.hostname,
await handle.client.stop(250).catch(() => { }); lspConfig.port,
handle.client.dispose(); lspConfig.path,
} lspConfig.lang
} catch (e) {
console.log("Client cleanup error:", e);
} finally {
handle.client = null;
}
// Cleanup WebSocket
if (handle.socket) {
console.log("Closing WebSocket...");
const socket = handle.socket;
socket.onopen = null;
socket.onerror = null;
socket.onclose = null;
socket.onmessage = null;
try {
if (
[WebSocket.OPEN, WebSocket.CONNECTING].includes(
socket.readyState as WebSocket["OPEN"] | WebSocket["CONNECTING"]
)
) {
socket.close(1000, "Connection replaced");
}
} catch (e) {
console.log("Socket close error:", e);
} finally {
handle.socket = null;
}
}
};
const initialize = async () => {
try {
// Cleanup old connection
await cleanupConnection(connectionRef.current);
const serverConfig = SUPPORTED_LANGUAGE_SERVERS.find(
(s) => s.id === language
); );
if (!serverConfig || signal.aborted) return; monacoLanguageClientRef.current = monacoLanguageClient;
// Create WebSocket connection
const lspUrl = `${serverConfig.protocol}://${serverConfig.hostname}${serverConfig.port ? `:${serverConfig.port}` : ""}${serverConfig.path || ""}`;
const webSocket = new WebSocket(normalizeUrl(lspUrl));
currentHandle.socket = webSocket;
// Wait for connection to establish or timeout
await Promise.race([
new Promise<void>((resolve, reject) => {
webSocket.onopen = () => {
if (signal.aborted) reject(new Error("Connection aborted"));
else resolve();
};
webSocket.onerror = () => reject(new Error("WebSocket error"));
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Connection timeout")), 5000)
),
]);
if (signal.aborted) {
webSocket.close(1001, "Connection aborted");
return;
}
// Initialize Language Client
const { MonacoLanguageClient } = await import("monaco-languageclient");
const { ErrorAction, CloseAction } = await import("vscode-languageclient");
const socket = toSocket(webSocket);
const client = new MonacoLanguageClient({
name: `${serverConfig.label} Client`,
clientOptions: {
documentSelector: [serverConfig.id],
errorHandler: {
error: () => ({ action: ErrorAction.Continue }),
closed: () => ({ action: CloseAction.DoNotRestart }),
},
},
connectionProvider: {
get: () =>
Promise.resolve({
reader: new WebSocketMessageReader(socket),
writer: new WebSocketMessageWriter(socket),
}),
},
});
client.start();
currentHandle.client = client;
// Bind WebSocket close event
webSocket.onclose = (event) => {
if (!signal.aborted) {
console.log("WebSocket closed:", event);
client.stop();
}
};
} catch (error) { } catch (error) {
if (!signal.aborted) { console.error("Failed to connect to LSP:", error);
console.error("Connection failed:", error);
} }
cleanupConnection(currentHandle); }, [isLspEnabled, language]);
}
};
initialize(); // Connect to LSP once the editor has mounted
const handleEditorDidMount = useCallback(
async (editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
await connectLSP();
setEditor(editor);
},
[connectLSP, setEditor]
);
// Reconnect to the LSP whenever language or lspConfig changes
useEffect(() => {
connectLSP();
}, [connectLSP]);
// Cleanup the LSP connection when the component unmounts
useEffect(() => {
return () => { return () => {
console.log("Cleanup triggered"); if (monacoLanguageClientRef.current) {
currentHandle.controller.abort(); monacoLanguageClientRef.current.stop();
cleanupConnection(currentHandle); monacoLanguageClientRef.current = null;
};
}, [language]);
const mergeOptions = {
...CODE_EDITOR_OPTIONS,
fontSize,
lineHeight,
};
function handleEditorChange(value: string | undefined) {
if (typeof window !== "undefined") {
localStorage.setItem(`code-editor-value-${language}`, value ?? "");
} }
};
}, []);
if (!hydrated) {
return <CodeEditorLoadingSkeleton />;
} }
const editorValue = function handleEditorWillMount(monaco: Monaco) {
typeof window !== "undefined" shikiToMonaco(highlighter, monaco);
? localStorage.getItem(`code-editor-value-${language}`) || }
DEFAULT_EDITOR_VALUE[language]
: DEFAULT_EDITOR_VALUE[language];
return ( return (
<Editor <Editor
defaultLanguage={language} language={language}
value={editorValue} theme={monacoTheme.id}
path={DEFAULT_EDITOR_PATH[language]} path={path}
theme={resolvedTheme === "light" ? "github-light-default" : "github-dark-default"} value={value}
className="h-full" beforeMount={handleEditorWillMount}
options={mergeOptions} onMount={handleEditorDidMount}
beforeMount={(monaco) => { options={editorConfig}
shikiToMonaco(highlighter, monaco); loading={<CodeEditorLoadingSkeleton />}
}} className="h-full w-full py-2"
onMount={(editor) => {
setEditor(editor);
}}
onChange={handleEditorChange}
// onValidate={(markers) => {
// markers.forEach((marker) => {
// console.log(marker.severity);
// console.log(marker.startLineNumber);
// console.log(marker.startColumn);
// console.log(marker.endLineNumber);
// console.log(marker.endColumn);
// console.log(marker.message);
// });
// }}
loading={
<div className="h-full w-full p-4">
<Skeleton className="h-full w-full rounded-3xl" />
</div>
}
/> />
); );
} }

View File

@ -1,9 +1,9 @@
"use client"; "use client";
import { cn } from "@/lib/utils";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CheckIcon, CopyIcon } from "lucide-react"; import { CheckIcon, CopyIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface CodeBlockWithCopyProps { interface CodeBlockWithCopyProps {
children: React.ReactNode; children: React.ReactNode;

View File

@ -0,0 +1,25 @@
import { COriginal, CplusplusOriginal } from "devicons-react";
import { EditorLanguage, EditorLanguageMetadata } from "@/types/editor-language";
// Define language configurations
const EditorLanguageConfig: Record<EditorLanguage, EditorLanguageMetadata> = {
[EditorLanguage.C]: {
id: EditorLanguage.C,
label: "C",
fileName: "main",
fileExtension: ".c",
icon: COriginal,
},
[EditorLanguage.CPP]: {
id: EditorLanguage.CPP,
label: "C++",
fileName: "main",
fileExtension: ".cpp",
icon: CplusplusOriginal,
},
};
// Default language configuration
const DefaultEditorLanguageConfig = EditorLanguageConfig[EditorLanguage.C]; // Default to C language
export { EditorLanguageConfig, DefaultEditorLanguageConfig };

View File

@ -1,6 +1,6 @@
import { type editor } from "monaco-editor"; import { type editor } from "monaco-editor";
export const CODE_EDITOR_OPTIONS: editor.IEditorConstructionOptions = { export const DefaultEditorOptionConfig: editor.IEditorConstructionOptions = {
autoIndent: "full", autoIndent: "full",
automaticLayout: true, automaticLayout: true,
contextmenu: true, contextmenu: true,
@ -15,16 +15,13 @@ export const CODE_EDITOR_OPTIONS: editor.IEditorConstructionOptions = {
lineHeight: 20, lineHeight: 20,
matchBrackets: "always", matchBrackets: "always",
minimap: { minimap: {
enabled: false enabled: false,
},
padding: {
top: 8
}, },
readOnly: false, readOnly: false,
scrollbar: { scrollbar: {
horizontalSliderSize: 10, horizontalSliderSize: 10,
verticalSliderSize: 10 verticalSliderSize: 10,
}, },
showFoldingControls: "always", showFoldingControls: "always",
wordWrap: "on", wordWrap: "on",
} };

View File

@ -1,3 +0,0 @@
import { SUPPORTED_LANGUAGES, SupportedLanguage } from "@/constants/language";
export const DEFAULT_EDITOR_LANGUAGE: SupportedLanguage = SUPPORTED_LANGUAGES[0].id;

View File

@ -1,6 +0,0 @@
import { SupportedLanguage } from "@/constants/language";
export const DEFAULT_EDITOR_PATH: Record<SupportedLanguage, string> = {
c: "file:///main.c",
cpp: "file:///main.cpp",
};

View File

@ -1,42 +1,9 @@
// Result type definitions import { EditorLanguage } from "@/types/editor-language";
export enum ExitCode { import { EditorLanguageConfig } from "./editor-language";
SE = 0, // System Error import { DockerMetadata, JudgeMetadata } from "@/types/judge";
CS = 1, // Compilation Success
CE = 2, // Compilation Error
TLE = 3, // Time Limit Exceeded
MLE = 4, // Memory Limit Exceeded
RE = 5, // Runtime Error
AC = 6, // Accepted
WA = 7, // Wrong Answer
}
export type JudgeResult = { export const DockerConfig: Record<EditorLanguage, DockerMetadata> = {
output: string; [EditorLanguage.C]: {
exitCode: ExitCode;
executionTime?: number;
memoryUsage?: number;
};
export interface LanguageConfig {
id: string;
label: string;
fileName: string;
fileExtension: string;
image: string;
tag: string;
workingDir: string;
timeLimit: number;
memoryLimit: number;
compileOutputLimit: number;
runOutputLimit: number;
}
export const LanguageConfigs: Record<string, LanguageConfig> = {
c: {
id: "c",
label: "C",
fileName: "main",
fileExtension: "c",
image: "gcc", image: "gcc",
tag: "latest", tag: "latest",
workingDir: "/src", workingDir: "/src",
@ -45,11 +12,7 @@ export const LanguageConfigs: Record<string, LanguageConfig> = {
compileOutputLimit: 1 * 1024 * 1024, compileOutputLimit: 1 * 1024 * 1024,
runOutputLimit: 1 * 1024 * 1024, runOutputLimit: 1 * 1024 * 1024,
}, },
cpp: { [EditorLanguage.CPP]: {
id: "cpp",
label: "C++",
fileName: "main",
fileExtension: "cpp",
image: "gcc", image: "gcc",
tag: "latest", tag: "latest",
workingDir: "/src", workingDir: "/src",
@ -57,5 +20,16 @@ export const LanguageConfigs: Record<string, LanguageConfig> = {
memoryLimit: 128, memoryLimit: 128,
compileOutputLimit: 1 * 1024 * 1024, compileOutputLimit: 1 * 1024 * 1024,
runOutputLimit: 1 * 1024 * 1024, runOutputLimit: 1 * 1024 * 1024,
}
}
export const JudgeConfig: Record<EditorLanguage, JudgeMetadata> = {
[EditorLanguage.C]: {
editorLanguageMetadata: EditorLanguageConfig[EditorLanguage.C],
dockerMetadata: DockerConfig[EditorLanguage.C],
},
[EditorLanguage.CPP]: {
editorLanguageMetadata: EditorLanguageConfig[EditorLanguage.CPP],
dockerMetadata: DockerConfig[EditorLanguage.CPP],
}, },
}; };

View File

@ -1,29 +1,22 @@
import { SupportedLanguage } from '@/constants/language' import { EditorLanguage } from "@/types/editor-language";
import { EditorLanguageConfig } from "./editor-language";
import { LanguageServerMetadata } from "@/types/language-server";
export interface LanguageServerConfig { const LanguageServerConfig: Record<EditorLanguage, LanguageServerMetadata> = {
id: SupportedLanguage [EditorLanguage.C]: {
label: string
hostname: string
protocol: string
port: number | null
path: string | null
}
export const SUPPORTED_LANGUAGE_SERVERS: LanguageServerConfig[] = [
{
id: "c",
label: "C",
protocol: process.env.NEXT_PUBLIC_LSP_C_PROTOCOL || "ws", protocol: process.env.NEXT_PUBLIC_LSP_C_PROTOCOL || "ws",
hostname: process.env.NEXT_PUBLIC_LSP_C_HOSTNAME || "localhost", hostname: process.env.NEXT_PUBLIC_LSP_C_HOSTNAME || "localhost",
port: process.env.NEXT_PUBLIC_LSP_C_PORT ? parseInt(process.env.NEXT_PUBLIC_LSP_C_PORT, 10) : 4594, port: process.env.NEXT_PUBLIC_LSP_C_PORT ? parseInt(process.env.NEXT_PUBLIC_LSP_C_PORT, 10) : 4594,
path: process.env.NEXT_PUBLIC_LSP_C_PATH || "/clangd", path: process.env.NEXT_PUBLIC_LSP_C_PATH || "/clangd",
lang: EditorLanguageConfig[EditorLanguage.C],
}, },
{ [EditorLanguage.CPP]: {
id: "cpp",
label: "C++",
protocol: process.env.NEXT_PUBLIC_LSP_CPP_PROTOCOL || "ws", protocol: process.env.NEXT_PUBLIC_LSP_CPP_PROTOCOL || "ws",
hostname: process.env.NEXT_PUBLIC_LSP_CPP_HOSTNAME || "localhost", hostname: process.env.NEXT_PUBLIC_LSP_CPP_HOSTNAME || "localhost",
port: process.env.NEXT_PUBLIC_LSP_CPP_PORT ? parseInt(process.env.NEXT_PUBLIC_LSP_CPP_PORT, 10) : 4595, port: process.env.NEXT_PUBLIC_LSP_CPP_PORT ? parseInt(process.env.NEXT_PUBLIC_LSP_CPP_PORT, 10) : 4595,
path: process.env.NEXT_PUBLIC_LSP_CPP_PATH || "/clangd", path: process.env.NEXT_PUBLIC_LSP_CPP_PATH || "/clangd",
lang: EditorLanguageConfig[EditorLanguage.CPP],
}, },
]; };
export default LanguageServerConfig;

View File

@ -0,0 +1,15 @@
import { MonacoTheme } from "@/types/monaco-theme";
// Define theme configurations
const MonacoThemeConfig = {
[MonacoTheme.GitHubLightDefault]: {
id: MonacoTheme.GitHubLightDefault,
label: "Github Light Default",
},
[MonacoTheme.GitHubDarkDefault]: {
id: MonacoTheme.GitHubDarkDefault,
label: "Github Dark Default",
},
};
export { MonacoThemeConfig };

View File

@ -1,13 +1,13 @@
import { SupportedLanguage } from "@/constants/language"; import { EditorLanguage } from "@/types/editor-language";
export const DEFAULT_EDITOR_VALUE: Record<SupportedLanguage, string> = { export const TEMP_DEFAULT_EDITOR_VALUE: Record<EditorLanguage, string> = {
c: `/** [EditorLanguage.C]: `/**
* Note: The returned array must be malloced, assume caller calls free(). * Note: The returned array must be malloced, assume caller calls free().
*/ */
int* twoSum(int* nums, int numsSize, int target, int* returnSize) { int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
}`, }`,
cpp: `class Solution { [EditorLanguage.CPP]: `class Solution {
public: public:
vector<int> twoSum(vector<int>& nums, int target) { vector<int> twoSum(vector<int>& nums, int target) {

View File

@ -1,16 +0,0 @@
import { COriginal, CplusplusOriginal } from 'devicons-react'
export const SUPPORTED_LANGUAGES = [
{
id: "c",
label: "C",
icon: <COriginal size={16} aria-hidden="true" />
},
{
id: "cpp",
label: "C++",
icon: <CplusplusOriginal size={16} aria-hidden="true" />,
},
] as const;
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]["id"];

View File

@ -0,0 +1,13 @@
import { useTheme } from "next-themes";
import { MonacoTheme } from "@/types/monaco-theme";
import { MonacoThemeConfig } from "@/config/monaco-theme";
export function useMonacoTheme() {
const { resolvedTheme } = useTheme();
const monacoTheme = resolvedTheme === "light" ? MonacoThemeConfig[MonacoTheme.GitHubLightDefault] : MonacoThemeConfig[MonacoTheme.GitHubDarkDefault];
return {
monacoTheme,
};
}

View File

@ -0,0 +1,68 @@
import normalizeUrl from "normalize-url";
import type { MessageTransports } from "vscode-languageclient";
import { EditorLanguageMetadata } from "@/types/editor-language";
import type { MonacoLanguageClient } from "monaco-languageclient";
import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from "vscode-ws-jsonrpc";
// Create the WebSocket URL based on the protocol and port
function createUrl(protocol: string, hostname: string, port: number | null, path: string | null): string {
return normalizeUrl(`${protocol}://${hostname}${port ? `:${port}` : ""}${path || ""}`);
}
// Create the language client with the given transports
async function createLanguageClient(transports: MessageTransports, lang: EditorLanguageMetadata): Promise<MonacoLanguageClient> {
const { MonacoLanguageClient } = await import("monaco-languageclient");
const { CloseAction, ErrorAction } = await import("vscode-languageclient");
return new MonacoLanguageClient({
name: `${lang.label} Language Client`,
clientOptions: {
// use a language id as a document selector
documentSelector: [lang.id],
// disable the default error handler
errorHandler: {
error: () => ({ action: ErrorAction.Continue }),
closed: () => ({ action: CloseAction.DoNotRestart }),
},
},
// create a language client connection from the JSON RPC connection on demand
connectionProvider: {
get: () => {
return Promise.resolve(transports);
}
}
});
}
// Connect to the WebSocket and create the language client
export function connectToLanguageServer(protocol: string, hostname: string, port: number | null, path: string | null, lang: EditorLanguageMetadata): Promise<MonacoLanguageClient> {
const url = createUrl(protocol, hostname, port, path);
const webSocket = new WebSocket(url);
return new Promise((resolve, reject) => {
// Handle the WebSocket opening event
webSocket.onopen = async () => {
const socket = toSocket(webSocket);
const reader = new WebSocketMessageReader(socket);
const writer = new WebSocketMessageWriter(socket);
try {
const languageClient = await createLanguageClient({ reader, writer }, lang);
// Start the language client
languageClient.start();
// Stop the language client when the reader closes
reader.onClose(() => languageClient.stop());
resolve(languageClient);
} catch (error) {
reject(error);
}
};
// Handle WebSocket errors
webSocket.onerror = (error) => {
reject(error);
};
});
}

View File

@ -1,12 +1,23 @@
import { MonacoTheme } from "@/types/monaco-theme";
import { createHighlighter, Highlighter } from "shiki"; import { createHighlighter, Highlighter } from "shiki";
import { EditorLanguage } from "@/types/editor-language";
// Get all values from the ProgrammingLanguage and Theme enums
const themes = Object.values(MonacoTheme);
const languages = Object.values(EditorLanguage);
// Use lazy initialization for highlighter
let highlighter: Highlighter; let highlighter: Highlighter;
async function initializeHighlighter() { async function initializeHighlighter() {
try {
highlighter = await createHighlighter({ highlighter = await createHighlighter({
themes: ["github-light-default", "github-dark-default"], themes: themes, // Use all values from the Theme enum
langs: ["c"], langs: languages, // Use all values from the ProgrammingLanguage enum
}); });
} catch (error) {
console.error("Error initializing highlighter:", error);
}
} }
initializeHighlighter(); initializeHighlighter();

View File

@ -1,6 +1,13 @@
import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge";
import { twMerge } from "tailwind-merge" import { clsx, type ClassValue } from "clsx";
import { EditorLanguage } from "@/types/editor-language";
import LanguageServerConfig from "@/config/language-server";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
export function getPath(lang: EditorLanguage): string {
const config = LanguageServerConfig[lang];
return `file:///${config.lang.fileName}${config.lang.fileExtension}`;
}

View File

@ -1,58 +1,75 @@
import { create } from "zustand"; import { create } from "zustand";
import { type editor } from "monaco-editor"; import { getPath } from "@/lib/utils";
import { persist } from "zustand/middleware"; import type { editor } from "monaco-editor";
import { JudgeResult } from "@/config/judge"; import { JudgeResultMetadata } from "@/types/judge";
import { CODE_EDITOR_OPTIONS } from "@/constants/option"; import { EditorLanguage } from "@/types/editor-language";
import { SupportedLanguage } from "@/constants/language"; import { createJSONStorage, persist } from "zustand/middleware";
import { MonacoLanguageClient } from "monaco-languageclient"; import { LanguageServerMetadata } from "@/types/language-server";
import { DEFAULT_EDITOR_LANGUAGE } from "@/config/editor/language"; import { DefaultEditorOptionConfig } from "@/config/editor-option";
import { DefaultEditorLanguageConfig } from "@/config/editor-language";
interface CodeEditorState { interface CodeEditorState {
editor: editor.IStandaloneCodeEditor | null;
language: SupportedLanguage;
languageClient: MonacoLanguageClient | null;
hydrated: boolean; hydrated: boolean;
result: JudgeResult | null; language: EditorLanguage;
setEditor: (editor: editor.IStandaloneCodeEditor | null) => void; path: string;
setLanguage: (language: SupportedLanguage) => void; value: string;
setLanguageClient: (languageClient: MonacoLanguageClient | null) => void; lspConfig: LanguageServerMetadata | null;
isLspEnabled: boolean;
editorConfig: editor.IEditorConstructionOptions;
editor: editor.IStandaloneCodeEditor | null;
result: JudgeResultMetadata | null;
setHydrated: (value: boolean) => void; setHydrated: (value: boolean) => void;
setResult: (result: JudgeResult) => void; setLanguage: (language: EditorLanguage) => void;
setPath: (path: string) => void;
setValue: (value: string) => void;
setLspConfig: (lspConfig: LanguageServerMetadata) => void;
setIsLspEnabled: (enabled: boolean) => void;
setEditorConfig: (editorConfig: editor.IEditorConstructionOptions) => void;
setEditor: (editor: editor.IStandaloneCodeEditor) => void;
setResult: (result: JudgeResultMetadata) => void;
} }
export const useCodeEditorStore = create<CodeEditorState>()( export const useCodeEditorStore = create<CodeEditorState>()(
persist( persist(
(set) => ({ (set) => ({
editor: null,
language: DEFAULT_EDITOR_LANGUAGE,
languageClient: null,
hydrated: false, hydrated: false,
language: DefaultEditorLanguageConfig.id,
path: getPath(DefaultEditorLanguageConfig.id),
value: "#include<stdio.h>",
lspConfig: null,
isLspEnabled: true,
editorConfig: DefaultEditorOptionConfig,
editor: null,
result: null, result: null,
setEditor: (editor) => set({ editor }),
setLanguage: (language) => set({ language }),
setLanguageClient: (languageClient) => set({ languageClient }),
setHydrated: (value) => set({ hydrated: value }), setHydrated: (value) => set({ hydrated: value }),
setLanguage: (language) => set({ language }),
setPath: (path) => set({ path }),
setValue: (value) => set({ value }),
setLspConfig: (lspConfig) => set({ lspConfig }),
setIsLspEnabled: (enabled) => set({ isLspEnabled: enabled }),
setEditorConfig: (editorConfig) => set({ editorConfig }),
setEditor: (editor) => set({ editor: editor }),
setResult: (result) => set({ result }), setResult: (result) => set({ result }),
}), }),
{ {
name: "code-editor-language", name: "code-editor-store",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({ partialize: (state) => ({
language: state.language, language: state.language,
path: state.path,
value: state.value,
isLspEnabled: state.isLspEnabled,
editorConfig: state.editorConfig,
}), }),
onRehydrateStorage: () => (state, error) => { onRehydrateStorage: () => {
return (state, error) => {
if (error) { if (error) {
console.error("hydrate error", error); console.error("An error happened during hydration", error);
} else if (state) { } else if (state) {
state.setHydrated(true); state.setHydrated(true);
} }
};
}, },
} }
) )
); );
export const useCodeEditorOptionStore = create<editor.IEditorConstructionOptions>((set) => ({
fontSize: CODE_EDITOR_OPTIONS.fontSize,
lineHeight: CODE_EDITOR_OPTIONS.lineHeight,
setFontSize: (fontSize: number) => set({ fontSize }),
setLineHeight: (lineHeight: number) => set({ lineHeight }),
}));

View File

@ -0,0 +1,12 @@
export enum EditorLanguage {
C = "c",
CPP = "cpp",
}
export type EditorLanguageMetadata = {
id: EditorLanguage;
label: string;
fileName: string;
fileExtension: string;
icon: React.FunctionComponent<React.SVGProps<SVGElement> & { size?: number | string }>;
};

35
src/types/judge.ts Normal file
View File

@ -0,0 +1,35 @@
import { EditorLanguageMetadata } from "./editor-language";
// Result type definitions
export enum ExitCode {
SE = 0, // System Error
CS = 1, // Compilation Success
CE = 2, // Compilation Error
TLE = 3, // Time Limit Exceeded
MLE = 4, // Memory Limit Exceeded
RE = 5, // Runtime Error
AC = 6, // Accepted
WA = 7, // Wrong Answer
}
export type JudgeResultMetadata = {
output: string;
exitCode: ExitCode;
executionTime?: number;
memoryUsage?: number;
};
export type JudgeMetadata = {
editorLanguageMetadata: EditorLanguageMetadata;
dockerMetadata: DockerMetadata;
};
export type DockerMetadata = {
image: string;
tag: string;
workingDir: string;
timeLimit: number;
memoryLimit: number;
compileOutputLimit: number;
runOutputLimit: number;
}

View File

@ -0,0 +1,9 @@
import { EditorLanguageMetadata } from "./editor-language";
export type LanguageServerMetadata = {
protocol: string;
hostname: string;
port: number | null;
path: string | null;
lang: EditorLanguageMetadata;
};

View File

@ -0,0 +1,9 @@
export enum MonacoTheme {
GitHubLightDefault = "github-light-default",
GitHubDarkDefault = "github-dark-default",
}
export type MonacoThemeMetadata = {
id: MonacoTheme;
label: string;
};