/
nayvok
/
livecoding
Обзор
Документация
Войти
/
nayvok
/
livecoding
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
frontend/src/components/CodeEditor.tsx
135 строк
4 KB
nayvok
fix: harden sessions and WebRTC recovery
17 июн 2026, 22:02
17 июн 2026, 22:02
37b9a92
Код
Авторство
О чём код?
import { useEffect, useRef, useState } from 'react' import { Compartment, EditorState, type Extension } from '@codemirror/state' import { EditorView, highlightActiveLine, highlightActiveLineGutter, keymap, lineNumbers, } from '@codemirror/view' import { defaultKeymap, history, historyKeymap, } from '@codemirror/commands' import { bracketMatching, defaultHighlightStyle, foldKeymap, indentOnInput, syntaxHighlighting, } from '@codemirror/language' import { javascript } from '@codemirror/lang-javascript' import { python } from '@codemirror/lang-python' import { yCollab } from 'y-codemirror.next' import { nanoid } from 'nanoid' import { useSession } from '@/context/useSession' import { pickColor } from '@/lib/colors' type LanguageFactory = () => Extension const LANGUAGE_EXTENSIONS: Record<string, LanguageFactory> = { javascript: () => javascript(), python: () => python(), } function resolveLanguage(id: string): Extension { const factory = LANGUAGE_EXTENSIONS[id] ?? LANGUAGE_EXTENSIONS.javascript! return factory() } interface CodeEditorProps { language: string } export function CodeEditor({ language }: CodeEditorProps) { const { ydoc, awareness, role } = useSession() const containerRef = useRef<HTMLDivElement>(null) const viewRef = useRef<EditorView | null>(null) const languageCompartmentRef = useRef<Compartment>(new Compartment()) const [identity] = useState(() => { const userId = nanoid(8) return { userId, color: pickColor(userId), name: role === 'teacher' ? 'Преподаватель' : 'Студент', } }) // EditorView создается один раз. useEffect(() => { const container = containerRef.current if (!container || !awareness) return const ytext = ydoc.getText('content') awareness.setLocalStateField('user', { name: identity.name, color: identity.color, colorLight: identity.color + '33', }) const state = EditorState.create({ doc: ytext.toString(), extensions: [ lineNumbers(), highlightActiveLineGutter(), history(), indentOnInput(), bracketMatching(), highlightActiveLine(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), keymap.of([...defaultKeymap, ...historyKeymap, ...foldKeymap]), languageCompartmentRef.current.of(resolveLanguage(language)), yCollab(ytext, awareness), EditorView.theme({ '&': { height: '100%', fontSize: '14px', backgroundColor: 'transparent', color: 'var(--foreground)', }, '.cm-scroller': { overflow: 'auto', fontFamily: 'ui-monospace, Consolas, monospace', }, '.cm-content': { padding: '8px 0', caretColor: 'var(--foreground)' }, '.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--foreground)' }, '.cm-gutters': { backgroundColor: 'transparent', color: 'var(--muted-foreground)', borderRight: '1px solid var(--border)', }, '.cm-activeLine': { backgroundColor: 'color-mix(in oklch, var(--accent) 60%, transparent)' }, '.cm-activeLineGutter': { backgroundColor: 'color-mix(in oklch, var(--accent) 60%, transparent)' }, '&.cm-focused .cm-selectionBackground, ::selection': { backgroundColor: 'color-mix(in oklch, var(--primary) 30%, transparent)', }, }), ], }) const view = new EditorView({ state, parent: container }) viewRef.current = view return () => { view.destroy() viewRef.current = null // awareness чистит провайдер } // language меняется ниже через compartment // eslint-disable-next-line react-hooks/exhaustive-deps }, [ydoc, awareness, identity]) // Смена языка без пересоздания редактора. useEffect(() => { const view = viewRef.current if (!view) return view.dispatch({ effects: languageCompartmentRef.current.reconfigure(resolveLanguage(language)), }) }, [language]) return <div ref={containerRef} className="h-full w-full overflow-hidden" /> }