Improved editor

This commit is contained in:
2026-02-06 23:45:42 +01:00
parent daad7392a0
commit 3b2809ed85
6 changed files with 203 additions and 205 deletions
+2 -2
View File
@@ -314,5 +314,5 @@
} }
.cm-editor { .cm-editor {
height: 100%;; height: 100%;
} }
+81 -13
View File
@@ -1,13 +1,14 @@
"use client"; "use client";
import CodeMirror, { EditorState } from "@uiw/react-codemirror"; import CodeMirror, { EditorState, EditorView } from "@uiw/react-codemirror";
import { devRun } from "./theme/theme"; import { devRun } from "./theme";
import { javascript } from "@codemirror/lang-javascript"; import { javascript } from "@codemirror/lang-javascript";
import { rust } from "@codemirror/lang-rust"; import { rust } from "@codemirror/lang-rust";
import { go } from "@codemirror/lang-go"; import { go } from "@codemirror/lang-go";
import { json } from "@codemirror/lang-json"; import { json } from "@codemirror/lang-json";
import { python } from "@codemirror/lang-python"; import { python } from "@codemirror/lang-python";
import { java } from "@codemirror/lang-java"; import { java } from "@codemirror/lang-java";
import { useEffect, useState } from "react";
type Language = type Language =
| "javascript" | "javascript"
@@ -37,12 +38,17 @@ function getLanguageExtension(lang: Language) {
} }
} }
export default function Editor() { type Move = {
return ( latency: number;
<CodeMirror cursor: number;
readOnly changes: {
className="h-full w-full" from: number;
value={`// Import the necessary React library features to: number;
insert: string;
};
};
const code = `// Import the necessary React library features
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
// Define a functional component named "App" // Define a functional component named "App"
@@ -59,7 +65,72 @@ const domNode = document.getElementById('root');
// Create a root and render your component into the DOM // Create a root and render your component into the DOM
const root = createRoot(domNode); const root = createRoot(domNode);
root.render(<App />); `} root.render(<App />);`.repeat(3);
// Helper to split text into chunks of ~4 chars
function chunkText(text: string, size = 4) {
const chunks = [];
for (let i = 0; i < text.length; i += size) {
chunks.push(text.slice(i, i + size));
}
return chunks;
}
const chunks = chunkText(code, 4);
const moves: Move[] = chunks.map((chunk, index) => ({
latency: 1, // 100ms per chunk
cursor: index * 4, // approximate cursor position
changes: {
from: index * 4,
to: index * 4,
insert: chunk,
},
}));
export default function Editor() {
const [editorView, setEditorView] = useState<EditorView>();
useEffect(() => {
if (!editorView) return;
let cancelled = false;
const executeMove = async (move: Move) => {
return new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
if (!editorView || cancelled) return resolve();
editorView.dispatch({
selection: { anchor: move.cursor },
scrollIntoView: true,
changes: move.changes,
});
resolve();
}, move.latency);
return () => clearTimeout(timeout);
});
};
(async () => {
for (const move of moves) {
if (cancelled) break;
await executeMove(move);
}
})();
return () => {
cancelled = true; // stop future moves
};
}, [editorView]);
return (
<CodeMirror
readOnly
className="h-full w-full"
value={``}
extensions={[ extensions={[
getLanguageExtension("javascript"), getLanguageExtension("javascript"),
EditorState.transactionFilter.of((tr) => { EditorState.transactionFilter.of((tr) => {
@@ -72,10 +143,7 @@ root.render(<App />); `}
theme={devRun} theme={devRun}
onCreateEditor={(view) => { onCreateEditor={(view) => {
view.focus(); view.focus();
view.dispatch({ setEditorView(view);
selection: { anchor: 1 },
scrollIntoView: true,
});
}} }}
/> />
); );
+119
View File
@@ -0,0 +1,119 @@
import { EditorView } from "@codemirror/view";
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags as t } from "@lezer/highlight";
/**
* devRun — direct CodeMirror 6 theme
* Based on thememirror (dracula), app-integrated
*/
export const devRun = [
EditorView.theme(
{
"&": {
backgroundColor: "var(--card)",
color: "#e6e6eb",
},
".cm-content": {
caretColor: "var(--primary)",
},
".cm-cursor, .cm-dropCursor": {
borderLeftColor: "var(--primary)",
},
"&.cm-focused .cm-selectionBackground, .cm-content ::selection": {
backgroundColor: "rgba(120, 120, 150, 0.25)",
},
".cm-activeLine": {
backgroundColor: "rgba(255, 255, 255, 0.03)",
},
".cm-gutters": {
backgroundColor: "var(--card)",
color: "rgb(120, 122, 128)",
border: "none",
},
".cm-activeLineGutter": {
backgroundColor: "rgba(255, 255, 255, 0.03)",
},
// - Scrollbar -
".cm-scroller": {
scrollbarWidth: "thin",
scrollbarColor: "rgba(201, 133, 255, 0.3) transparent",
},
},
{ dark: true },
),
syntaxHighlighting(
HighlightStyle.define([
// Comments — low priority, low contrast
{
tag: t.comment,
color: "#5f6a8a",
fontStyle: "italic",
},
// Strings & literals — data, not logic
{
tag: [t.string, t.special(t.brace)],
color: "#f4e58c",
},
// Numbers / booleans — benchmarks & truth
{
tag: [t.number, t.bool, t.null],
color: "#c792ea",
fontWeight: "500",
},
// Keywords & operators — decisions & flow
{
tag: [t.keyword, t.operator],
color: "#ff5fa2",
fontWeight: "600",
},
// Types & definitions — structure
{
tag: [t.definitionKeyword, t.typeName],
color: "#6be6ff",
},
// Type definitions — neutral, readable
{
tag: t.definition(t.typeName),
color: "#e6e6eb",
},
// Functions, classes, props — execution paths
{
tag: [
t.className,
t.definition(t.propertyName),
t.function(t.variableName),
t.attributeName,
],
color: "#4dff9f",
fontWeight: "500",
},
// Variables — slightly muted, context-aware
{
tag: t.variableName,
color: "#cfd3dc",
},
// Invalid / errors — instant attention
{
tag: t.invalid,
color: "#ff5555",
fontWeight: "700",
},
]),
),
];
@@ -1,105 +0,0 @@
import { EditorView } from "@codemirror/view";
import { Extension } from "@codemirror/state";
import {
HighlightStyle,
TagStyle,
syntaxHighlighting,
} from "@codemirror/language";
interface Options {
/**
* Theme variant. Determines which styles CodeMirror will apply by default.
*/
variant: Variant;
/**
* Settings to customize the look of the editor, like background, gutter, selection and others.
*/
settings: Settings;
/**
* Syntax highlighting styles.
*/
styles: TagStyle[];
}
type Variant = "light" | "dark";
interface Settings {
/**
* Editor background.
*/
background: string;
/**
* Default text color.
*/
foreground: string;
/**
* Caret color.
*/
caret: string;
/**
* Selection background.
*/
selection: string;
/**
* Background of highlighted lines.
*/
lineHighlight: string;
/**
* Gutter background.
*/
gutterBackground: string;
/**
* Text color inside gutter.
*/
gutterForeground: string;
}
const createTheme = ({ variant, settings, styles }: Options): Extension => {
const theme = EditorView.theme(
{
// eslint-disable-next-line @typescript-eslint/naming-convention
"&": {
backgroundColor: settings.background,
color: settings.foreground,
},
".cm-content": {
caretColor: settings.caret,
},
".cm-cursor, .cm-dropCursor": {
borderLeftColor: settings.caret,
},
"&.cm-focused .cm-selectionBackgroundm .cm-selectionBackground, .cm-content ::selection":
{
backgroundColor: settings.selection,
},
".cm-activeLine": {
backgroundColor: settings.lineHighlight,
},
".cm-gutters": {
backgroundColor: settings.gutterBackground,
color: settings.gutterForeground,
},
".cm-activeLineGutter": {
backgroundColor: settings.lineHighlight,
},
},
{
dark: variant === "dark",
},
);
const highlightStyle = HighlightStyle.define(styles);
const extension = [theme, syntaxHighlighting(highlightStyle)];
return extension;
};
export default createTheme;
-81
View File
@@ -1,81 +0,0 @@
import { tags as t } from "@lezer/highlight";
import createTheme from "./create-theme";
// Original theme: thememirror (dracula)
export const devRun = createTheme({
variant: "dark",
settings: {
background: "var(--card)", // stays app-integrated
foreground: "#e6e6eb",
caret: "var(--primary)",
selection: "rgba(120, 120, 150, 0.25)",
gutterBackground: "var(--card)",
gutterForeground: "rgb(120, 122, 128)",
lineHighlight: "rgba(255, 255, 255, 0.03)",
},
styles: [
// Comments — low priority, low contrast
{
tag: t.comment,
color: "#5f6a8a",
fontStyle: "italic",
},
// Strings & literals — data, not logic
{
tag: [t.string, t.special(t.brace)],
color: "#f4e58c",
},
// Numbers / booleans — benchmarks & truth
{
tag: [t.number, t.bool, t.null],
color: "#c792ea",
fontWeight: "500",
},
// Keywords & operators — decisions & flow
{
tag: [t.keyword, t.operator],
color: "#ff5fa2",
fontWeight: "600",
},
// Types & definitions — structure
{
tag: [t.definitionKeyword, t.typeName],
color: "#6be6ff",
},
// Type definitions — neutral, readable
{
tag: t.definition(t.typeName),
color: "#e6e6eb",
},
// Functions, classes, props — execution paths
{
tag: [
t.className,
t.definition(t.propertyName),
t.function(t.variableName),
t.attributeName,
],
color: "#4dff9f",
fontWeight: "500",
},
// Variables — slightly muted, context-aware
{
tag: t.variableName,
color: "#cfd3dc",
},
// Invalid / errors — instant attention
{
tag: t.invalid,
color: "#ff5555",
fontWeight: "700",
},
],
});
+1 -4
View File
@@ -1,13 +1,10 @@
"use client"; "use client";
import CodeMirror, { EditorState } from "@uiw/react-codemirror";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import RunTimer, { RunInfo } from "./components/RunTimer"; import RunTimer, { RunInfo } from "./components/RunTimer";
import RunChat, { ChatMessage } from "./components/RunChat"; import RunChat, { ChatMessage } from "./components/RunChat";
import { FileTree } from "./components/FileTree"; import { FileTree } from "./components/FileTree";
import { File } from "lucide-react"; import { File } from "lucide-react";
import { devRun } from "./components/theme/theme";
import { javascript } from "@codemirror/lang-javascript";
import Editor from "./components/Editor"; import Editor from "./components/Editor";
export default function Run() { export default function Run() {
@@ -53,7 +50,7 @@ export default function Run() {
<header className="text-sm font-medium text-muted-foreground flex items-center gap-1"> <header className="text-sm font-medium text-muted-foreground flex items-center gap-1">
<File size={16} /> myfile.js <File size={16} /> myfile.js
</header> </header>
<div className="flex-1 pt-3 pb-5"> <div className="flex-1 pt-3 pb-5 h-full">
<Editor /> <Editor />
</div> </div>
</div> </div>