From 6802005f8c1143930171504bb08b852b976488e7 Mon Sep 17 00:00:00 2001 From: funnyboy-roks Date: Fri, 10 Oct 2025 08:41:06 -0500 Subject: [PATCH 1/4] switch to codemirror --- client/app/competitor/page.tsx | 8 +- client/components/CodeViewer.tsx | 57 ++---- client/components/Editor.tsx | 53 ++---- client/lib/competitor-state.ts | 3 +- client/lib/editor/lang.ts | 287 ++++++++++++++++++++++++++++++ client/lib/services/testing.ts | 4 +- client/lib/types.ts | 7 +- client/package-lock.json | 289 ++++++++++++++++++++++++++----- client/package.json | 4 +- 9 files changed, 576 insertions(+), 136 deletions(-) create mode 100644 client/lib/editor/lang.ts diff --git a/client/app/competitor/page.tsx b/client/app/competitor/page.tsx index d7fd8a9..811424e 100644 --- a/client/app/competitor/page.tsx +++ b/client/app/competitor/page.tsx @@ -59,8 +59,8 @@ const EditorButtons = () => { // Defaults to first language if no language selected useEffect(() => { - if (currQuestion?.languages?.length) { - setSelectedLanguage((prev) => prev ?? currQuestion?.languages?.[0]?.name); + if (currQuestion?.languages.length) { + setSelectedLanguage((prev) => prev ?? currQuestion.languages[0]); } }, [currQuestion, setSelectedLanguage]); @@ -173,12 +173,12 @@ const EditorButtons = () => { - setSelectedLanguage(currQuestion.languages.find(l => l.name === v) ?? null)}> - {currQuestion?.languages?.map((l) => ( + {currQuestion?.languages.map((l) => ( {l.name} diff --git a/client/components/CodeViewer.tsx b/client/components/CodeViewer.tsx index 93209ae..3cca649 100644 --- a/client/components/CodeViewer.tsx +++ b/client/components/CodeViewer.tsx @@ -1,53 +1,26 @@ -import React, { useEffect, useState } from 'react'; -import AceEditor from 'react-ace'; +import React from 'react'; +import { StreamLanguage } from '@codemirror/language'; +import CodeMirror from '@uiw/react-codemirror'; import { useAtom } from 'jotai'; import { editorSettingsAtom } from '@/lib/competitor-state'; -import 'ace-builds/src-noconflict/theme-monokai'; -import('ace-builds/src-noconflict/mode-javascript'); -import 'ace-builds/src-noconflict/keybinding-vim'; -import 'ace-builds/src-noconflict/keybinding-emacs'; -import 'ace-builds/src-noconflict/keybinding-sublime'; -import 'ace-builds/src-noconflict/ext-language_tools'; +import { vim } from '@replit/codemirror-vim'; +import { cn } from '@/lib/utils'; +import langs, { type LanguageSyntax } from '@/lib/editor/lang'; -export const CodeViewer = ({ code, className = '' }: { code: string; className?: string }) => { +export const CodeViewer = ({ code, language = 'javascript', className = '' }: { code: string; language?: LanguageSyntax; className?: string }) => { const [editorSettings] = useAtom(editorSettingsAtom); - const [editorTheme, setEditorTheme] = useState(editorSettings.theme); - - useEffect(() => { - (async () => { - await import(`ace-builds/src-noconflict/theme-${editorSettings.theme}`); - setEditorTheme(editorSettings.theme); - - if (editorSettings.keybind !== 'ace') { - await import(`ace-builds/src-noconflict/keybinding-${editorSettings.keybind}`); - } - })(); - }, [editorSettings]); return ( - ); }; diff --git a/client/components/Editor.tsx b/client/components/Editor.tsx index 718980e..334d290 100644 --- a/client/components/Editor.tsx +++ b/client/components/Editor.tsx @@ -1,62 +1,35 @@ import React, { useEffect, useState } from 'react'; -import AceEditor from 'react-ace'; +import CodeMirror from '@uiw/react-codemirror'; +import { StreamLanguage } from '@codemirror/language'; import { useAtom } from 'jotai'; import { editorContentAtom, editorSettingsAtom, selectedLanguageAtom, } from '@/lib/competitor-state'; -import 'ace-builds/esm-resolver'; -import 'ace-builds/src-noconflict/theme-monokai'; -import 'ace-builds/src-noconflict/ext-language_tools'; import { currQuestionAtom } from '@/lib/services/questions'; +import { vim } from "@replit/codemirror-vim" +import langs from '@/lib/editor/lang'; export default function CodeEditor() { const [editorContent, setEditorContent] = useAtom(editorContentAtom); const [editorSettings] = useAtom(editorSettingsAtom); - const [languageValue] = useAtom(selectedLanguageAtom); - const [editorTheme, setEditorTheme] = useState(editorSettings.theme); + const [language] = useAtom(selectedLanguageAtom); const [question] = useAtom(currQuestionAtom); - useEffect(() => { - (async () => { - await import(`ace-builds/src-noconflict/theme-${editorSettings.theme}`); - setEditorTheme(editorSettings.theme); - - if (editorSettings.keybind !== 'ace') { - await import(`ace-builds/src-noconflict/keybinding-${editorSettings.keybind}`); - } - if (languageValue) { - await import( - `ace-builds/src-noconflict/mode-${question?.languages?.find((l) => l.name === languageValue)?.syntax || 'plaintext'}` - ); - } - })(); - }, [editorSettings, languageValue, question]); + const lang = language ? langs[language.syntax] ?? langs.java : langs.java; return ( - l.name === languageValue)?.syntax || 'plaintext'} - theme={editorTheme} - name="code-editor" - editorProps={{ $blockScrolling: true }} - width="100%" + ); } diff --git a/client/lib/competitor-state.ts b/client/lib/competitor-state.ts index ad8d677..9d52140 100644 --- a/client/lib/competitor-state.ts +++ b/client/lib/competitor-state.ts @@ -1,6 +1,7 @@ import { atomWithStorage } from 'jotai/utils'; import { atom } from 'jotai'; import { currQuestionIdxAtom } from './services/questions'; +import { Language } from './types'; export interface EditorSettings { theme: string; @@ -55,7 +56,7 @@ export const editorContentAtom = atom( } ); -export const selectedLanguageAtom = atom(); +export const selectedLanguageAtom = atom(null); export interface TestResult { state: 'pass' | 'fail'; diff --git a/client/lib/editor/lang.ts b/client/lib/editor/lang.ts new file mode 100644 index 0000000..c0757e7 --- /dev/null +++ b/client/lib/editor/lang.ts @@ -0,0 +1,287 @@ +// imports generated using: +// grep 'export declare const .*' mode/*.d.ts | tr '/ :' ' ' | awk -F' ' 'BEGIN { last="";curr="" } { if (last == "") last = $2; if (last == $2) if (curr == "") curr = $6; else curr = curr ", " $6; else { split(last, arr, "."); print "import { " curr " } from \'@codemirror/legacy-modes/mode/" arr[1] "\';"; curr = $6 }; last = $2 } END { split(last, arr, "."); print "import { " curr " } from \'@codemirror/legacy-modes/mode/" arr[1] "\';"; }' +import { apl } from '@codemirror/legacy-modes/mode/apl'; +import { asciiArmor } from '@codemirror/legacy-modes/mode/asciiarmor'; +import { asterisk } from '@codemirror/legacy-modes/mode/asterisk'; +import { brainfuck } from '@codemirror/legacy-modes/mode/brainfuck'; +import { c, cpp, java, csharp, scala, kotlin, shader, nesC, objectiveC, objectiveCpp, squirrel, ceylon, dart } from '@codemirror/legacy-modes/mode/clike'; +import { clojure } from '@codemirror/legacy-modes/mode/clojure'; +import { cmake } from '@codemirror/legacy-modes/mode/cmake'; +import { cobol } from '@codemirror/legacy-modes/mode/cobol'; +import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript'; +import { commonLisp } from '@codemirror/legacy-modes/mode/commonlisp'; +import { crystal } from '@codemirror/legacy-modes/mode/crystal'; +import { css, sCSS, less, gss } from '@codemirror/legacy-modes/mode/css'; +import { cypher } from '@codemirror/legacy-modes/mode/cypher'; +import { d } from '@codemirror/legacy-modes/mode/d'; +import { diff } from '@codemirror/legacy-modes/mode/diff'; +import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'; +import { dtd } from '@codemirror/legacy-modes/mode/dtd'; +import { dylan } from '@codemirror/legacy-modes/mode/dylan'; +import { ebnf } from '@codemirror/legacy-modes/mode/ebnf'; +import { ecl } from '@codemirror/legacy-modes/mode/ecl'; +import { eiffel } from '@codemirror/legacy-modes/mode/eiffel'; +import { elm } from '@codemirror/legacy-modes/mode/elm'; +import { erlang } from '@codemirror/legacy-modes/mode/erlang'; +import { factor } from '@codemirror/legacy-modes/mode/factor'; +import { fcl } from '@codemirror/legacy-modes/mode/fcl'; +import { forth } from '@codemirror/legacy-modes/mode/forth'; +import { fortran } from '@codemirror/legacy-modes/mode/fortran'; +import { gas, gasArm } from '@codemirror/legacy-modes/mode/gas'; +import { gherkin } from '@codemirror/legacy-modes/mode/gherkin'; +import { go } from '@codemirror/legacy-modes/mode/go'; +import { groovy } from '@codemirror/legacy-modes/mode/groovy'; +import { haskell } from '@codemirror/legacy-modes/mode/haskell'; +import { haxe, hxml } from '@codemirror/legacy-modes/mode/haxe'; +import { http } from '@codemirror/legacy-modes/mode/http'; +import { idl } from '@codemirror/legacy-modes/mode/idl'; +import { javascript, json, jsonld, typescript } from '@codemirror/legacy-modes/mode/javascript'; +import { jinja2 } from '@codemirror/legacy-modes/mode/jinja2'; +import { julia } from '@codemirror/legacy-modes/mode/julia'; +import { liveScript } from '@codemirror/legacy-modes/mode/livescript'; +import { lua } from '@codemirror/legacy-modes/mode/lua'; +import { mathematica } from '@codemirror/legacy-modes/mode/mathematica'; +import { mbox } from '@codemirror/legacy-modes/mode/mbox'; +import { mirc } from '@codemirror/legacy-modes/mode/mirc'; +import { oCaml, fSharp, sml } from '@codemirror/legacy-modes/mode/mllike'; +import { modelica } from '@codemirror/legacy-modes/mode/modelica'; +import { mscgen, msgenny, xu } from '@codemirror/legacy-modes/mode/mscgen'; +import { mumps } from '@codemirror/legacy-modes/mode/mumps'; +import { nginx } from '@codemirror/legacy-modes/mode/nginx'; +import { nsis } from '@codemirror/legacy-modes/mode/nsis'; +import { ntriples } from '@codemirror/legacy-modes/mode/ntriples'; +import { octave } from '@codemirror/legacy-modes/mode/octave'; +import { oz } from '@codemirror/legacy-modes/mode/oz'; +import { pascal } from '@codemirror/legacy-modes/mode/pascal'; +import { pegjs } from '@codemirror/legacy-modes/mode/pegjs'; +import { perl } from '@codemirror/legacy-modes/mode/perl'; +import { pig } from '@codemirror/legacy-modes/mode/pig'; +import { powerShell } from '@codemirror/legacy-modes/mode/powershell'; +import { properties } from '@codemirror/legacy-modes/mode/properties'; +import { protobuf } from '@codemirror/legacy-modes/mode/protobuf'; +import { pug } from '@codemirror/legacy-modes/mode/pug'; +import { puppet } from '@codemirror/legacy-modes/mode/puppet'; +import { python, cython } from '@codemirror/legacy-modes/mode/python'; +import { q } from '@codemirror/legacy-modes/mode/q'; +import { r } from '@codemirror/legacy-modes/mode/r'; +import { rpmChanges, rpmSpec } from '@codemirror/legacy-modes/mode/rpm'; +import { ruby } from '@codemirror/legacy-modes/mode/ruby'; +import { rust } from '@codemirror/legacy-modes/mode/rust'; +import { sas } from '@codemirror/legacy-modes/mode/sas'; +import { sass } from '@codemirror/legacy-modes/mode/sass'; +import { scheme } from '@codemirror/legacy-modes/mode/scheme'; +import { shell } from '@codemirror/legacy-modes/mode/shell'; +import { sieve } from '@codemirror/legacy-modes/mode/sieve'; +import { smalltalk } from '@codemirror/legacy-modes/mode/smalltalk'; +import { solr } from '@codemirror/legacy-modes/mode/solr'; +import { sparql } from '@codemirror/legacy-modes/mode/sparql'; +import { spreadsheet } from '@codemirror/legacy-modes/mode/spreadsheet'; +import { standardSQL, msSQL, mySQL, mariaDB, sqlite, cassandra, plSQL, hive, pgSQL, gql, gpSQL, sparkSQL, esper } from '@codemirror/legacy-modes/mode/sql'; +import { stex, stexMath } from '@codemirror/legacy-modes/mode/stex'; +import { stylus } from '@codemirror/legacy-modes/mode/stylus'; +import { swift } from '@codemirror/legacy-modes/mode/swift'; +import { tcl } from '@codemirror/legacy-modes/mode/tcl'; +import { textile } from '@codemirror/legacy-modes/mode/textile'; +import { tiddlyWiki } from '@codemirror/legacy-modes/mode/tiddlywiki'; +import { tiki } from '@codemirror/legacy-modes/mode/tiki'; +import { toml } from '@codemirror/legacy-modes/mode/toml'; +import { troff } from '@codemirror/legacy-modes/mode/troff'; +import { ttcnCfg } from '@codemirror/legacy-modes/mode/ttcn-cfg'; +import { ttcn } from '@codemirror/legacy-modes/mode/ttcn'; +import { turtle } from '@codemirror/legacy-modes/mode/turtle'; +import { vb } from '@codemirror/legacy-modes/mode/vb'; +import { vbScript, vbScriptASP } from '@codemirror/legacy-modes/mode/vbscript'; +import { velocity } from '@codemirror/legacy-modes/mode/velocity'; +import { verilog, tlv } from '@codemirror/legacy-modes/mode/verilog'; +import { vhdl } from '@codemirror/legacy-modes/mode/vhdl'; +import { wast } from '@codemirror/legacy-modes/mode/wast'; +import { webIDL } from '@codemirror/legacy-modes/mode/webidl'; +import { xml, html } from '@codemirror/legacy-modes/mode/xml'; +import { xQuery } from '@codemirror/legacy-modes/mode/xquery'; +import { yacas } from '@codemirror/legacy-modes/mode/yacas'; +import { yaml } from '@codemirror/legacy-modes/mode/yaml'; +import { z80, ez80 } from '@codemirror/legacy-modes/mode/z80'; + + + +// Exports generated with: +// grep 'export declare const .*' mode/*.d.ts | tr '/ :' ' ' | awk -F' ' 'BEGIN { print "const langs: Record> = {" } { print " " $6 ","; if (tolower($6) != $6) print " " tolower($6)": "$6"," } END { print "\n};\nexport default langs;" }' +const langs = { + apl, + asciiArmor, + asciiarmor: asciiArmor, + asterisk, + brainfuck, + c, + cpp, + java, + csharp, + scala, + kotlin, + shader, + nesC, + nesc: nesC, + objectiveC, + objectivec: objectiveC, + objectiveCpp, + objectivecpp: objectiveCpp, + squirrel, + ceylon, + dart, + clojure, + cmake, + cobol, + coffeeScript, + coffeescript: coffeeScript, + commonLisp, + commonlisp: commonLisp, + crystal, + css, + sCSS, + scss: sCSS, + less, + gss, + cypher, + d, + diff, + dockerFile, + dockerfile: dockerFile, + dtd, + dylan, + ebnf, + ecl, + eiffel, + elm, + erlang, + factor, + fcl, + forth, + fortran, + gas, + gasArm, + gasarm: gasArm, + gherkin, + go, + groovy, + haskell, + haxe, + hxml, + http, + idl, + javascript, + json, + jsonld, + typescript, + jinja2, + julia, + liveScript, + livescript: liveScript, + lua, + mathematica, + mbox, + mirc, + oCaml, + ocaml: oCaml, + fSharp, + fsharp: fSharp, + sml, + modelica, + mscgen, + msgenny, + xu, + mumps, + nginx, + nsis, + ntriples, + octave, + oz, + pascal, + pegjs, + perl, + pig, + powerShell, + powershell: powerShell, + properties, + protobuf, + pug, + puppet, + python, + cython, + q, + r, + rpmChanges, + rpmchanges: rpmChanges, + rpmSpec, + rpmspec: rpmSpec, + ruby, + rust, + sas, + sass, + scheme, + shell, + sieve, + smalltalk, + solr, + sparql, + spreadsheet, + standardSQL, + standardsql: standardSQL, + msSQL, + mssql: msSQL, + mySQL, + mysql: mySQL, + mariaDB, + mariadb: mariaDB, + sqlite, + cassandra, + plSQL, + plsql: plSQL, + hive, + pgSQL, + pgsql: pgSQL, + gql, + gpSQL, + gpsql: gpSQL, + sparkSQL, + sparksql: sparkSQL, + esper, + stex, + stexMath, + stexmath: stexMath, + stylus, + swift, + tcl, + textile, + tiddlyWiki, + tiddlywiki: tiddlyWiki, + tiki, + toml, + troff, + ttcnCfg, + ttcncfg: ttcnCfg, + ttcn, + turtle, + vb, + vbScript, + vbscript: vbScript, + vbScriptASP, + vbscriptasp: vbScriptASP, + velocity, + verilog, + tlv, + vhdl, + wast, + webIDL, + webidl: webIDL, + xml, + html, + xQuery, + xquery: xQuery, + yacas, + yaml, + z80, + ez80, +}; +export default langs; + +export type LanguageSyntax = keyof typeof langs; diff --git a/client/lib/services/testing.ts b/client/lib/services/testing.ts index a2e0daf..4a6da62 100644 --- a/client/lib/services/testing.ts +++ b/client/lib/services/testing.ts @@ -25,7 +25,7 @@ export const useTesting = () => { try { const { results, failed, passed } = await ws.sendAndWait({ kind: 'run-test', - language: selectedLanguage?.toLowerCase() || 'java', + language: selectedLanguage?.name.toLowerCase() || 'java', problem: currentQuestionIdx, solution: editorContent, }); @@ -42,7 +42,7 @@ export const useTesting = () => { try { const res = await ws.sendAndWait({ kind: 'submit', - language: selectedLanguage?.toLowerCase() || 'java', + language: selectedLanguage?.name.toLowerCase() || 'java', problem: currentQuestionIdx, solution: editorContent, }); diff --git a/client/lib/types.ts b/client/lib/types.ts index 5d3d541..39425f9 100644 --- a/client/lib/types.ts +++ b/client/lib/types.ts @@ -1,3 +1,4 @@ +import { LanguageSyntax } from './editor/lang'; import { User } from './services/auth'; export type TestState = 'pass' | 'fail' | 'in-progress' | 'not-attempted'; @@ -9,7 +10,7 @@ export interface Test { } export interface QuestionResponse { - languages?: Languages[]; + languages: Language[]; title: string; points: number; description?: string; @@ -57,9 +58,9 @@ export interface LeaderboardEntry { submissionStates: TestState[]; } -export interface Languages { +export interface Language { name: string; - syntax: string; + syntax: LanguageSyntax; } export interface Announcement { diff --git a/client/package-lock.json b/client/package-lock.json index 73b2d47..18af198 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "name": "client", "version": "0.1.0", "dependencies": { + "@codemirror/legacy-modes": "^6.5.2", "@faker-js/faker": "^9.8.0", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.2", @@ -29,11 +30,13 @@ "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.8", + "@replit/codemirror-vim": "^6.3.0", "@tanstack/react-query": "^5.71.10", "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.2.1", "@tauri-apps/plugin-fs": "^2.2.1", "@types/diff": "^7.0.2", + "@uiw/react-codemirror": "^4.25.2", "ansi-to-html": "^0.7.2", "class-variance-authority": "^0.7.1", "classnames": "^2.5.1", @@ -46,7 +49,6 @@ "next": "^15.1.6", "next-themes": "^0.4.4", "react": "^19.0.0", - "react-ace": "^14.0.1", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", "react-resizable-panels": "^2.1.7", @@ -82,6 +84,117 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.0.tgz", + "integrity": "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.9.0.tgz", + "integrity": "sha512-454TVgjhO6cMufsyyGN70rGIfJxJEjcqjBG2x2Y03Y/+Fm99d3O/Kv1QDYWuG6hvxsgmjXmBuATikIIYvERX+w==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", + "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.0.tgz", + "integrity": "sha512-wZxW+9XDytH3SKvS8cQzMyQCaaazH8XL1EMHleHe00wVzsv7NBQKVW2yzEHrRhmM7ZOhVdItPbvlRBvMp9ej7A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.5", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.5.tgz", + "integrity": "sha512-SFVsNAgsAoou+BjRewMqN+m9jaztB9wCWN9RSRgePqUbq8UVlvJfku5zB2KVhLPgH/h0RLk38tvd4tGeAhygnw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@emnapi/runtime": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", @@ -855,6 +968,36 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@next/env": { "version": "15.5.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz", @@ -2244,6 +2387,19 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@replit/codemirror-vim": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz", + "integrity": "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==", + "license": "MIT", + "peerDependencies": { + "@codemirror/commands": "6.x.x", + "@codemirror/language": "6.x.x", + "@codemirror/search": "6.x.x", + "@codemirror/state": "6.x.x", + "@codemirror/view": "6.x.x" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2604,11 +2760,58 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/ace-builds": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.39.0.tgz", - "integrity": "sha512-MqoZojv4gpc5QyTMor/dS6kmruDV9db9LVZbCiT4qYz6WsDiv4qyG5f7ZPc+wjUl6oLMqgCAsBjo1whdSVyMlQ==", - "license": "BSD-3-Clause" + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.2.tgz", + "integrity": "sha512-s2fbpdXrSMWEc86moll/d007ZFhu6jzwNu5cWv/2o7egymvLeZO52LWkewgbr+BUCGWGPsoJVWeaejbsb/hLcw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.2.tgz", + "integrity": "sha512-XP3R1xyE0CP6Q0iR0xf3ed+cJzJnfmbLelgJR6osVVtMStGGZP3pGQjjwDRYptmjGHfEELUyyBLdY25h0BQg7w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.2", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } }, "node_modules/acorn": { "version": "8.15.0", @@ -3200,6 +3403,21 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3234,6 +3452,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3431,12 +3655,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", - "license": "Apache-2.0" - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -5162,6 +5380,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -5305,20 +5524,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5330,6 +5535,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -6128,6 +6334,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -6174,23 +6381,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-ace": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-14.0.1.tgz", - "integrity": "sha512-z6YAZ20PNf/FqmYEic//G/UK6uw0rn21g58ASgHJHl9rfE4nITQLqthr9rHMVQK4ezwohJbp2dGrZpkq979PYQ==", - "license": "MIT", - "dependencies": { - "ace-builds": "^1.36.3", - "diff-match-patch": "^1.0.5", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", @@ -6223,6 +6413,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/react-remove-scroll": { @@ -6977,6 +7168,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -7400,6 +7597,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/client/package.json b/client/package.json index f0cbfee..0665e10 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,7 @@ "format:fix": "prettier --write --ignore-path .gitignore ." }, "dependencies": { + "@codemirror/legacy-modes": "^6.5.2", "@faker-js/faker": "^9.8.0", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.2", @@ -32,11 +33,13 @@ "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.8", + "@replit/codemirror-vim": "^6.3.0", "@tanstack/react-query": "^5.71.10", "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.2.1", "@tauri-apps/plugin-fs": "^2.2.1", "@types/diff": "^7.0.2", + "@uiw/react-codemirror": "^4.25.2", "ansi-to-html": "^0.7.2", "class-variance-authority": "^0.7.1", "classnames": "^2.5.1", @@ -49,7 +52,6 @@ "next": "^15.1.6", "next-themes": "^0.4.4", "react": "^19.0.0", - "react-ace": "^14.0.1", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", "react-resizable-panels": "^2.1.7", From b0066a8ae364eca5329ee8fd45ef39c2d5e92c7a Mon Sep 17 00:00:00 2001 From: funnyboy-roks Date: Sat, 11 Oct 2025 06:34:00 -0500 Subject: [PATCH 2/4] it's basically done --- client/components/Editor.tsx | 122 +++++- client/components/Settings.tsx | 389 +++++++---------- client/components/UserMenu.tsx | 12 +- client/lib/competitor-state.ts | 35 +- client/lib/editor/{lang.ts => langs.ts} | 0 client/lib/editor/settings.ts | 45 ++ client/lib/editor/themes.ts | 191 +++++++++ client/lib/types.ts | 2 +- client/package-lock.json | 544 ++++++++++++++++++++++++ client/package.json | 5 + 10 files changed, 1064 insertions(+), 281 deletions(-) rename client/lib/editor/{lang.ts => langs.ts} (100%) create mode 100644 client/lib/editor/settings.ts create mode 100644 client/lib/editor/themes.ts diff --git a/client/components/Editor.tsx b/client/components/Editor.tsx index 334d290..7476a82 100644 --- a/client/components/Editor.tsx +++ b/client/components/Editor.tsx @@ -1,35 +1,129 @@ -import React, { useEffect, useState } from 'react'; -import CodeMirror from '@uiw/react-codemirror'; +import React, { useEffect, useRef, useState } from 'react'; +import CodeMirror, { BasicSetupOptions, Extension, basicSetup, ReactCodeMirrorRef } from '@uiw/react-codemirror'; +import { keymap, lineNumbers } from '@codemirror/view'; +import { search } from "@codemirror/search" import { StreamLanguage } from '@codemirror/language'; +import { indentationMarkers } from '@replit/codemirror-indentation-markers'; +import { showMinimap } from '@replit/codemirror-minimap'; +import { vim } from "@replit/codemirror-vim" +import { emacs } from "@replit/codemirror-emacs" +import { vscodeKeymap } from "@replit/codemirror-vscode-keymap" import { useAtom } from 'jotai'; import { editorContentAtom, editorSettingsAtom, selectedLanguageAtom, } from '@/lib/competitor-state'; -import { currQuestionAtom } from '@/lib/services/questions'; -import { vim } from "@replit/codemirror-vim" -import langs from '@/lib/editor/lang'; +import langs from '@/lib/editor/langs'; +import themes from '@/lib/editor/themes'; + +const relativeLineNumbers = () => lineNumbers({ + formatNumber: (lineNo, state) => { + // Trailing spaces are trimmed, so ' ' + ZWSP + const SPACE = ' ​'; + if (lineNo > state.doc.lines) { + return lineNo + SPACE; + } + const cursorLine = state.doc.lineAt(state.selection.asSingle().ranges[0].to).number; + if (lineNo === cursorLine) { + return cursorLine + SPACE; + } else { + return Math.abs(cursorLine - lineNo).toString(); + } + }, +}); export default function CodeEditor() { const [editorContent, setEditorContent] = useAtom(editorContentAtom); - const [editorSettings] = useAtom(editorSettingsAtom); + const [settings] = useAtom(editorSettingsAtom); const [language] = useAtom(selectedLanguageAtom); - const [question] = useAtom(currQuestionAtom); + const $editor = useRef(null); + const [extensions, setExtensions] = useState([]); + + useEffect(() => { + const basicOptions: BasicSetupOptions = { + allowMultipleSelections: true, + autocompletion: settings.autocompletion, + bracketMatching: settings.highlightMatchingBracket, + closeBrackets: settings.autoCloseBrackets, + closeBracketsKeymap: true, + completionKeymap: true, + defaultKeymap: settings.keymap === 'default', + drawSelection: true, // ? + dropCursor: true, + foldGutter: true, + foldKeymap: false, + highlightActiveLine: settings.highlightActiveLine, + highlightActiveLineGutter: true, + highlightSelectionMatches: settings.highlightSelectionMatches, + highlightSpecialChars: true, + /// undo history + history: true, + historyKeymap: true, + /// configures whether some inputs trigger reindentation of the current line. + indentOnInput: true, + lineNumbers: settings.lineNumbers !== 'off', + lintKeymap: true, + searchKeymap: settings.keymap === 'default', + /// Alt+left mouse to select in a rectangle + rectangularSelection: true, + /// Crosshair cursor for ^^^ + crosshairCursor: true, + syntaxHighlighting: true, + tabSize: settings.tabSize, + }; + + const extensions = [ + ...basicSetup(basicOptions), + search(), // See https://github.com/uiwjs/react-codemirror/issues/404#issuecomment-1809447795 + ]; - const lang = language ? langs[language.syntax] ?? langs.java : langs.java; + const lang = language ? langs[language.syntax] : undefined; + if (lang) + extensions.push(StreamLanguage.define(lang)); + if (settings.lineNumbers === 'relative') + extensions.push(relativeLineNumbers()); + if (settings.indentGuides) + extensions.push(indentationMarkers({ thickness: 3 })); + if (settings.theme) + extensions.push(themes[settings.theme]?.extension ?? themes.red); + if (settings.minimap) { + extensions.push(showMinimap.compute(['doc'], _state => ({ + create: (_v) => ({ dom: document.createElement('div') }), + displayText: 'blocks', + showOverlay: 'mouse-over', + }))); + } + + switch (settings.keymap) { + case 'default': break; + case 'vscode': extensions.push(keymap.of(vscodeKeymap)); break; + case 'vim': extensions.push(vim()); break; + case 'emacs': extensions.push(emacs()); break; + } + + if ($editor.current) { + // TODO: this seems less than ideal + $editor.current.editor?.querySelectorAll('.cm-editor *').forEach(e => { + e.style.fontSize = `${settings.fontSize}px`; + }); + } + console.debug('rebuilt extensions'); + + setExtensions(extensions); + }, [language, settings, $editor]); return ( ); } diff --git a/client/components/Settings.tsx b/client/components/Settings.tsx index ae3227c..2f60cc7 100644 --- a/client/components/Settings.tsx +++ b/client/components/Settings.tsx @@ -11,77 +11,36 @@ import { } from '@/components/ui/select'; import { useState } from 'react'; import { useAtom } from 'jotai'; -import { EditorSettings, editorSettingsAtom } from '@/lib/competitor-state'; +import { editorSettingsAtom } from '@/lib/competitor-state'; import { Label } from './ui/label'; +import { EditorSettings, Keymap, LineNumbers } from '@/lib/editor/settings'; +import themes, { Theme } from '@/lib/editor/themes'; +import { ScrollArea } from './ui/scroll-area'; -const EDITOR_OPTIONS = [ - { id: 'highlightActiveLine', label: 'Highlight Active Line' }, - { id: 'useSoftTabs', label: 'Enable Soft Tabs' }, - { id: 'relativeLineNumbers', label: 'Relative Line Numbers' }, - { id: 'showGutter', label: 'Show Gutter' }, - { id: 'displayIndentGuides', label: 'Show Indent Guides' }, - { id: 'enableBasicAutocompletion', label: 'Enable Autocompletion' }, - { id: 'enableLiveAutocompletion', label: 'Enable Live Autocompletion' }, -] as const; +type Option = { id: K; label: string; description?: string; }; -const THEMES = [ - { id: 'ambiance', label: 'Ambiance' }, - { id: 'chaos', label: 'Chaos' }, - { id: 'chrome', label: 'Chrome' }, - { id: 'cloud9_day', label: 'Cloud9 Day' }, - { id: 'cloud9_night', label: 'Cloud9 Night' }, - { id: 'cloud9_night_low_color', label: 'Cloud9 Night Low Color' }, - { id: 'cloud_editor_dark', label: 'Cloud Editor Dark' }, - { id: 'cloud_editor', label: 'Cloud Editor' }, - { id: 'clouds', label: 'Clouds' }, - { id: 'clouds_midnight', label: 'Clouds Midnight' }, - { id: 'cobalt', label: 'Cobalt' }, - { id: 'crimson_editor', label: 'Crimson Editor' }, - { id: 'dawn', label: 'Dawn' }, - { id: 'dracula', label: 'Dracula' }, - { id: 'dreamweaver', label: 'Dreamweaver' }, - { id: 'eclipse', label: 'Eclipse' }, - { id: 'github_dark', label: 'GitHub Dark' }, - { id: 'github', label: 'GitHub' }, - { id: 'github_light_default', label: 'GitHub Light Default' }, - { id: 'gob', label: 'Gob' }, - { id: 'gruvbox_dark_hard', label: 'Gruvbox Dark Hard' }, - { id: 'gruvbox', label: 'Gruvbox' }, - { id: 'gruvbox_light_hard', label: 'Gruvbox Light Hard' }, - { id: 'idle_fingers', label: 'Idle Fingers' }, - { id: 'iplastic', label: 'iPlastic' }, - { id: 'katzenmilch', label: 'Katzenmilch' }, - { id: 'kr_theme', label: 'KR Theme' }, - { id: 'kuroir', label: 'Kuroir' }, - { id: 'merbivore', label: 'Merbivore' }, - { id: 'merbivore_soft', label: 'Merbivore Soft' }, - { id: 'mono_industrial', label: 'Mono Industrial' }, - { id: 'monokai', label: 'Monokai' }, - { id: 'nord_dark', label: 'Nord Dark' }, - { id: 'one_dark', label: 'One Dark' }, - { id: 'pastel_on_dark', label: 'Pastel on Dark' }, - { id: 'solarized_dark', label: 'Solarized Dark' }, - { id: 'solarized_light', label: 'Solarized Light' }, - { id: 'sqlserver', label: 'SQL Server' }, - { id: 'terminal', label: 'Terminal' }, - { id: 'textmate', label: 'Textmate' }, - { id: 'tomorrow', label: 'Tomorrow' }, - { id: 'tomorrow_night_blue', label: 'Tomorrow Night Blue' }, - { id: 'tomorrow_night_bright', label: 'Tomorrow Night Bright' }, - { id: 'tomorrow_night_eighties', label: 'Tomorrow Night Eighties' }, - { id: 'tomorrow_night', label: 'Tomorrow Night' }, - { id: 'twilight', label: 'Twilight' }, - { id: 'vibrant_ink', label: 'Vibrant Ink' }, - { id: 'xcode', label: 'Xcode' }, -] as const; +const TOGGLES: Option[] = [ + { id: 'highlightActiveLine', label: 'Highlight Active Line' }, + { id: 'minimap', label: 'Show Minimap' }, + { id: 'indentGuides', label: 'Indent Guides' }, + { id: 'autocompletion', label: 'Enable Autocompletion' }, + { id: 'highlightMatchingBracket', label: 'Higlight Matching Brackets' }, + { id: 'highlightSelectionMatches', label: 'Highlight Matching Selections' }, + { id: 'autoCloseBrackets', label: 'Automatically Clost Brackets' }, +]; -const KEYBINDS = [ - { id: 'ace', label: 'Ace (Default)' }, +const KEYMAPS: Option[] = [ + { id: 'default', label: 'Default' }, { id: 'vscode', label: 'VSCode' }, - { id: 'vim', label: 'Vim' }, + { id: 'vim', label: 'VIM' }, { id: 'emacs', label: 'Emacs' }, - { id: 'sublime', label: 'Sublime' }, -] as const; +]; + +const LINE_NUMBERS: Option[] = [ + { id: 'off', label: 'Off' }, + { id: 'normal', label: 'Normal' }, + { id: 'relative', label: 'Relative' }, +]; const CURSOR_STYLES = [ { id: 'ace', label: 'Ace (Default)' }, @@ -91,24 +50,149 @@ const CURSOR_STYLES = [ { id: 'wide', label: 'Wide' }, ] as const; -const FOLDS = [ - { id: 'manual', label: 'Manual' }, - { id: 'markbegin', label: 'Mark Begin' }, - { id: 'markbeginend', label: 'Mark Begin and End' }, -] as const; const TABS = [ { id: 'general', label: 'General', disabled: true }, { id: 'editor', label: 'Editor', disabled: false }, ] as const; -export function Editor() { - const [selectedItem, setSelectedItem] = useState<(typeof TABS)[number]['id']>('editor'); +const parseInRange = (num: string, min: number, max?: number): number => { + const n = +num; + if (isNaN(n) || n <= min) return min; + if (max && n >= max) return max; + return n; +} + +const EditorTab = () => { const [editorSettings, setEditorSettings] = useAtom(editorSettingsAtom); return ( -
-
+
+
+ + +
+ +
+ + +
+ +
+ + + setEditorSettings({ + ...editorSettings, + fontSize: parseInRange(e.target.value, 12), + }) + } + /> +
+ +
+ + + setEditorSettings({ + ...editorSettings, + tabSize: parseInRange(e.target.value, 1, 16), + }) + } + /> +
+ +
+ + +
+ +
+ {TOGGLES.map((option) => ( +
+ + setEditorSettings({ + ...editorSettings, + [option.id]: checked, + }) + } + /> + +
+ ))} +
+ +
+ ); +}; + +export default function Settings() { + const [selectedItem, setSelectedItem] = useState<(typeof TABS)[number]['id']>('editor'); + + return ( +
+

Configuration Options

{TABS.map((t, i) => (
); } diff --git a/client/components/UserMenu.tsx b/client/components/UserMenu.tsx index 20b6dc7..a448f44 100644 --- a/client/components/UserMenu.tsx +++ b/client/components/UserMenu.tsx @@ -1,7 +1,7 @@ import { useTheme } from 'next-themes'; import { useState } from 'react'; import { Button } from './ui/button'; -import { User, Sun, Moon, SunMoon, LogOut, Settings, Bell } from 'lucide-react'; +import { User, Sun, Moon, SunMoon, LogOut, SettingsIcon, Bell } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, @@ -19,7 +19,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; import { useAtom } from 'jotai'; import { currentUserAtom } from '@/lib/services/auth'; import { useLogin } from '@/lib/services/auth'; -import { Editor } from './Settings'; +import Settings from './Settings'; import { useRouter } from 'next/navigation'; import { announcementsAtom } from '@/lib/services/announcement'; import { Separator } from './ui/separator'; @@ -98,7 +98,7 @@ export default function UserMenu() { setOpen(true)}> - Settings + Settings @@ -110,11 +110,13 @@ export default function UserMenu() { - + Settings - +
+ +
diff --git a/client/lib/competitor-state.ts b/client/lib/competitor-state.ts index 9d52140..0cff7e0 100644 --- a/client/lib/competitor-state.ts +++ b/client/lib/competitor-state.ts @@ -2,40 +2,9 @@ import { atomWithStorage } from 'jotai/utils'; import { atom } from 'jotai'; import { currQuestionIdxAtom } from './services/questions'; import { Language } from './types'; +import { defaultEditorSettings, EditorSettings } from './editor/settings'; -export interface EditorSettings { - theme: string; - useSoftTabs: boolean; - showGutter: boolean; - enableBasicAutocompletion: boolean; - enableLiveAutocompletion: boolean; - highlightActiveLine: boolean; - relativeLineNumbers: boolean; - displayIndentGuides: boolean; - fontSize: number; - tabSize: number; - keybind: 'ace' | 'vscode' | 'vim' | 'emacs' | 'sublime' | undefined; - cursorStyle: 'ace' | 'slim' | 'smooth' | 'smooth-slim' | 'wide' | undefined; - foldStyle: 'manual' | 'markbegin' | 'markbeginend' | undefined; -} - -// Default Editor Configurations -export const editorSettingsAtom = atomWithStorage('editor-settings', { - theme: 'monokai', - useSoftTabs: true, - showGutter: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - highlightActiveLine: false, - relativeLineNumbers: false, - displayIndentGuides: false, - fontSize: 16, - tabSize: 4, - keybind: 'ace', - cursorStyle: 'ace', - foldStyle: 'manual', -}); - +export const editorSettingsAtom = atomWithStorage('editor-settings', defaultEditorSettings); export const currentTabAtom = atom<'text-editor' | 'leaderboard'>('text-editor'); const editorsAtom = atomWithStorage('editors', []); diff --git a/client/lib/editor/lang.ts b/client/lib/editor/langs.ts similarity index 100% rename from client/lib/editor/lang.ts rename to client/lib/editor/langs.ts diff --git a/client/lib/editor/settings.ts b/client/lib/editor/settings.ts new file mode 100644 index 0000000..dcd1a56 --- /dev/null +++ b/client/lib/editor/settings.ts @@ -0,0 +1,45 @@ +import { Theme } from "./themes"; + +export type Keymap = 'default' | 'vscode' | 'vim' | 'emacs'; +export type LineNumbers = 'off' | 'normal' | 'relative'; + +export interface EditorSettings { + theme: Theme; + keymap: Keymap; + fontSize: number; + autocompletion: boolean; + minimap: boolean; + lineNumbers: LineNumbers; + highlightMatchingBracket: boolean; + autoCloseBrackets: boolean; + highlightActiveLine: boolean; + highlightSelectionMatches: boolean; + tabSize: number; + indentGuides: boolean; + + // useSoftTabs: boolean; + // showGutter: boolean; + // displayIndentGuides: boolean; + // cursorStyle: 'ace' | 'slim' | 'smooth' | 'smooth-slim' | 'wide'; + // foldStyle: 'manual' | 'markbegin' | 'markbeginend'; +} + +export const defaultEditorSettings: EditorSettings = { + theme: 'monokai', + keymap: 'default', + fontSize: 16, + autocompletion: true, + minimap: true, + lineNumbers: 'normal', + highlightMatchingBracket: true, + autoCloseBrackets: true, + highlightActiveLine: true, + highlightSelectionMatches: true, + tabSize: 4, + indentGuides: false, + + // useSoftTabs: true, + // showGutter: true, + // cursorStyle: 'ace', + // foldStyle: 'manual', +}; diff --git a/client/lib/editor/themes.ts b/client/lib/editor/themes.ts new file mode 100644 index 0000000..d114aa3 --- /dev/null +++ b/client/lib/editor/themes.ts @@ -0,0 +1,191 @@ +import * as allThemes from '@uiw/codemirror-themes-all' + +const themes = { + abcdef: { + extension: allThemes.abcdef, + name: 'abcdef', + }, + abyss: { + extension: allThemes.abyss, + name: 'Abyss', + }, + androidstudio: { + extension: allThemes.androidstudio, + name: 'Android Studio', + }, + andromeda: { + extension: allThemes.andromeda, + name: 'Andromeda', + }, + atomone: { + extension: allThemes.atomone, + name: 'Atomone', + }, + aura: { + extension: allThemes.aura, + name: 'Aura', + }, + basicDark: { + extension: allThemes.basicDark, + name: 'Basic Dark', + }, + basicLight: { + extension: allThemes.basicLight, + name: 'Basic Light', + }, + bbedit: { + extension: allThemes.bbedit, + name: 'Bbedit', + }, + bespin: { + extension: allThemes.bespin, + name: 'Bespin', + }, + consoleDark: { + extension: allThemes.consoleDark, + name: 'Console Dark', + }, + consoleLight: { + extension: allThemes.consoleLight, + name: 'Console Light', + }, + copilot: { + extension: allThemes.copilot, + name: 'Copilot', + }, + darcula: { + extension: allThemes.darcula, + name: 'Darcula', + }, + dracula: { + extension: allThemes.dracula, + name: 'Dracula', + }, + duotoneDark: { + extension: allThemes.duotoneDark, + name: 'Duotone Dark', + }, + duotoneLight: { + extension: allThemes.duotoneLight, + name: 'Duotone Light', + }, + eclipse: { + extension: allThemes.eclipse, + name: 'Eclipse', + }, + githubDark: { + extension: allThemes.githubDark, + name: 'Github Dark', + }, + githubLight: { + extension: allThemes.githubLight, + name: 'Github Light', + }, + gruvboxDark: { + extension: allThemes.gruvboxDark, + name: 'Gruvbox Dark', + }, + gruvboxLight: { + extension: allThemes.gruvboxLight, + name: 'Gruvbox Light', + }, + kimbie: { + extension: allThemes.kimbie, + name: 'Kimbie', + }, + material: { + extension: allThemes.material, + name: 'Material', + }, + materialDark: { + extension: allThemes.materialDark, + name: 'Material Dark', + }, + materialLight: { + extension: allThemes.materialLight, + name: 'Material Light', + }, + monokai: { + extension: allThemes.monokai, + name: 'Monokai', + }, + monokaiDimmed: { + extension: allThemes.monokaiDimmed, + name: 'Monokai Dimmed', + }, + noctisLilac: { + extension: allThemes.noctisLilac, + name: 'Noctis Lilac', + }, + nord: { + extension: allThemes.nord, + name: 'Nord', + }, + okaidia: { + extension: allThemes.okaidia, + name: 'Okaidia', + }, + quietlight: { + extension: allThemes.quietlight, + name: 'Quietlight', + }, + red: { + extension: allThemes.red, + name: 'Red', + }, + solarizedDark: { + extension: allThemes.solarizedDark, + name: 'Solarized Dark', + }, + solarizedLight: { + extension: allThemes.solarizedLight, + name: 'Solarized Light', + }, + sublime: { + extension: allThemes.sublime, + name: 'Sublime', + }, + tokyoNight: { + extension: allThemes.tokyoNight, + name: 'Tokyo Night', + }, + tokyoNightDay: { + extension: allThemes.tokyoNightDay, + name: 'Tokyo Night Day', + }, + tokyoNightStorm: { + extension: allThemes.tokyoNightStorm, + name: 'Tokyo Night Storm', + }, + tomorrowNightBlue: { + extension: allThemes.tomorrowNightBlue, + name: 'Tomorrow Night Blue', + }, + vscodeDark: { + extension: allThemes.vscodeDark, + name: 'VSCode Dark', + }, + vscodeLight: { + extension: allThemes.vscodeLight, + name: 'VSCode Light', + }, + whiteDark: { + extension: allThemes.whiteDark, + name: 'White Dark', + }, + whiteLight: { + extension: allThemes.whiteLight, + name: 'White Light', + }, + xcodeDark: { + extension: allThemes.xcodeDark, + name: 'XCode Dark', + }, + xcodeLight: { + extension: allThemes.xcodeLight, + name: 'XCode Light', + }, +} as const; +export default themes; +export const themeNames = Object.keys(themes); +export type Theme = keyof typeof themes; diff --git a/client/lib/types.ts b/client/lib/types.ts index 39425f9..996cf42 100644 --- a/client/lib/types.ts +++ b/client/lib/types.ts @@ -1,4 +1,4 @@ -import { LanguageSyntax } from './editor/lang'; +import { LanguageSyntax } from './editor/langs'; import { User } from './services/auth'; export type TestState = 'pass' | 'fail' | 'in-progress' | 'not-attempted'; diff --git a/client/package-lock.json b/client/package-lock.json index 18af198..cb6415b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -30,12 +30,17 @@ "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.8", + "@replit/codemirror-emacs": "^6.1.0", + "@replit/codemirror-indentation-markers": "^6.5.3", + "@replit/codemirror-minimap": "^0.5.2", "@replit/codemirror-vim": "^6.3.0", + "@replit/codemirror-vscode-keymap": "^6.0.2", "@tanstack/react-query": "^5.71.10", "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.2.1", "@tauri-apps/plugin-fs": "^2.2.1", "@types/diff": "^7.0.2", + "@uiw/codemirror-themes-all": "^4.25.2", "@uiw/react-codemirror": "^4.25.2", "ansi-to-html": "^0.7.2", "class-variance-authority": "^0.7.1", @@ -2387,6 +2392,47 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@replit/codemirror-emacs": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz", + "integrity": "sha512-74DITnht6Cs6sHg02PQ169IKb1XgtyhI9sLD0JeOFco6Ds18PT+dkD8+DgXBDokne9UIFKsBbKPnpFRAz60/Lw==", + "license": "MIT", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.2", + "@codemirror/commands": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.1", + "@codemirror/view": "^6.3.0" + } + }, + "node_modules/@replit/codemirror-indentation-markers": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/@replit/codemirror-indentation-markers/-/codemirror-indentation-markers-6.5.3.tgz", + "integrity": "sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==", + "license": "MIT", + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/@replit/codemirror-minimap": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@replit/codemirror-minimap/-/codemirror-minimap-0.5.2.tgz", + "integrity": "sha512-eNAtpr0hOG09/5zqAQ5PkgZEb3V/MHi30zentCxiR73r+utR2m9yVMCpBmfsWbb8mWxUWhMGPiHxM5hFtnscQA==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.5" + }, + "peerDependencies": { + "@codemirror/language": "^6.9.1", + "@codemirror/lint": "^6.4.2", + "@codemirror/state": "^6.3.1", + "@codemirror/view": "^6.21.3", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.1.6" + } + }, "node_modules/@replit/codemirror-vim": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz", @@ -2400,6 +2446,21 @@ "@codemirror/view": "6.x.x" } }, + "node_modules/@replit/codemirror-vscode-keymap": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@replit/codemirror-vscode-keymap/-/codemirror-vscode-keymap-6.0.2.tgz", + "integrity": "sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==", + "license": "MIT", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2787,6 +2848,489 @@ "@codemirror/view": ">=6.0.0" } }, + "node_modules/@uiw/codemirror-theme-abcdef": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-abcdef/-/codemirror-theme-abcdef-4.25.2.tgz", + "integrity": "sha512-/Rh/MjeTDNo54w0NOD/URoNZfucHRsT7AzKLHo7ij3OpaweZr8wTPkRG0cnnX78m99wAiZKW4QH2Z/DcKBfBsA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-abyss": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-abyss/-/codemirror-theme-abyss-4.25.2.tgz", + "integrity": "sha512-rxOJ5KcDVy6fVnXhqgPz7pIN3h2ko1vy88asDlLrTn1El8FTEFThL4LHZqDu3PgnHO73b/5o+94S2FUqpqID4g==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-androidstudio": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-androidstudio/-/codemirror-theme-androidstudio-4.25.2.tgz", + "integrity": "sha512-mb1L6GkL5jj52NMoUOaDee0Z66/HoFJ3D61i1WllNCKQ8oF3dPypA1SljtbCRdD/GxrQDKkuAjs0m7SlULOI6g==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-andromeda": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-andromeda/-/codemirror-theme-andromeda-4.25.2.tgz", + "integrity": "sha512-iQjZ49SIvJwK7J8+OqWOhvca9axMiHC6TXYSoY/Ufab0rplIYgiNQ45byVenWg+6zIgQqTnPZ6J59GgrBrldaA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-atomone": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-atomone/-/codemirror-theme-atomone-4.25.2.tgz", + "integrity": "sha512-Zv4Q03d/PeEs96Y4UajxrzWmo32PgzQ8SeBHN3EcmHnPN8bCCHfB8NxL8f14OOMydg0geI/R2wRBargW3VcQxA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-aura": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-aura/-/codemirror-theme-aura-4.25.2.tgz", + "integrity": "sha512-oFy0qOYFit+hZJEiDd9jXPjusv0aVNiE6yKJMQGbDQkhDcnEk2qj9OMoTTDQBnfHaMSj5VIedmGQMfupWJo01w==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-basic": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-basic/-/codemirror-theme-basic-4.25.2.tgz", + "integrity": "sha512-+bVzLRUFGYdZR87Al2uUBzJ6n58GSitZhTQ8rtY+1cPd8N4H870oKyMqBkslrIoRY6F3lzuIJsPYJzdWYfD/jw==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-bbedit": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-bbedit/-/codemirror-theme-bbedit-4.25.2.tgz", + "integrity": "sha512-YM3t7+4AcBv4KH4oLy+hVvAgc2kGVAkL5HYji+raLiwX36uyR4ttudiPJyH1d+EoAFoB7kgdb96eKnNlQ0JziQ==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-bespin": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-bespin/-/codemirror-theme-bespin-4.25.2.tgz", + "integrity": "sha512-He0giqNhK5uODiV5yt4ti46cVOrci1A90+BWOo2Y9vROKrMtSUF22kXPTjCeuBdZmPPH/ttABl6UVcyK89HyXQ==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-console": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-console/-/codemirror-theme-console-4.25.2.tgz", + "integrity": "sha512-Xemsk//gUf76JvqJEc8kXdbkjfRA0u7dnRQjhS8kjoYY4a8JcbwNiESuMyNL7ZHg597zaeUox4mYFqAKVWmNYA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + } + }, + "node_modules/@uiw/codemirror-theme-copilot": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-copilot/-/codemirror-theme-copilot-4.25.2.tgz", + "integrity": "sha512-fsJbXVyeqZm1olA6arpZUI6oRSWvK4hKqzNnsefaVDXs4klSEbq5LG7oa/velqeV9W+/+Zenf9l8/h+sOYn94w==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-darcula": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-darcula/-/codemirror-theme-darcula-4.25.2.tgz", + "integrity": "sha512-PA0UNHB83gC7jFY8UmEjD5rCwYqFYprexNldHt2gd8aqLzSCPaF5o3HrP9kBONf6zNsJ+YVnOJP/0z+89rQs0w==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-dracula": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-dracula/-/codemirror-theme-dracula-4.25.2.tgz", + "integrity": "sha512-CB/7N71tIFJ0UsQqPyQEg26+/023RTLhZpL+a2ZHctExgLVsANgmAMYCFkZFuzg2/kLdYHH86GRYsQF8G0JmuQ==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-duotone": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-duotone/-/codemirror-theme-duotone-4.25.2.tgz", + "integrity": "sha512-5abvxj5pAeA2l5xnb3qEmYQiuSDgDPtM8kn8Y0HEHZWEDUOICsk65Gpgrrohw/olloGnfNoJQe+rheKWydSMcA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-eclipse": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-eclipse/-/codemirror-theme-eclipse-4.25.2.tgz", + "integrity": "sha512-9K7Y4dENKf6afrf+MWILCJWaq5Rfqc+AcwsdmUCybB58l/p0VPj8Rb+sN8Ax+GABfC+ij6pr81F8R2+C4yFOQA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-github": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.2.tgz", + "integrity": "sha512-9g3ujmYCNU2VQCp0+XzI1NS5hSZGgXRtH+5yWli5faiPvHGYZUVke+5Pnzdn/1tkgW6NpTQ7U/JHsyQkgbnZ/w==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-gruvbox-dark": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-gruvbox-dark/-/codemirror-theme-gruvbox-dark-4.25.2.tgz", + "integrity": "sha512-MrU1cNQ75mmwe/xoi+oiuG/NwZ7AaXGjkMmZDXzxJ30qDgvwQMl1LhTnqTGCRD5xdDFGMy2D2+qGEz73ykDjVg==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-kimbie": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-kimbie/-/codemirror-theme-kimbie-4.25.2.tgz", + "integrity": "sha512-Z8xA2gugSPJCki1BXoiXDJxYgUN/CAIR8qd5HuyAkqiAccrKzoSPJJGTzPFgIrknbG0cwrIIMu+fKbhprwJ8PA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-material": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-material/-/codemirror-theme-material-4.25.2.tgz", + "integrity": "sha512-IDN/TN8xul6QduarhQ1khD2WFHyPUYmjxBq8Z8w249ltahFo+QT/H7DcuityTijWgnSg5CRPXHL6CQLDOumZcg==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-monokai": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-monokai/-/codemirror-theme-monokai-4.25.2.tgz", + "integrity": "sha512-ZNcmoLyUJ6DYrtS0rhwSEWfVHOYSiOsgwdq4sGodK+s6Ax6c0E3ceoelesEqR6fTSfofjbFGprmF+hrJfDBmeA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-monokai-dimmed": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-monokai-dimmed/-/codemirror-theme-monokai-dimmed-4.25.2.tgz", + "integrity": "sha512-dDgFmvILZOufh+IhDDI3IaYaIS8Bkx19Qkt0iRu2btt4vzWsxhNeGho8lpmEZe7zbdeydbkkUBUXnz00ftDntQ==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-noctis-lilac": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-noctis-lilac/-/codemirror-theme-noctis-lilac-4.25.2.tgz", + "integrity": "sha512-RDanJ1BSEA56hBz97LDk/1yOxlqDqjBin2JVA2PshcDKak+c37unrC63ZmhVy+u6RWBvai2tHZCnv/A17o3Vtg==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-nord": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-nord/-/codemirror-theme-nord-4.25.2.tgz", + "integrity": "sha512-8Frqku0DAHD3XoTzm67RY+3zD1XmcoeN5C10zk4kU8PGAXDfxWqclZ/DK246zmPVKVQPqo4GT5jFoX2tA1JAsQ==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-okaidia": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-okaidia/-/codemirror-theme-okaidia-4.25.2.tgz", + "integrity": "sha512-l3Ov302XbIx3n22zrb0WazauLFaKIV74VMALXrvZpmdiY4rAZVulfXBAFmYDY9lCwGasaXpDsfEuHYx/4pDvOQ==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-quietlight": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-quietlight/-/codemirror-theme-quietlight-4.25.2.tgz", + "integrity": "sha512-UCr226qFTsDlZAClQyPXw4l/Za4lz50A7V2R6Okat44uu8RlFr0BDLTTpDoenw6g8IhLOt+/hyLtbb/UxPvPCw==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-red": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-red/-/codemirror-theme-red-4.25.2.tgz", + "integrity": "sha512-oABWhcNYdgzlYbC3q9aVO3idV8kkBSrUpScbvG9fDY6QhSJmatcoZsPL9Pb4iMXQmZe1r4S7iHJts6LwPX3+Xg==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-solarized": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-solarized/-/codemirror-theme-solarized-4.25.2.tgz", + "integrity": "sha512-ZMHz2aYwFhWxayL4tZrciixyfz9jO8oAfnGV1rPvW3QPeX9eMEbkRq7soRDpJL0Iy8zaxi7SsgoXBoyfX/SY6A==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-sublime": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-sublime/-/codemirror-theme-sublime-4.25.2.tgz", + "integrity": "sha512-QPqyX6yt9YirBSMmXbk98EfxGwdOPZ/LdM8zhvwITHF2+guYHfi+YPePOeGlAO9sGz5eG2ns1SqhTxDEnLurYQ==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-tokyo-night": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-tokyo-night/-/codemirror-theme-tokyo-night-4.25.2.tgz", + "integrity": "sha512-GXowrvAHSdhDwO5UdBx7HKClSm4GEcr7EZGszJwKMGWe8UBYhZBIzjD7V3BVbbMQq7vBK+TzSI/+xUiCIRle+w==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-tokyo-night-day": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-tokyo-night-day/-/codemirror-theme-tokyo-night-day-4.25.2.tgz", + "integrity": "sha512-f4jfBx+S3ykq4beRKpcUXoBH6g7c9c5VTxIrEQv9wazsFqhx9aur1ytViTLiWtsT2zRFCM+D8ruXHNqF5pIlHQ==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-tokyo-night-storm": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-tokyo-night-storm/-/codemirror-theme-tokyo-night-storm-4.25.2.tgz", + "integrity": "sha512-ogQxvBvbxB9DirbDYZUmTlbPuDRsOJjBq3ki+binRFkRpi7nt5kqV94ePjdRzL6vPQgv+JAHjJZ5g4mWhrDEyg==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-tomorrow-night-blue": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-tomorrow-night-blue/-/codemirror-theme-tomorrow-night-blue-4.25.2.tgz", + "integrity": "sha512-HZF2AfUsoICpvok8gY3MAJ9bfezUbwmZIUJ7jM9pZNwXoXmX/fDQLb4weHy4XYfPQcml0DjbQFDqJ0/GMvv8HA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-vscode": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.25.2.tgz", + "integrity": "sha512-0vZAAtC65v64sYKtUMvgg9xPw1QlcnFTzGv8vZ5fD4SmSXFZWXTapjezwhGmvR7/UdAPFyh/4korgBnQo9ix3A==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-white": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-white/-/codemirror-theme-white-4.25.2.tgz", + "integrity": "sha512-APuGFFxbdKFJ2v+KnGQ5T3n3y8331QT04cPjsJC54X+a9azIj3zsd7OiXbmDQD7lNakom1G4dF1ktsFvqFgPuA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-theme-xcode": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-xcode/-/codemirror-theme-xcode-4.25.2.tgz", + "integrity": "sha512-GtgiR/pBftaK0yA9F1EwWNGnnN4POr4ODniC0u27YkreUiVqiPajJEh/w2jcJV8vOMo/1+csE1GKSuvZUr2BhA==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.2.tgz", + "integrity": "sha512-WFYxW3OlCkMomXQBlQdGj1JZ011UNCa7xYdmgYqywVc4E8f5VgIzRwCZSBNVjpWGGDBOjc+Z6F65l7gttP16pg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-themes-all": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes-all/-/codemirror-themes-all-4.25.2.tgz", + "integrity": "sha512-hJJIXwnWdhSVGrdCEwTAlF2EuXrLOdADBK3d7Gr8dNrgJGltwSpXuuQ0akDYEC5DKwuH6ceOwVxOKhSskMm1/g==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-theme-abcdef": "4.25.2", + "@uiw/codemirror-theme-abyss": "4.25.2", + "@uiw/codemirror-theme-androidstudio": "4.25.2", + "@uiw/codemirror-theme-andromeda": "4.25.2", + "@uiw/codemirror-theme-atomone": "4.25.2", + "@uiw/codemirror-theme-aura": "4.25.2", + "@uiw/codemirror-theme-basic": "4.25.2", + "@uiw/codemirror-theme-bbedit": "4.25.2", + "@uiw/codemirror-theme-bespin": "4.25.2", + "@uiw/codemirror-theme-console": "4.25.2", + "@uiw/codemirror-theme-copilot": "4.25.2", + "@uiw/codemirror-theme-darcula": "4.25.2", + "@uiw/codemirror-theme-dracula": "4.25.2", + "@uiw/codemirror-theme-duotone": "4.25.2", + "@uiw/codemirror-theme-eclipse": "4.25.2", + "@uiw/codemirror-theme-github": "4.25.2", + "@uiw/codemirror-theme-gruvbox-dark": "4.25.2", + "@uiw/codemirror-theme-kimbie": "4.25.2", + "@uiw/codemirror-theme-material": "4.25.2", + "@uiw/codemirror-theme-monokai": "4.25.2", + "@uiw/codemirror-theme-monokai-dimmed": "4.25.2", + "@uiw/codemirror-theme-noctis-lilac": "4.25.2", + "@uiw/codemirror-theme-nord": "4.25.2", + "@uiw/codemirror-theme-okaidia": "4.25.2", + "@uiw/codemirror-theme-quietlight": "4.25.2", + "@uiw/codemirror-theme-red": "4.25.2", + "@uiw/codemirror-theme-solarized": "4.25.2", + "@uiw/codemirror-theme-sublime": "4.25.2", + "@uiw/codemirror-theme-tokyo-night": "4.25.2", + "@uiw/codemirror-theme-tokyo-night-day": "4.25.2", + "@uiw/codemirror-theme-tokyo-night-storm": "4.25.2", + "@uiw/codemirror-theme-tomorrow-night-blue": "4.25.2", + "@uiw/codemirror-theme-vscode": "4.25.2", + "@uiw/codemirror-theme-white": "4.25.2", + "@uiw/codemirror-theme-xcode": "4.25.2", + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, "node_modules/@uiw/react-codemirror": { "version": "4.25.2", "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.2.tgz", diff --git a/client/package.json b/client/package.json index 0665e10..5ac5799 100644 --- a/client/package.json +++ b/client/package.json @@ -33,12 +33,17 @@ "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.8", + "@replit/codemirror-emacs": "^6.1.0", + "@replit/codemirror-indentation-markers": "^6.5.3", + "@replit/codemirror-minimap": "^0.5.2", "@replit/codemirror-vim": "^6.3.0", + "@replit/codemirror-vscode-keymap": "^6.0.2", "@tanstack/react-query": "^5.71.10", "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.2.1", "@tauri-apps/plugin-fs": "^2.2.1", "@types/diff": "^7.0.2", + "@uiw/codemirror-themes-all": "^4.25.2", "@uiw/react-codemirror": "^4.25.2", "ansi-to-html": "^0.7.2", "class-variance-authority": "^0.7.1", From ca0df5688435483bb354da6b313eea1d006232e8 Mon Sep 17 00:00:00 2001 From: funnyboy-roks Date: Sat, 11 Oct 2025 06:43:40 -0500 Subject: [PATCH 3/4] prettier, eslint, CodeViewer --- client/app/competitor/page.tsx | 9 ++- client/components/CodeViewer.tsx | 48 ++++++++++----- client/components/Editor.tsx | 99 ++---------------------------- client/components/Settings.tsx | 23 +++---- client/components/UserMenu.tsx | 2 +- client/lib/competitor-state.ts | 5 +- client/lib/editor/index.ts | 101 +++++++++++++++++++++++++++++++ client/lib/editor/langs.ts | 34 +++++++++-- client/lib/editor/settings.ts | 2 +- client/lib/editor/themes.ts | 2 +- 10 files changed, 191 insertions(+), 134 deletions(-) create mode 100644 client/lib/editor/index.ts diff --git a/client/app/competitor/page.tsx b/client/app/competitor/page.tsx index 811424e..7934a8c 100644 --- a/client/app/competitor/page.tsx +++ b/client/app/competitor/page.tsx @@ -173,7 +173,14 @@ const EditorButtons = () => { - + setSelectedLanguage( + currQuestion.languages.find((l) => l.name === v) ?? null + ) + } + > diff --git a/client/components/CodeViewer.tsx b/client/components/CodeViewer.tsx index 3cca649..8a36a21 100644 --- a/client/components/CodeViewer.tsx +++ b/client/components/CodeViewer.tsx @@ -1,26 +1,44 @@ -import React from 'react'; -import { StreamLanguage } from '@codemirror/language'; -import CodeMirror from '@uiw/react-codemirror'; +import React, { useEffect, useRef, useState } from 'react'; +import CodeMirror, { Extension, ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { useAtom } from 'jotai'; import { editorSettingsAtom } from '@/lib/competitor-state'; -import { vim } from '@replit/codemirror-vim'; import { cn } from '@/lib/utils'; -import langs, { type LanguageSyntax } from '@/lib/editor/lang'; +import { type LanguageSyntax } from '@/lib/editor/langs'; +import { getExtensions } from '@/lib/editor'; -export const CodeViewer = ({ code, language = 'javascript', className = '' }: { code: string; language?: LanguageSyntax; className?: string }) => { - const [editorSettings] = useAtom(editorSettingsAtom); +export const CodeViewer = ({ + code, + language = 'javascript', + className = '', +}: { + code: string; + language?: LanguageSyntax; + className?: string; +}) => { + const [settings] = useAtom(editorSettingsAtom); + const $editor = useRef(null); + const [extensions, setExtensions] = useState([]); + + useEffect(() => { + setExtensions(getExtensions(settings, language)); + if ($editor.current) { + // TODO: this seems less than ideal + $editor.current.editor?.querySelectorAll('.cm-editor *').forEach((e) => { + e.style.fontSize = `${settings.fontSize}px`; + }); + } + }, [language, settings, $editor]); return ( ); }; diff --git a/client/components/Editor.tsx b/client/components/Editor.tsx index 7476a82..6a0be49 100644 --- a/client/components/Editor.tsx +++ b/client/components/Editor.tsx @@ -1,37 +1,12 @@ import React, { useEffect, useRef, useState } from 'react'; -import CodeMirror, { BasicSetupOptions, Extension, basicSetup, ReactCodeMirrorRef } from '@uiw/react-codemirror'; -import { keymap, lineNumbers } from '@codemirror/view'; -import { search } from "@codemirror/search" -import { StreamLanguage } from '@codemirror/language'; -import { indentationMarkers } from '@replit/codemirror-indentation-markers'; -import { showMinimap } from '@replit/codemirror-minimap'; -import { vim } from "@replit/codemirror-vim" -import { emacs } from "@replit/codemirror-emacs" -import { vscodeKeymap } from "@replit/codemirror-vscode-keymap" +import CodeMirror, { Extension, ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { useAtom } from 'jotai'; import { editorContentAtom, editorSettingsAtom, selectedLanguageAtom, } from '@/lib/competitor-state'; -import langs from '@/lib/editor/langs'; -import themes from '@/lib/editor/themes'; - -const relativeLineNumbers = () => lineNumbers({ - formatNumber: (lineNo, state) => { - // Trailing spaces are trimmed, so ' ' + ZWSP - const SPACE = ' ​'; - if (lineNo > state.doc.lines) { - return lineNo + SPACE; - } - const cursorLine = state.doc.lineAt(state.selection.asSingle().ranges[0].to).number; - if (lineNo === cursorLine) { - return cursorLine + SPACE; - } else { - return Math.abs(cursorLine - lineNo).toString(); - } - }, -}); +import { getExtensions } from '@/lib/editor'; export default function CodeEditor() { const [editorContent, setEditorContent] = useAtom(editorContentAtom); @@ -41,76 +16,13 @@ export default function CodeEditor() { const [extensions, setExtensions] = useState([]); useEffect(() => { - const basicOptions: BasicSetupOptions = { - allowMultipleSelections: true, - autocompletion: settings.autocompletion, - bracketMatching: settings.highlightMatchingBracket, - closeBrackets: settings.autoCloseBrackets, - closeBracketsKeymap: true, - completionKeymap: true, - defaultKeymap: settings.keymap === 'default', - drawSelection: true, // ? - dropCursor: true, - foldGutter: true, - foldKeymap: false, - highlightActiveLine: settings.highlightActiveLine, - highlightActiveLineGutter: true, - highlightSelectionMatches: settings.highlightSelectionMatches, - highlightSpecialChars: true, - /// undo history - history: true, - historyKeymap: true, - /// configures whether some inputs trigger reindentation of the current line. - indentOnInput: true, - lineNumbers: settings.lineNumbers !== 'off', - lintKeymap: true, - searchKeymap: settings.keymap === 'default', - /// Alt+left mouse to select in a rectangle - rectangularSelection: true, - /// Crosshair cursor for ^^^ - crosshairCursor: true, - syntaxHighlighting: true, - tabSize: settings.tabSize, - }; - - const extensions = [ - ...basicSetup(basicOptions), - search(), // See https://github.com/uiwjs/react-codemirror/issues/404#issuecomment-1809447795 - ]; - - const lang = language ? langs[language.syntax] : undefined; - if (lang) - extensions.push(StreamLanguage.define(lang)); - if (settings.lineNumbers === 'relative') - extensions.push(relativeLineNumbers()); - if (settings.indentGuides) - extensions.push(indentationMarkers({ thickness: 3 })); - if (settings.theme) - extensions.push(themes[settings.theme]?.extension ?? themes.red); - if (settings.minimap) { - extensions.push(showMinimap.compute(['doc'], _state => ({ - create: (_v) => ({ dom: document.createElement('div') }), - displayText: 'blocks', - showOverlay: 'mouse-over', - }))); - } - - switch (settings.keymap) { - case 'default': break; - case 'vscode': extensions.push(keymap.of(vscodeKeymap)); break; - case 'vim': extensions.push(vim()); break; - case 'emacs': extensions.push(emacs()); break; - } - + setExtensions(getExtensions(settings, language?.syntax)); if ($editor.current) { // TODO: this seems less than ideal - $editor.current.editor?.querySelectorAll('.cm-editor *').forEach(e => { + $editor.current.editor?.querySelectorAll('.cm-editor *').forEach((e) => { e.style.fontSize = `${settings.fontSize}px`; }); } - console.debug('rebuilt extensions'); - - setExtensions(extensions); }, [language, settings, $editor]); return ( @@ -119,10 +31,9 @@ export default function CodeEditor() { ref={$editor} autoFocus theme="none" - className="w-full h-full" + className="h-full w-full" value={editorContent} onChange={setEditorContent} - basicSetup={false} /> ); diff --git a/client/components/Settings.tsx b/client/components/Settings.tsx index 2f60cc7..4fb9208 100644 --- a/client/components/Settings.tsx +++ b/client/components/Settings.tsx @@ -17,7 +17,7 @@ import { EditorSettings, Keymap, LineNumbers } from '@/lib/editor/settings'; import themes, { Theme } from '@/lib/editor/themes'; import { ScrollArea } from './ui/scroll-area'; -type Option = { id: K; label: string; description?: string; }; +type Option = { id: K; label: string; description?: string }; const TOGGLES: Option[] = [ { id: 'highlightActiveLine', label: 'Highlight Active Line' }, @@ -42,15 +42,6 @@ const LINE_NUMBERS: Option[] = [ { id: 'relative', label: 'Relative' }, ]; -const CURSOR_STYLES = [ - { id: 'ace', label: 'Ace (Default)' }, - { id: 'slim', label: 'Slim' }, - { id: 'smooth', label: 'Smooth' }, - { id: 'smooth-slim', label: 'Smooth and Slim' }, - { id: 'wide', label: 'Wide' }, -] as const; - - const TABS = [ { id: 'general', label: 'General', disabled: true }, { id: 'editor', label: 'Editor', disabled: false }, @@ -61,13 +52,13 @@ const parseInRange = (num: string, min: number, max?: number): number => { if (isNaN(n) || n <= min) return min; if (max && n >= max) return max; return n; -} +}; const EditorTab = () => { const [editorSettings, setEditorSettings] = useAtom(editorSettingsAtom); return ( -
+