Compare commits

..

4 Commits

35 changed files with 549 additions and 534 deletions

View File

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

View File

@ -1,49 +0,0 @@
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 ] || [ -f bun.lockb ]; then \ elif [ -f bun.lock ]; 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,6 +25,7 @@ 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
@ -40,7 +41,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 ] || [ -f bun.lockb ]; then \ elif [ -f bun.lock ]; 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" && \
@ -60,6 +61,9 @@ 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
@ -67,7 +71,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 root USER nextjs
EXPOSE 3000 EXPOSE 3000

74
Dockerfile.cn Normal file
View File

@ -0,0 +1,74 @@
# 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,46 +26,29 @@ 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 the repository # Clone 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 the application with pre-built images # Build and launch containers
docker compose up -d docker compose up -d --build
``` ```
### 🔨 Local Manual Build ### 🔨 Manual Installation
Build the images locally and start the containers:
```sh ```sh
# Clone the repository # Clone 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 start containers using the local configuration # Start core LSP containers
docker compose -f compose.local.yml up -d --build docker compose up -d --build lsp-c lsp-cpp
```
### 🛠 Development Mode # Install dependencies (using Bun)
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
``` ```
@ -114,12 +97,10 @@ 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"
``` ```

View File

@ -16,6 +16,7 @@
"@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@types/vscode": "^1.97.0", "@types/vscode": "^1.97.0",
"bun": "^1.2.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"devicons-react": "^1.4.0", "devicons-react": "^1.4.0",
@ -198,6 +199,28 @@
"@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=="], "@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=="], "@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=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
@ -448,6 +471,8 @@
"buildcheck": ["buildcheck@0.0.6", "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.6.tgz", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], "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=="], "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=="], "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: monaco-editor-lsp-next:
build: build:
context: ./ context: ./
dockerfile: Dockerfile dockerfile: Dockerfile.cn
image: monaco-editor-lsp-next:latest image: monaco-editor-lsp-next:latest
container_name: monaco-editor-lsp-next container_name: monaco-editor-lsp-next
restart: always restart: always
@ -19,13 +19,14 @@ 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: build:
context: ./docker/lsp/clangd context: ./docker/lsp/clangd
dockerfile: Dockerfile dockerfile: Dockerfile.cn
image: lsp-c:latest image: lsp-c:latest
container_name: lsp-c container_name: lsp-c
restart: always restart: always
@ -42,7 +43,7 @@ services:
lsp-cpp: lsp-cpp:
build: build:
context: ./docker/lsp/clangd context: ./docker/lsp/clangd
dockerfile: Dockerfile dockerfile: Dockerfile.cn
image: lsp-cpp:latest image: lsp-cpp:latest
container_name: lsp-cpp container_name: lsp-cpp
restart: always restart: always

View File

@ -1,6 +1,9 @@
services: services:
monaco-editor-lsp-next: monaco-editor-lsp-next:
image: cfngc4594/monaco-editor-lsp-next:latest build:
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:
@ -16,11 +19,15 @@ 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:
image: cfngc4594/lsp-c:latest build:
context: ./docker/lsp/clangd
dockerfile: Dockerfile
image: lsp-c:latest
container_name: lsp-c container_name: lsp-c
restart: always restart: always
ports: ports:
@ -34,7 +41,10 @@ services:
retries: 5 retries: 5
lsp-cpp: lsp-cpp:
image: cfngc4594/lsp-cpp:latest build:
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: 243 KiB

After

Width:  |  Height:  |  Size: 256 KiB

View File

@ -0,0 +1,31 @@
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,6 +21,7 @@
"@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@types/vscode": "^1.97.0", "@types/vscode": "^1.97.0",
"bun": "^1.2.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"devicons-react": "^1.4.0", "devicons-react": "^1.4.0",

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,37 +7,28 @@ 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, setPath, setLspConfig } = useCodeEditorStore(); const { hydrated, language, setLanguage } = 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={handleValueChange}> <Select value={language} onValueChange={setLanguage}>
<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">
{Object.values(EditorLanguageConfig).map((langConfig) => ( {SUPPORTED_LANGUAGES.map((lang) => (
<SelectItem key={langConfig.id} value={langConfig.id}> <SelectItem key={lang.id} value={lang.id}>
<langConfig.icon size={16} aria-hidden="true" /> {lang.icon}
<span className="truncate text-sm font-semibold mr-2"> <span className="truncate text-sm font-semibold mr-2">
{langConfig.label} {lang.label}
</span> </span>
</SelectItem> </SelectItem>
))} ))}

View File

@ -1,18 +1,19 @@
"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 { RotateCcw } from "lucide-react"; import { DEFAULT_EDITOR_VALUE } from "@/config/editor/value";
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>
@ -23,7 +24,7 @@ export default function ResetButton() {
aria-label="Reset Code" aria-label="Reset Code"
onClick={() => { onClick={() => {
if (editor) { if (editor) {
const value = TEMP_DEFAULT_EDITOR_VALUE[language]; const value = 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,9 +3,7 @@
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 { JudgeConfig } from "@/config/judge"; import { ExitCode, JudgeResult, LanguageConfigs } 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" });
@ -43,11 +41,10 @@ function createTarStream(file: string, value: string) {
} }
export async function judge( export async function judge(
language: EditorLanguage, language: string,
value: string value: string
): Promise<JudgeResultMetadata> { ): Promise<JudgeResult> {
const { fileName, fileExtension } = JudgeConfig[language].editorLanguageMetadata; const { fileName, fileExtension, image, tag, workingDir, memoryLimit, timeLimit, compileOutputLimit, runOutputLimit } = LanguageConfigs[language];
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;
@ -73,14 +70,14 @@ export async function judge(
} }
} }
async function compile(container: Docker.Container, file: string, fileName: string, maxOutput: number = 1 * 1024 * 1024): Promise<JudgeResultMetadata> { async function compile(container: Docker.Container, file: string, fileName: string, maxOutput: number = 1 * 1024 * 1024): Promise<JudgeResult> {
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<JudgeResultMetadata>((resolve, reject) => { return new Promise<JudgeResult>((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 });
@ -129,7 +126,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: JudgeResultMetadata; let result: JudgeResult;
if (exitCode !== 0 || stderr) { if (exitCode !== 0 || stderr) {
result = { output: stderr || "Compilation Error", exitCode: ExitCode.CE }; result = { output: stderr || "Compilation Error", exitCode: ExitCode.CE };
@ -148,14 +145,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<JudgeResultMetadata> { async function run(container: Docker.Container, fileName: string, timeLimit?: number, maxOutput: number = 1 * 1024 * 1024): Promise<JudgeResult> {
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<JudgeResultMetadata>((resolve, reject) => { return new Promise<JudgeResult>((resolve, reject) => {
const stdoutChunks: string[] = []; const stdoutChunks: string[] = [];
let stdoutLength = 0; let stdoutLength = 0;
const stdoutStream = new Writable({ const stdoutStream = new Writable({
@ -214,7 +211,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: JudgeResultMetadata; let result: JudgeResult;
// Exit code 0 means successful execution // Exit code 0 means successful execution
if (exitCode === 0) { if (exitCode === 0) {

View File

@ -1,127 +1,240 @@
"use client"; "use client";
import {
toSocket,
WebSocketMessageReader,
WebSocketMessageWriter,
} from "vscode-ws-jsonrpc";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Skeleton } from "./ui/skeleton"; import { useTheme } from "next-themes";
import normalizeUrl from "normalize-url";
import { highlighter } from "@/lib/shiki"; import { highlighter } from "@/lib/shiki";
import type { editor } from "monaco-editor"; import { useEffect, useRef } from "react";
import { shikiToMonaco } from "@shikijs/monaco"; import { shikiToMonaco } from "@shikijs/monaco";
import type { Monaco } from "@monaco-editor/react"; import { Skeleton } from "@/components/ui/skeleton";
import { useCallback, useEffect, useRef } from "react"; import { CODE_EDITOR_OPTIONS } from "@/constants/option";
import { useMonacoTheme } from "@/hooks/use-monaco-theme"; import { DEFAULT_EDITOR_PATH } from "@/config/editor/path";
import LanguageServerConfig from "@/config/language-server"; import { DEFAULT_EDITOR_VALUE } from "@/config/editor/value";
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: () => <CodeEditorLoadingSkeleton />, loading: () => (
<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 { const { resolvedTheme } = useTheme();
hydrated, const connectionRef = useRef<ConnectionHandle>({
language, client: null,
path, socket: null,
value, controller: new AbortController(),
editorConfig, });
isLspEnabled, const { fontSize, lineHeight } = useCodeEditorOptionStore();
setEditor, const { language, setEditor } = useCodeEditorStore();
} = useCodeEditorStore();
const { monacoTheme } = useMonacoTheme();
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const monacoLanguageClientRef = useRef<MonacoLanguageClient | null>(null);
// Connect to LSP only if enabled useEffect(() => {
const connectLSP = useCallback(async () => { const currentHandle: ConnectionHandle = {
if (!(isLspEnabled && language && editorRef.current)) return; client: null,
socket: null,
controller: new AbortController(),
};
const signal = currentHandle.controller.signal;
connectionRef.current = currentHandle;
const lspConfig = LanguageServerConfig[language]; const cleanupConnection = async (handle: ConnectionHandle) => {
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 {
const monacoLanguageClient = await connectToLanguageServer( // Cleanup Language Client
lspConfig.protocol, if (handle.client) {
lspConfig.hostname, console.log("Stopping language client...");
lspConfig.port, await handle.client.stop(250).catch(() => { });
lspConfig.path, handle.client.dispose();
lspConfig.lang }
); } catch (e) {
monacoLanguageClientRef.current = monacoLanguageClient; console.log("Client cleanup error:", e);
} catch (error) { } finally {
console.error("Failed to connect to LSP:", error); handle.client = null;
} }
}, [isLspEnabled, language]);
// Connect to LSP once the editor has mounted // Cleanup WebSocket
const handleEditorDidMount = useCallback( if (handle.socket) {
async (editor: editor.IStandaloneCodeEditor) => { console.log("Closing WebSocket...");
editorRef.current = editor; const socket = handle.socket;
await connectLSP(); socket.onopen = null;
setEditor(editor); socket.onerror = null;
}, socket.onclose = null;
[connectLSP, setEditor] socket.onmessage = null;
);
// Reconnect to the LSP whenever language or lspConfig changes try {
useEffect(() => { if (
connectLSP(); [WebSocket.OPEN, WebSocket.CONNECTING].includes(
}, [connectLSP]); socket.readyState as WebSocket["OPEN"] | WebSocket["CONNECTING"]
)
// Cleanup the LSP connection when the component unmounts ) {
useEffect(() => { socket.close(1000, "Connection replaced");
return () => { }
if (monacoLanguageClientRef.current) { } catch (e) {
monacoLanguageClientRef.current.stop(); console.log("Socket close error:", e);
monacoLanguageClientRef.current = null; } finally {
handle.socket = null;
}
} }
}; };
}, []);
if (!hydrated) { const initialize = async () => {
return <CodeEditorLoadingSkeleton />; 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;
} }
function handleEditorWillMount(monaco: Monaco) { // Initialize Language Client
shikiToMonaco(highlighter, monaco); 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 ?? "");
}
}
const editorValue =
typeof window !== "undefined"
? localStorage.getItem(`code-editor-value-${language}`) ||
DEFAULT_EDITOR_VALUE[language]
: DEFAULT_EDITOR_VALUE[language];
return ( return (
<Editor <Editor
language={language} defaultLanguage={language}
theme={monacoTheme.id} value={editorValue}
path={path} path={DEFAULT_EDITOR_PATH[language]}
value={value} theme={resolvedTheme === "light" ? "github-light-default" : "github-dark-default"}
beforeMount={handleEditorWillMount} className="h-full"
onMount={handleEditorDidMount} options={mergeOptions}
options={editorConfig} beforeMount={(monaco) => {
loading={<CodeEditorLoadingSkeleton />} shikiToMonaco(highlighter, monaco);
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

@ -1,25 +0,0 @@
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

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

View File

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

View File

@ -1,13 +1,13 @@
import { EditorLanguage } from "@/types/editor-language"; import { SupportedLanguage } from "@/constants/language";
export const TEMP_DEFAULT_EDITOR_VALUE: Record<EditorLanguage, string> = { export const DEFAULT_EDITOR_VALUE: Record<SupportedLanguage, string> = {
[EditorLanguage.C]: `/** 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) {
}`, }`,
[EditorLanguage.CPP]: `class Solution { cpp: `class Solution {
public: public:
vector<int> twoSum(vector<int>& nums, int target) { vector<int> twoSum(vector<int>& nums, int target) {

View File

@ -1,9 +1,42 @@
import { EditorLanguage } from "@/types/editor-language"; // Result type definitions
import { EditorLanguageConfig } from "./editor-language"; export enum ExitCode {
import { DockerMetadata, JudgeMetadata } from "@/types/judge"; 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 const DockerConfig: Record<EditorLanguage, DockerMetadata> = { export type JudgeResult = {
[EditorLanguage.C]: { 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",
image: "gcc", image: "gcc",
tag: "latest", tag: "latest",
workingDir: "/src", workingDir: "/src",
@ -12,7 +45,11 @@ export const DockerConfig: Record<EditorLanguage, DockerMetadata> = {
compileOutputLimit: 1 * 1024 * 1024, compileOutputLimit: 1 * 1024 * 1024,
runOutputLimit: 1 * 1024 * 1024, runOutputLimit: 1 * 1024 * 1024,
}, },
[EditorLanguage.CPP]: { cpp: {
id: "cpp",
label: "C++",
fileName: "main",
fileExtension: "cpp",
image: "gcc", image: "gcc",
tag: "latest", tag: "latest",
workingDir: "/src", workingDir: "/src",
@ -20,16 +57,5 @@ export const DockerConfig: Record<EditorLanguage, DockerMetadata> = {
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,22 +1,29 @@
import { EditorLanguage } from "@/types/editor-language"; import { SupportedLanguage } from '@/constants/language'
import { EditorLanguageConfig } from "./editor-language";
import { LanguageServerMetadata } from "@/types/language-server";
const LanguageServerConfig: Record<EditorLanguage, LanguageServerMetadata> = { export interface LanguageServerConfig {
[EditorLanguage.C]: { id: SupportedLanguage
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

@ -1,15 +0,0 @@
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

@ -0,0 +1,16 @@
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

@ -1,6 +1,6 @@
import { type editor } from "monaco-editor"; import { type editor } from "monaco-editor";
export const DefaultEditorOptionConfig: editor.IEditorConstructionOptions = { export const CODE_EDITOR_OPTIONS: editor.IEditorConstructionOptions = {
autoIndent: "full", autoIndent: "full",
automaticLayout: true, automaticLayout: true,
contextmenu: true, contextmenu: true,
@ -15,13 +15,16 @@ export const DefaultEditorOptionConfig: 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,13 +0,0 @@
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

@ -1,68 +0,0 @@
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,23 +1,12 @@
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: themes, // Use all values from the Theme enum themes: ["github-light-default", "github-dark-default"],
langs: languages, // Use all values from the ProgrammingLanguage enum langs: ["c"],
}); });
} catch (error) {
console.error("Error initializing highlighter:", error);
}
} }
initializeHighlighter(); initializeHighlighter();

View File

@ -1,13 +1,6 @@
import { twMerge } from "tailwind-merge"; import { clsx, type ClassValue } from "clsx"
import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"
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,75 +1,58 @@
import { create } from "zustand"; import { create } from "zustand";
import { getPath } from "@/lib/utils"; import { type editor } from "monaco-editor";
import type { editor } from "monaco-editor"; import { persist } from "zustand/middleware";
import { JudgeResultMetadata } from "@/types/judge"; import { JudgeResult } from "@/config/judge";
import { EditorLanguage } from "@/types/editor-language"; import { CODE_EDITOR_OPTIONS } from "@/constants/option";
import { createJSONStorage, persist } from "zustand/middleware"; import { SupportedLanguage } from "@/constants/language";
import { LanguageServerMetadata } from "@/types/language-server"; import { MonacoLanguageClient } from "monaco-languageclient";
import { DefaultEditorOptionConfig } from "@/config/editor-option"; import { DEFAULT_EDITOR_LANGUAGE } from "@/config/editor/language";
import { DefaultEditorLanguageConfig } from "@/config/editor-language";
interface CodeEditorState { interface CodeEditorState {
hydrated: boolean;
language: EditorLanguage;
path: string;
value: string;
lspConfig: LanguageServerMetadata | null;
isLspEnabled: boolean;
editorConfig: editor.IEditorConstructionOptions;
editor: editor.IStandaloneCodeEditor | null; editor: editor.IStandaloneCodeEditor | null;
result: JudgeResultMetadata | 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;
setHydrated: (value: boolean) => void; setHydrated: (value: boolean) => void;
setLanguage: (language: EditorLanguage) => void; setResult: (result: JudgeResult) => 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) => ({
hydrated: false,
language: DefaultEditorLanguageConfig.id,
path: getPath(DefaultEditorLanguageConfig.id),
value: "#include<stdio.h>",
lspConfig: null,
isLspEnabled: true,
editorConfig: DefaultEditorOptionConfig,
editor: null, editor: null,
language: DEFAULT_EDITOR_LANGUAGE,
languageClient: null,
hydrated: false,
result: null, result: null,
setHydrated: (value) => set({ hydrated: value }), setEditor: (editor) => set({ editor }),
setLanguage: (language) => set({ language }), setLanguage: (language) => set({ language }),
setPath: (path) => set({ path }), setLanguageClient: (languageClient) => set({ languageClient }),
setValue: (value) => set({ value }), setHydrated: (value) => set({ hydrated: 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-store", name: "code-editor-language",
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: () => { onRehydrateStorage: () => (state, error) => {
return (state, error) => {
if (error) { if (error) {
console.error("An error happened during hydration", error); console.error("hydrate error", 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

@ -1,12 +0,0 @@
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 }>;
};

View File

@ -1,35 +0,0 @@
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

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

View File

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