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
35 changed files with 522 additions and 537 deletions

View File

@ -1,6 +1,11 @@
.git
.github
.next
node_modules
.dockerignore
.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; \
elif [ -f package-lock.json ]; then npm ci; \
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 && \
curl -fsSL https://bun.sh/install | bash && \
export BUN_INSTALL="$HOME/.bun" && \
@ -25,7 +25,6 @@ RUN \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
@ -41,7 +40,7 @@ RUN \
if [ -f yarn.lock ]; then yarn 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 bun.lock ]; then \
elif [ -f bun.lock ] || [ -f bun.lockb ]; then \
apk add --no-cache curl bash && \
curl -fsSL https://bun.sh/install | bash && \
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.
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# 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/static ./.next/static
USER nextjs
USER root
EXPOSE 3000

View File

@ -1,74 +0,0 @@
# syntax=docker.io/docker/dockerfile:1
FROM dockerp.com/node:22-alpine AS base
FROM base AS deps
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirror.nju.edu.cn/alpine#g' /etc/apk/repositories && \
apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* bun.lock* .npmrc* ./
# 统一配置所有包管理器使用淘宝源
RUN \
npm config set registry https://registry.npmmirror.com && \
if [ -f yarn.lock ]; then \
# yarn config set registry https://registry.npmmirror.com && \
yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then \
npm ci --registry=https://registry.npmmirror.com; \
elif [ -f pnpm-lock.yaml ]; then \
corepack enable pnpm && \
# pnpm config set registry https://registry.npmmirror.com && \
pnpm i --frozen-lockfile; \
elif [ -f bun.lock ]; then \
sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirror.nju.edu.cn/alpine#g' /etc/apk/repositories && \
apk add --no-cache curl bash && \
npm install -g bun && \
export BUN_INSTALL="$HOME/.bun" && \
export PATH="$BUN_INSTALL/bin:$PATH" && \
bun install --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
# 合并构建命令
RUN \
npm config set registry https://registry.npmmirror.com && \
if [ -f yarn.lock ]; then yarn 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 bun.lock ]; then \
sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirror.nju.edu.cn/alpine#g' /etc/apk/repositories && \
apk add --no-cache curl bash && \
npm install -g bun && \
export BUN_INSTALL="$HOME/.bun" && \
export PATH="$BUN_INSTALL/bin:$PATH" && \
bun run build; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS runner
WORKDIR /app
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirror.nju.edu.cn/alpine#g' /etc/apk/repositories && \
apk add --no-cache curl && \
addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000 HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View File

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

View File

@ -16,7 +16,6 @@
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@types/vscode": "^1.97.0",
"bun": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"devicons-react": "^1.4.0",
@ -199,28 +198,6 @@
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "https://registry.npmmirror.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xBz/Q7X6AFwMg7MXtBemjjt5uB+tvEYBmi9Zbm1r8qnI2V8m/Smuhma0EARhiVfLuIAYj2EM5qjzxeAFV4TBJA=="],
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ufyty+2754QCFDhq447H39JiqabMlFRItLn1YFp+2hdpKak7KCYLGOUuHnlr1pmImKJzDHURjnvTTq1QRlUWAA=="],
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-stsq8vBiYgfGunBGlf2M7ST7Ymyw3WnwrxEeJ04vkKmMEEE2LpX8Rkol6UPRvZawab9s9/scFIRgFi6hu9H4SQ=="],
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-OhVpzme2vvLA7w8GYeJg2SQ2h2CwJQN9oYDiGeoML4EwE+DEoYHdxgaBZsQySOwZtFIr8ufpc/8iD4hssJ50qg=="],
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-+lxWF7up9MuB1ZdGxXCH3AH3XmYtdBC6soQ38+yg3+y3iOPrAlSG+wytHEkypN/UU2mGvCuaEED3cMvejrGdDw=="],
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-oof3ii92Cz2yIOZRbVFHMHmffCutRPFITIdXLZ2/rkqVuKUe0ZdqWjHPhxJFm31AL9MlJ/dSqDbPb51SaLI7tw=="],
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-3nmDDZJH73MzhBg2sRYioj4CE8wgaz0w24OieMqj4/c44BbNr3X5RewrldsMD2cU6DtVbi52FuD5WpTw3N8nmw=="],
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-cLdMbK7srNoUbYSG3Tp4GYdPAO0+5mgUhdbU053GZs0DLQmQ8h1JQhALp+ZjrUWstmQe7ddcNu7l7EAu6E76XA=="],
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qDsUvKCW0WUlVOt6Yx5eEzxgCbvzuSJBsu0sXtr6uGt8TnMKghmliRO5FmiMLQ0k/PUDA8vPJCtuv5k14bDi6g=="],
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-4YRJd4pdaTWEM+uawYmchOeNv874RGAFpIZQubWnN4SBf6HfcDm0OMMZcm0f0I70Wd5gbPg1+rvCRtDZWVmZog=="],
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-j/G4bnfsRAiCvpTADda40m29iSGvueIaF9Kc9hBu4jN8dTS9fEXdNNXuf8c30/z7/npxw2dhzsAn8jbc5QvD1A=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
@ -471,8 +448,6 @@
"buildcheck": ["buildcheck@0.0.6", "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.6.tgz", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="],
"bun": ["bun@1.2.4", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.2.4", "@oven/bun-darwin-x64": "1.2.4", "@oven/bun-darwin-x64-baseline": "1.2.4", "@oven/bun-linux-aarch64": "1.2.4", "@oven/bun-linux-aarch64-musl": "1.2.4", "@oven/bun-linux-x64": "1.2.4", "@oven/bun-linux-x64-baseline": "1.2.4", "@oven/bun-linux-x64-musl": "1.2.4", "@oven/bun-linux-x64-musl-baseline": "1.2.4", "@oven/bun-windows-x64": "1.2.4", "@oven/bun-windows-x64-baseline": "1.2.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bun.exe" } }, "sha512-ZY0EZ/UKqheaLeAtMsfJA6jWoWvV9HAtfFaOJSmS3LrNpFKs1Sg5fZLSsczN1h3a+Dtheo4O3p3ZYWrf40kRGw=="],
"busboy": ["busboy@1.6.0", "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
"call-bind": ["call-bind@1.0.8", "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],

View File

@ -2,7 +2,7 @@ services:
monaco-editor-lsp-next:
build:
context: ./
dockerfile: Dockerfile.cn
dockerfile: Dockerfile
image: monaco-editor-lsp-next:latest
container_name: monaco-editor-lsp-next
restart: always
@ -19,14 +19,13 @@ services:
timeout: 5s
retries: 5
start_period: 10s
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
lsp-c:
build:
context: ./docker/lsp/clangd
dockerfile: Dockerfile.cn
dockerfile: Dockerfile
image: lsp-c:latest
container_name: lsp-c
restart: always
@ -43,7 +42,7 @@ services:
lsp-cpp:
build:
context: ./docker/lsp/clangd
dockerfile: Dockerfile.cn
dockerfile: Dockerfile
image: lsp-cpp:latest
container_name: lsp-cpp
restart: always

View File

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

BIN
demo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

After

Width:  |  Height:  |  Size: 243 KiB

View File

@ -1,31 +0,0 @@
FROM dockerp.com/alpine:latest AS builder
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirror.nju.edu.cn/alpine#g' /etc/apk/repositories&& \
apk add --no-cache git npm
# 修改为南京大学镜像源
RUN npm config set registry https://repo.nju.edu.cn/repository/npm/
WORKDIR /app
RUN git clone https://gh-proxy.com/github.com/wylieconlon/jsonrpc-ws-proxy.git
WORKDIR /app/jsonrpc-ws-proxy
COPY servers.yml .
# 合并命令减少镜像层
RUN npm install --registry=https://repo.nju.edu.cn/repository/npm/ && npm run prepare
FROM dockerp.com/alpine:latest
RUN sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirror.nju.edu.cn/alpine#g' /etc/apk/repositories&& \
apk add --no-cache build-base clang-extra-tools nodejs
WORKDIR /app/jsonrpc-ws-proxy
COPY --from=builder /app/jsonrpc-ws-proxy .
EXPOSE 3000
CMD ["node", "dist/server.js", "--port", "3000", "--languageServers", "servers.yml"]

View File

@ -21,7 +21,6 @@
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@types/vscode": "^1.97.0",
"bun": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"devicons-react": "^1.4.0",

View File

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

View File

@ -7,28 +7,37 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { getPath } from "@/lib/utils";
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 { SUPPORTED_LANGUAGES } from "@/constants/language";
export default function LanguageSelector() {
const { hydrated, language, setLanguage } = useCodeEditorStore();
const { hydrated, language, setLanguage, setPath, setLspConfig } = useCodeEditorStore();
if (!hydrated) {
return <Skeleton className="h-6 w-16 rounded-2xl" />;
}
const handleValueChange = (lang: EditorLanguage) => {
setLanguage(lang);
setPath(getPath(lang));
setLspConfig(LanguageServerConfig[lang]);
};
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">
<SelectValue placeholder="Select language" />
</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">
{SUPPORTED_LANGUAGES.map((lang) => (
<SelectItem key={lang.id} value={lang.id}>
{lang.icon}
{Object.values(EditorLanguageConfig).map((langConfig) => (
<SelectItem key={langConfig.id} value={langConfig.id}>
<langConfig.icon size={16} aria-hidden="true" />
<span className="truncate text-sm font-semibold mr-2">
{lang.label}
{langConfig.label}
</span>
</SelectItem>
))}

View File

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

View File

@ -3,7 +3,9 @@
import tar from "tar-stream";
import Docker from "dockerode";
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
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
@ -41,10 +43,11 @@ function createTarStream(file: string, value: string) {
}
export async function judge(
language: string,
language: EditorLanguage,
value: string
): Promise<JudgeResult> {
const { fileName, fileExtension, image, tag, workingDir, memoryLimit, timeLimit, compileOutputLimit, runOutputLimit } = LanguageConfigs[language];
): Promise<JudgeResultMetadata> {
const { fileName, fileExtension } = JudgeConfig[language].editorLanguageMetadata;
const { image, tag, workingDir, memoryLimit, timeLimit, compileOutputLimit, runOutputLimit } = JudgeConfig[language].dockerMetadata;
const file = `${fileName}.${fileExtension}`;
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({
Cmd: ["gcc", "-O2", file, "-o", fileName],
AttachStdout: true,
AttachStderr: true,
});
return new Promise<JudgeResult>((resolve, reject) => {
return new Promise<JudgeResultMetadata>((resolve, reject) => {
compileExec.start({}, (error, stream) => {
if (error || !stream) {
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 exitCode = (await compileExec.inspect()).ExitCode;
let result: JudgeResult;
let result: JudgeResultMetadata;
if (exitCode !== 0 || stderr) {
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
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({
Cmd: [`./${fileName}`],
AttachStdout: true,
AttachStderr: true,
});
return new Promise<JudgeResult>((resolve, reject) => {
return new Promise<JudgeResultMetadata>((resolve, reject) => {
const stdoutChunks: string[] = [];
let stdoutLength = 0;
const stdoutStream = new Writable({
@ -211,7 +214,7 @@ async function run(container: Docker.Container, fileName: string, timeLimit?: nu
const stderr = stderrChunks.join("");
const exitCode = (await runExec.inspect()).ExitCode;
let result: JudgeResult;
let result: JudgeResultMetadata;
// Exit code 0 means successful execution
if (exitCode === 0) {

View File

@ -1,240 +1,127 @@
"use client";
import {
toSocket,
WebSocketMessageReader,
WebSocketMessageWriter,
} from "vscode-ws-jsonrpc";
import dynamic from "next/dynamic";
import { useTheme } from "next-themes";
import normalizeUrl from "normalize-url";
import { Skeleton } from "./ui/skeleton";
import { highlighter } from "@/lib/shiki";
import { useEffect, useRef } from "react";
import type { editor } from "monaco-editor";
import { shikiToMonaco } from "@shikijs/monaco";
import { Skeleton } from "@/components/ui/skeleton";
import { CODE_EDITOR_OPTIONS } from "@/constants/option";
import { DEFAULT_EDITOR_PATH } from "@/config/editor/path";
import { DEFAULT_EDITOR_VALUE } from "@/config/editor/value";
import type { Monaco } from "@monaco-editor/react";
import { useCallback, useEffect, useRef } from "react";
import { useMonacoTheme } from "@/hooks/use-monaco-theme";
import LanguageServerConfig from "@/config/language-server";
import { connectToLanguageServer } from "@/lib/language-server";
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
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(
async () => {
await import("vscode");
const monaco = await import("monaco-editor");
const { loader } = await import("@monaco-editor/react");
loader.config({ monaco });
return (await import("@monaco-editor/react")).Editor;
},
{
ssr: false,
loading: () => (
<div className="h-full w-full p-4">
<Skeleton className="h-full w-full rounded-3xl" />
</div>
),
loading: () => <CodeEditorLoadingSkeleton />,
}
);
type ConnectionHandle = {
client: MonacoLanguageClient | null;
socket: WebSocket | null;
controller: AbortController;
};
export default function CodeEditor() {
const { resolvedTheme } = useTheme();
const connectionRef = useRef<ConnectionHandle>({
client: null,
socket: null,
controller: new AbortController(),
});
const { fontSize, lineHeight } = useCodeEditorOptionStore();
const { language, setEditor } = useCodeEditorStore();
const {
hydrated,
language,
path,
value,
editorConfig,
isLspEnabled,
setEditor,
} = useCodeEditorStore();
const { monacoTheme } = useMonacoTheme();
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const monacoLanguageClientRef = useRef<MonacoLanguageClient | null>(null);
useEffect(() => {
const currentHandle: ConnectionHandle = {
client: null,
socket: null,
controller: new AbortController(),
};
const signal = currentHandle.controller.signal;
connectionRef.current = currentHandle;
// Connect to LSP only if enabled
const connectLSP = useCallback(async () => {
if (!(isLspEnabled && language && editorRef.current)) return;
const cleanupConnection = async (handle: ConnectionHandle) => {
try {
// Cleanup Language Client
if (handle.client) {
console.log("Stopping language client...");
await handle.client.stop(250).catch(() => { });
handle.client.dispose();
}
} catch (e) {
console.log("Client cleanup error:", e);
} finally {
handle.client = null;
}
const lspConfig = LanguageServerConfig[language];
// 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;
if (!lspConfig) return;
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;
// 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) {
if (!signal.aborted) {
console.error("Connection failed:", error);
}
cleanupConnection(currentHandle);
}
};
initialize();
return () => {
console.log("Cleanup triggered");
currentHandle.controller.abort();
cleanupConnection(currentHandle);
};
}, [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 there's an existing language client, stop it first
if (monacoLanguageClientRef.current) {
monacoLanguageClientRef.current.stop();
monacoLanguageClientRef.current = null;
}
// Create a new language client
try {
const monacoLanguageClient = await connectToLanguageServer(
lspConfig.protocol,
lspConfig.hostname,
lspConfig.port,
lspConfig.path,
lspConfig.lang
);
monacoLanguageClientRef.current = monacoLanguageClient;
} catch (error) {
console.error("Failed to connect to LSP:", error);
}
}, [isLspEnabled, language]);
// 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 () => {
if (monacoLanguageClientRef.current) {
monacoLanguageClientRef.current.stop();
monacoLanguageClientRef.current = null;
}
};
}, []);
if (!hydrated) {
return <CodeEditorLoadingSkeleton />;
}
const editorValue =
typeof window !== "undefined"
? localStorage.getItem(`code-editor-value-${language}`) ||
DEFAULT_EDITOR_VALUE[language]
: DEFAULT_EDITOR_VALUE[language];
function handleEditorWillMount(monaco: Monaco) {
shikiToMonaco(highlighter, monaco);
}
return (
<Editor
defaultLanguage={language}
value={editorValue}
path={DEFAULT_EDITOR_PATH[language]}
theme={resolvedTheme === "light" ? "github-light-default" : "github-dark-default"}
className="h-full"
options={mergeOptions}
beforeMount={(monaco) => {
shikiToMonaco(highlighter, monaco);
}}
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>
}
language={language}
theme={monacoTheme.id}
path={path}
value={value}
beforeMount={handleEditorWillMount}
onMount={handleEditorDidMount}
options={editorConfig}
loading={<CodeEditorLoadingSkeleton />}
className="h-full w-full py-2"
/>
);
}

View File

@ -1,9 +1,9 @@
"use client";
import { cn } from "@/lib/utils";
import { useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { CheckIcon, CopyIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface CodeBlockWithCopyProps {
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";
export const CODE_EDITOR_OPTIONS: editor.IEditorConstructionOptions = {
export const DefaultEditorOptionConfig: editor.IEditorConstructionOptions = {
autoIndent: "full",
automaticLayout: true,
contextmenu: true,
@ -15,16 +15,13 @@ export const CODE_EDITOR_OPTIONS: editor.IEditorConstructionOptions = {
lineHeight: 20,
matchBrackets: "always",
minimap: {
enabled: false
},
padding: {
top: 8
enabled: false,
},
readOnly: false,
scrollbar: {
horizontalSliderSize: 10,
verticalSliderSize: 10
verticalSliderSize: 10,
},
showFoldingControls: "always",
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
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
}
import { EditorLanguage } from "@/types/editor-language";
import { EditorLanguageConfig } from "./editor-language";
import { DockerMetadata, JudgeMetadata } from "@/types/judge";
export type JudgeResult = {
output: string;
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",
export const DockerConfig: Record<EditorLanguage, DockerMetadata> = {
[EditorLanguage.C]: {
image: "gcc",
tag: "latest",
workingDir: "/src",
@ -45,11 +12,7 @@ export const LanguageConfigs: Record<string, LanguageConfig> = {
compileOutputLimit: 1 * 1024 * 1024,
runOutputLimit: 1 * 1024 * 1024,
},
cpp: {
id: "cpp",
label: "C++",
fileName: "main",
fileExtension: "cpp",
[EditorLanguage.CPP]: {
image: "gcc",
tag: "latest",
workingDir: "/src",
@ -57,5 +20,16 @@ export const LanguageConfigs: Record<string, LanguageConfig> = {
memoryLimit: 128,
compileOutputLimit: 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 {
id: SupportedLanguage
label: string
hostname: string
protocol: string
port: number | null
path: string | null
}
export const SUPPORTED_LANGUAGE_SERVERS: LanguageServerConfig[] = [
{
id: "c",
label: "C",
const LanguageServerConfig: Record<EditorLanguage, LanguageServerMetadata> = {
[EditorLanguage.C]: {
protocol: process.env.NEXT_PUBLIC_LSP_C_PROTOCOL || "ws",
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,
path: process.env.NEXT_PUBLIC_LSP_C_PATH || "/clangd",
lang: EditorLanguageConfig[EditorLanguage.C],
},
{
id: "cpp",
label: "C++",
[EditorLanguage.CPP]: {
protocol: process.env.NEXT_PUBLIC_LSP_CPP_PROTOCOL || "ws",
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,
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> = {
c: `/**
export const TEMP_DEFAULT_EDITOR_VALUE: Record<EditorLanguage, string> = {
[EditorLanguage.C]: `/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
}`,
cpp: `class Solution {
[EditorLanguage.CPP]: `class Solution {
public:
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 { 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;
async function initializeHighlighter() {
highlighter = await createHighlighter({
themes: ["github-light-default", "github-dark-default"],
langs: ["c"],
});
try {
highlighter = await createHighlighter({
themes: themes, // Use all values from the Theme enum
langs: languages, // Use all values from the ProgrammingLanguage enum
});
} catch (error) {
console.error("Error initializing highlighter:", error);
}
}
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[]) {
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 { type editor } from "monaco-editor";
import { persist } from "zustand/middleware";
import { JudgeResult } from "@/config/judge";
import { CODE_EDITOR_OPTIONS } from "@/constants/option";
import { SupportedLanguage } from "@/constants/language";
import { MonacoLanguageClient } from "monaco-languageclient";
import { DEFAULT_EDITOR_LANGUAGE } from "@/config/editor/language";
import { getPath } from "@/lib/utils";
import type { editor } from "monaco-editor";
import { JudgeResultMetadata } from "@/types/judge";
import { EditorLanguage } from "@/types/editor-language";
import { createJSONStorage, persist } from "zustand/middleware";
import { LanguageServerMetadata } from "@/types/language-server";
import { DefaultEditorOptionConfig } from "@/config/editor-option";
import { DefaultEditorLanguageConfig } from "@/config/editor-language";
interface CodeEditorState {
editor: editor.IStandaloneCodeEditor | null;
language: SupportedLanguage;
languageClient: MonacoLanguageClient | null;
hydrated: boolean;
result: JudgeResult | null;
setEditor: (editor: editor.IStandaloneCodeEditor | null) => void;
setLanguage: (language: SupportedLanguage) => void;
setLanguageClient: (languageClient: MonacoLanguageClient | null) => void;
language: EditorLanguage;
path: string;
value: string;
lspConfig: LanguageServerMetadata | null;
isLspEnabled: boolean;
editorConfig: editor.IEditorConstructionOptions;
editor: editor.IStandaloneCodeEditor | null;
result: JudgeResultMetadata | null;
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>()(
persist(
(set) => ({
editor: null,
language: DEFAULT_EDITOR_LANGUAGE,
languageClient: null,
hydrated: false,
language: DefaultEditorLanguageConfig.id,
path: getPath(DefaultEditorLanguageConfig.id),
value: "#include<stdio.h>",
lspConfig: null,
isLspEnabled: true,
editorConfig: DefaultEditorOptionConfig,
editor: null,
result: null,
setEditor: (editor) => set({ editor }),
setLanguage: (language) => set({ language }),
setLanguageClient: (languageClient) => set({ languageClient }),
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 }),
}),
{
name: "code-editor-language",
name: "code-editor-store",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
language: state.language,
path: state.path,
value: state.value,
isLspEnabled: state.isLspEnabled,
editorConfig: state.editorConfig,
}),
onRehydrateStorage: () => (state, error) => {
if (error) {
console.error("hydrate error", error);
} else if (state) {
state.setHydrated(true);
}
onRehydrateStorage: () => {
return (state, error) => {
if (error) {
console.error("An error happened during hydration", error);
} else if (state) {
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;
};