diff --git a/src/components/code/AdvancedCodeEditor.tsx b/src/components/code/AdvancedCodeEditor.tsx new file mode 100644 index 0000000..ade9f96 --- /dev/null +++ b/src/components/code/AdvancedCodeEditor.tsx @@ -0,0 +1,375 @@ +'use client'; + +import React, { Suspense } from 'react'; +import { + Play, + RotateCcw, + Wand2, + Sun, + Moon, + ZoomIn, + ZoomOut, + X, + CheckCircle, + XCircle, + Loader2, + Terminal, +} from 'lucide-react'; +import { useCodeEditor } from '@/hooks/useCodeEditor'; +import { SyntaxHighlighter } from './SyntaxHighlighter'; +import { AutoCompletion } from './AutoCompletion'; +import { CollaborativeEditing } from './CollaborativeEditing'; +import type { CompletionSuggestion } from '@/utils/codeUtils'; + +// Lazy-load Monaco to avoid SSR issues in Next.js +const MonacoEditor = React.lazy(() => + import('@monaco-editor/react').then((mod) => ({ default: mod.default })), +); + +// --------------------------------------------------------------------------- +// Props +// --------------------------------------------------------------------------- + +interface AdvancedCodeEditorProps { + initialCode?: string; + initialLanguage?: string; + roomId?: string; + onCodeChange?: (code: string) => void; + height?: string; +} + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +const EditorLoader: React.FC = () => ( +
+
+ + Loading editor… +
+
+); + +// --------------------------------------------------------------------------- +// Main component +// --------------------------------------------------------------------------- + +export const AdvancedCodeEditor: React.FC = ({ + initialCode, + initialLanguage = 'javascript', + roomId, + onCodeChange, + height = 'calc(100vh - 80px)', +}) => { + const { + code, + language, + theme, + fontSize, + isRunning, + output, + validationErrors, + collaborators, + autoCompleteEnabled, + currentWord, + languages, + languageConfig, + setCode, + setLanguage, + toggleTheme, + increaseFontSize, + decreaseFontSize, + runCode, + handleFormat, + resetCode, + clearOutput, + toggleAutoComplete, + handleEditorMount, + } = useCodeEditor({ initialCode, initialLanguage, roomId, onCodeChange }); + + // Insert suggestion text into editor via code state + const handleSuggestionSelect = (suggestion: CompletionSuggestion) => { + const insertText = suggestion.insertText.replace(/\$\d+/g, ''); + setCode(code + insertText); + }; + + const isDark = theme === 'vs-dark'; + + return ( +
+ {/* ------------------------------------------------------------------ */} + {/* Top toolbar */} + {/* ------------------------------------------------------------------ */} +
+ {/* Left: language selector + language badge */} +
+ + +
+ + {/* Centre: action buttons */} +
+ {/* Run */} + + + {/* Format */} + + + {/* Reset */} + + + {/* Font size */} +
+ + + {fontSize} + + +
+ + {/* Theme toggle */} + +
+ + {/* Right: auto-completion + collaborators */} +
+ + +
+
+ + {/* ------------------------------------------------------------------ */} + {/* Monaco editor */} + {/* ------------------------------------------------------------------ */} +
+ }> + setCode(val ?? '')} + onMount={handleEditorMount} + options={{ + fontSize, + minimap: { enabled: true }, + wordWrap: 'on', + lineNumbers: 'on', + renderLineHighlight: 'all', + scrollBeyondLastLine: false, + automaticLayout: true, + padding: { top: 12, bottom: 12 }, + suggestOnTriggerCharacters: autoCompleteEnabled, + quickSuggestions: autoCompleteEnabled, + tabSize: languageConfig.id === 'python' ? 4 : 2, + detectIndentation: false, + formatOnPaste: true, + smoothScrolling: true, + cursorBlinking: 'expand', + cursorSmoothCaretAnimation: 'on', + bracketPairColorization: { enabled: true }, + fontFamily: '"Fira Code", "Cascadia Code", "Consolas", monospace', + fontLigatures: true, + }} + height="100%" + width="100%" + /> + +
+ + {/* ------------------------------------------------------------------ */} + {/* Status bar */} + {/* ------------------------------------------------------------------ */} +
+
+ .{languageConfig.extension} + {validationErrors.length > 0 ? ( + + + {validationErrors.length} issue{validationErrors.length > 1 ? 's' : ''} + + ) : ( + + OK + + )} +
+ + Ln {1} · UTF-8 · {theme === 'vs-dark' ? 'Dark' : 'Light'} + +
+ + {/* ------------------------------------------------------------------ */} + {/* Output panel */} + {/* ------------------------------------------------------------------ */} + {output && ( +
+ {/* Output header */} +
+
+ + + Output + + {/* Exit code badge */} + + exit {output.exitCode} + + {output.executionTimeMs}ms +
+ +
+ + {/* stdout */} + {output.stdout && ( +
+              {output.stdout}
+            
+ )} + + {/* stderr */} + {output.stderr && ( +
+              {output.stderr}
+            
+ )} +
+ )} +
+ ); +}; diff --git a/src/components/code/AutoCompletion.tsx b/src/components/code/AutoCompletion.tsx new file mode 100644 index 0000000..ad478d2 --- /dev/null +++ b/src/components/code/AutoCompletion.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { Zap, ZapOff } from 'lucide-react'; +import { getAutoCompletionSuggestions, type CompletionSuggestion } from '@/utils/codeUtils'; + +interface AutoCompletionProps { + language: string; + word: string; + enabled: boolean; + onToggle: () => void; + onSelect?: (suggestion: CompletionSuggestion) => void; +} + +const KIND_COLORS: Record = { + keyword: 'text-purple-400', + snippet: 'text-blue-400', + function: 'text-yellow-400', + variable: 'text-green-400', +}; + +const KIND_LABELS: Record = { + keyword: 'kw', + snippet: '{}', + function: 'fn', + variable: 'var', +}; + +export const AutoCompletion: React.FC = ({ + language, + word, + enabled, + onToggle, + onSelect, +}) => { + const suggestions = enabled && word + ? getAutoCompletionSuggestions(language, word) + : []; + + return ( +
+ {/* Toggle button */} + + + {/* Suggestions panel */} + {suggestions.length > 0 && ( +
+
+ Suggestions for "{word}" +
+
    + {suggestions.slice(0, 8).map((s, i) => ( +
  • + +
  • + ))} +
+
+ )} +
+ ); +}; diff --git a/src/components/code/CollaborativeEditing.tsx b/src/components/code/CollaborativeEditing.tsx new file mode 100644 index 0000000..318934e --- /dev/null +++ b/src/components/code/CollaborativeEditing.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useState } from 'react'; +import { Users, Wifi } from 'lucide-react'; +import type { Collaborator } from '@/hooks/useCodeEditor'; + +interface CollaborativeEditingProps { + collaborators: Collaborator[]; + roomId?: string; +} + +export const CollaborativeEditing: React.FC = ({ + collaborators, + roomId, +}) => { + const [isConnected, setIsConnected] = useState(false); + const [activeCount, setActiveCount] = useState(collaborators.length); + + // Simulate a Socket.IO connection lifecycle + useEffect(() => { + if (!roomId) return; + + // Simulate connection delay + const connectTimer = setTimeout(() => { + setIsConnected(true); + }, 800); + + // Simulate occasional collaborator join/leave + const updateTimer = setInterval(() => { + setActiveCount((prev) => { + const delta = Math.random() > 0.5 ? 0 : Math.random() > 0.5 ? 1 : -1; + return Math.max(0, Math.min(collaborators.length + 2, prev + delta)); + }); + }, 8000); + + return () => { + clearTimeout(connectTimer); + clearInterval(updateTimer); + setIsConnected(false); + }; + }, [roomId, collaborators.length]); + + const visibleCollaborators = collaborators.slice(0, 4); + const overflow = Math.max(0, activeCount - 4); + + return ( +
+ {/* Connection status */} + {roomId && ( +
+ + + {isConnected ? 'Live' : 'Connecting…'} + +
+ )} + + {/* Avatar stack */} +
+
+ {visibleCollaborators.map((user) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {user.name} + {/* Presence dot */} + +
+ ))} + + {overflow > 0 && ( +
+ +{overflow} +
+ )} +
+ + {/* Count badge */} +
+ + {activeCount} live +
+
+
+ ); +}; diff --git a/src/components/code/SyntaxHighlighter.tsx b/src/components/code/SyntaxHighlighter.tsx new file mode 100644 index 0000000..ca4644a --- /dev/null +++ b/src/components/code/SyntaxHighlighter.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { getLanguageConfig } from '@/utils/codeUtils'; + +interface SyntaxHighlighterProps { + language: string; + showLabel?: boolean; + size?: 'sm' | 'md'; +} + +export const SyntaxHighlighter: React.FC = ({ + language, + showLabel = true, + size = 'md', +}) => { + const config = getLanguageConfig(language); + + const dotSize = size === 'sm' ? 'w-2 h-2' : 'w-3 h-3'; + const textSize = size === 'sm' ? 'text-xs' : 'text-sm'; + const padding = size === 'sm' ? 'px-2 py-0.5' : 'px-3 py-1'; + + return ( + + + {showLabel && config.label} + + ); +}; diff --git a/src/hooks/useCodeEditor.tsx b/src/hooks/useCodeEditor.tsx new file mode 100644 index 0000000..60ad9f4 --- /dev/null +++ b/src/hooks/useCodeEditor.tsx @@ -0,0 +1,259 @@ +import { useState, useCallback, useRef } from 'react'; +import type { editor } from 'monaco-editor'; +import { + getLanguageConfig, + getAllLanguages, + formatCode as formatCodeUtil, + validateCode, + simulateCodeExecution, + type ExecutionResult, + type LanguageConfig, +} from '@/utils/codeUtils'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface Collaborator { + id: string; + name: string; + color: string; + avatar: string; + cursorLine?: number; + cursorColumn?: number; +} + +export interface UseCodeEditorOptions { + initialCode?: string; + initialLanguage?: string; + roomId?: string; + onCodeChange?: (code: string) => void; +} + +export interface UseCodeEditorReturn { + // State + code: string; + language: string; + theme: 'vs-dark' | 'light'; + fontSize: number; + isRunning: boolean; + output: ExecutionResult | null; + validationErrors: Array<{ line: number; message: string }>; + collaborators: Collaborator[]; + autoCompleteEnabled: boolean; + currentWord: string; + + // Config helpers + languages: LanguageConfig[]; + languageConfig: LanguageConfig; + + // Actions + setCode: (code: string) => void; + setLanguage: (lang: string) => void; + toggleTheme: () => void; + increaseFontSize: () => void; + decreaseFontSize: () => void; + runCode: () => Promise; + handleFormat: () => void; + resetCode: () => void; + clearOutput: () => void; + toggleAutoComplete: () => void; + setCurrentWord: (word: string) => void; + + // Monaco editor ref + editorRef: React.MutableRefObject; + handleEditorMount: (editorInstance: editor.IStandaloneCodeEditor) => void; +} + +// --------------------------------------------------------------------------- +// Mock collaborator data (simulates Socket.IO presence) +// --------------------------------------------------------------------------- + +const MOCK_COLLABORATORS: Collaborator[] = [ + { + id: 'user-1', + name: 'Alice', + color: '#6366f1', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Alice', + cursorLine: 3, + cursorColumn: 10, + }, + { + id: 'user-2', + name: 'Bob', + color: '#ec4899', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Bob', + cursorLine: 7, + cursorColumn: 5, + }, + { + id: 'user-3', + name: 'Charlie', + color: '#10b981', + avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Charlie', + cursorLine: 12, + cursorColumn: 1, + }, +]; + +// --------------------------------------------------------------------------- +// Hook +// --------------------------------------------------------------------------- + +export const useCodeEditor = ({ + initialCode, + initialLanguage = 'javascript', + onCodeChange, +}: UseCodeEditorOptions = {}): UseCodeEditorReturn => { + const langConfig = getLanguageConfig(initialLanguage); + + const [language, setLanguageState] = useState(initialLanguage); + const [theme, setTheme] = useState<'vs-dark' | 'light'>('vs-dark'); + const [fontSize, setFontSize] = useState(14); + const [isRunning, setIsRunning] = useState(false); + const [output, setOutput] = useState(null); + const [validationErrors, setValidationErrors] = useState>([]); + const [collaborators] = useState(MOCK_COLLABORATORS); + const [autoCompleteEnabled, setAutoCompleteEnabled] = useState(true); + const [currentWord, setCurrentWord] = useState(''); + const [code, setCodeState] = useState( + initialCode ?? langConfig.defaultCode, + ); + + const editorRef = useRef(null); + + // ------------------------------------------------------------------------- + // Setters + // ------------------------------------------------------------------------- + + const setCode = useCallback( + (newCode: string) => { + setCodeState(newCode); + onCodeChange?.(newCode); + const result = validateCode(language, newCode); + setValidationErrors(result.errors); + }, + [language, onCodeChange], + ); + + const setLanguage = useCallback( + (lang: string) => { + setLanguageState(lang); + const config = getLanguageConfig(lang); + setCodeState(config.defaultCode); + onCodeChange?.(config.defaultCode); + setOutput(null); + setValidationErrors([]); + }, + [onCodeChange], + ); + + const toggleTheme = useCallback(() => { + setTheme((prev) => (prev === 'vs-dark' ? 'light' : 'vs-dark')); + }, []); + + const increaseFontSize = useCallback(() => { + setFontSize((prev) => Math.min(prev + 2, 28)); + }, []); + + const decreaseFontSize = useCallback(() => { + setFontSize((prev) => Math.max(prev - 2, 10)); + }, []); + + // ------------------------------------------------------------------------- + // Code actions + // ------------------------------------------------------------------------- + + const runCode = useCallback(async () => { + setIsRunning(true); + setOutput(null); + + // Simulate async execution delay + await new Promise((resolve) => setTimeout(resolve, 600)); + const result = simulateCodeExecution(language, code); + setOutput(result); + setIsRunning(false); + }, [language, code]); + + const handleFormat = useCallback(() => { + const formatted = formatCodeUtil(language, code); + setCodeState(formatted); + onCodeChange?.(formatted); + if (editorRef.current) { + editorRef.current.setValue(formatted); + } + }, [language, code, onCodeChange]); + + const resetCode = useCallback(() => { + const config = getLanguageConfig(language); + setCodeState(config.defaultCode); + onCodeChange?.(config.defaultCode); + setOutput(null); + setValidationErrors([]); + if (editorRef.current) { + editorRef.current.setValue(config.defaultCode); + } + }, [language, onCodeChange]); + + const clearOutput = useCallback(() => setOutput(null), []); + + const toggleAutoComplete = useCallback(() => { + setAutoCompleteEnabled((prev) => !prev); + }, []); + + // ------------------------------------------------------------------------- + // Monaco mount handler + // ------------------------------------------------------------------------- + + const handleEditorMount = useCallback( + (editorInstance: editor.IStandaloneCodeEditor) => { + editorRef.current = editorInstance; + + // Track cursor word for auto-completion panel + editorInstance.onDidChangeCursorPosition(() => { + const position = editorInstance.getPosition(); + if (!position) return; + const model = editorInstance.getModel(); + if (!model) return; + const word = model.getWordAtPosition(position); + setCurrentWord(word?.word ?? ''); + }); + }, + [], + ); + + return { + // State + code, + language, + theme, + fontSize, + isRunning, + output, + validationErrors, + collaborators, + autoCompleteEnabled, + currentWord, + + // Config helpers + languages: getAllLanguages(), + languageConfig: getLanguageConfig(language), + + // Actions + setCode, + setLanguage, + toggleTheme, + increaseFontSize, + decreaseFontSize, + runCode, + handleFormat, + resetCode, + clearOutput, + toggleAutoComplete, + setCurrentWord, + + // Monaco ref + editorRef, + handleEditorMount, + }; +}; diff --git a/src/utils/__tests__/codeUtils.test.ts b/src/utils/__tests__/codeUtils.test.ts new file mode 100644 index 0000000..f61aee7 --- /dev/null +++ b/src/utils/__tests__/codeUtils.test.ts @@ -0,0 +1,203 @@ +/** + * Unit Tests for codeUtils + */ +import { describe, it, expect } from 'vitest'; +import { + getAllLanguages, + getLanguageConfig, + getAutoCompletionSuggestions, + formatCode, + validateCode, + simulateCodeExecution, +} from '../codeUtils'; + +// --------------------------------------------------------------------------- +// getAllLanguages +// --------------------------------------------------------------------------- +describe('getAllLanguages', () => { + it('returns an array of at least 5 languages', () => { + const langs = getAllLanguages(); + expect(Array.isArray(langs)).toBe(true); + expect(langs.length).toBeGreaterThanOrEqual(5); + }); + + it('each language has required fields', () => { + getAllLanguages().forEach((lang) => { + expect(lang).toHaveProperty('id'); + expect(lang).toHaveProperty('label'); + expect(lang).toHaveProperty('extension'); + expect(lang).toHaveProperty('monacoLanguage'); + expect(lang).toHaveProperty('color'); + expect(lang).toHaveProperty('defaultCode'); + }); + }); +}); + +// --------------------------------------------------------------------------- +// getLanguageConfig +// --------------------------------------------------------------------------- +describe('getLanguageConfig', () => { + it('returns the correct config for a known language', () => { + const config = getLanguageConfig('javascript'); + expect(config.id).toBe('javascript'); + expect(config.label).toBe('JavaScript'); + expect(config.monacoLanguage).toBe('javascript'); + }); + + it('returns the correct config for python', () => { + const config = getLanguageConfig('python'); + expect(config.id).toBe('python'); + expect(config.extension).toBe('py'); + }); + + it('falls back to the first language for an unknown language id', () => { + const config = getLanguageConfig('cobol'); + expect(config).toBeDefined(); + expect(typeof config.id).toBe('string'); + }); +}); + +// --------------------------------------------------------------------------- +// getAutoCompletionSuggestions +// --------------------------------------------------------------------------- +describe('getAutoCompletionSuggestions', () => { + it('returns an array for a known language', () => { + const suggestions = getAutoCompletionSuggestions('javascript'); + expect(Array.isArray(suggestions)).toBe(true); + expect(suggestions.length).toBeGreaterThan(0); + }); + + it('filters suggestions by prefix (case-insensitive)', () => { + const suggestions = getAutoCompletionSuggestions('javascript', 'con'); + expect(suggestions.length).toBeGreaterThan(0); + suggestions.forEach((s) => { + expect(s.label.toLowerCase()).toMatch(/^con/); + }); + }); + + it('returns empty array when prefix matches nothing', () => { + const suggestions = getAutoCompletionSuggestions('javascript', 'zzz_no_match'); + expect(suggestions).toHaveLength(0); + }); + + it('each suggestion has required fields', () => { + const suggestions = getAutoCompletionSuggestions('python'); + suggestions.forEach((s) => { + expect(s).toHaveProperty('label'); + expect(s).toHaveProperty('kind'); + expect(s).toHaveProperty('detail'); + expect(s).toHaveProperty('insertText'); + }); + }); + + it('falls back gracefully for an unknown language', () => { + const suggestions = getAutoCompletionSuggestions('brainfuck'); + expect(Array.isArray(suggestions)).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// formatCode +// --------------------------------------------------------------------------- +describe('formatCode', () => { + it('trims trailing whitespace from each line', () => { + const code = 'const x = 1; \nconst y = 2; '; + const result = formatCode('javascript', code); + result.split('\n').forEach((line) => { + expect(line).not.toMatch(/ +$/); + }); + }); + + it('collapses multiple consecutive blank lines into one', () => { + const code = 'a\n\n\n\nb'; + const result = formatCode('javascript', code); + expect(result).not.toMatch(/\n{3,}/); + }); + + it('ensures a single trailing newline', () => { + const code = 'const x = 1;'; + const result = formatCode('javascript', code); + expect(result.endsWith('\n')).toBe(true); + }); + + it('converts tabs to 4 spaces for python', () => { + const code = '\tprint("hello")'; + const result = formatCode('python', code); + expect(result.startsWith(' ')).toBe(true); + }); + + it('returns empty string unchanged', () => { + expect(formatCode('javascript', '')).toBe(''); + }); +}); + +// --------------------------------------------------------------------------- +// validateCode +// --------------------------------------------------------------------------- +describe('validateCode', () => { + it('returns invalid for empty code', () => { + const result = validateCode('javascript', ' '); + expect(result.isValid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('returns valid for normal javascript', () => { + const result = validateCode('javascript', 'const x = 1;\nconsole.log(x);'); + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('detects eval() usage in javascript', () => { + const result = validateCode('javascript', 'eval("alert(1)")'); + expect(result.isValid).toBe(false); + expect(result.errors[0].message).toMatch(/eval/i); + }); + + it('detects tab indentation in python', () => { + const result = validateCode('python', '\tprint("hi")'); + expect(result.isValid).toBe(false); + expect(result.errors[0].message).toMatch(/tab/i); + }); + + it('returns valid structure on valid python', () => { + const result = validateCode('python', 'print("hello")'); + expect(result).toHaveProperty('isValid'); + expect(result).toHaveProperty('errors'); + }); +}); + +// --------------------------------------------------------------------------- +// simulateCodeExecution +// --------------------------------------------------------------------------- +describe('simulateCodeExecution', () => { + it('returns a valid ExecutionResult shape', () => { + const result = simulateCodeExecution('javascript', 'console.log("hi")'); + expect(result).toHaveProperty('stdout'); + expect(result).toHaveProperty('stderr'); + expect(result).toHaveProperty('exitCode'); + expect(result).toHaveProperty('executionTimeMs'); + }); + + it('exits with code 1 for empty code', () => { + const result = simulateCodeExecution('javascript', ' '); + expect(result.exitCode).toBe(1); + expect(result.stderr).toBeTruthy(); + }); + + it('extracts the string from console.log for javascript', () => { + const result = simulateCodeExecution('javascript', 'console.log("Hello, World!")'); + expect(result.stdout).toContain('Hello, World!'); + expect(result.exitCode).toBe(0); + }); + + it('extracts the string from print() for python', () => { + const result = simulateCodeExecution('python', 'print("Hello Python")'); + expect(result.stdout).toContain('Hello Python'); + expect(result.exitCode).toBe(0); + }); + + it('returns a non-negative executionTimeMs', () => { + const result = simulateCodeExecution('go', 'package main\nfunc main() {}'); + expect(result.executionTimeMs).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/src/utils/codeUtils.ts b/src/utils/codeUtils.ts new file mode 100644 index 0000000..eae27e6 --- /dev/null +++ b/src/utils/codeUtils.ts @@ -0,0 +1,336 @@ +/** + * Code Editor Utilities + * Provides language configs, auto-completion suggestions, code formatting, + * validation, and simulated code execution for the Advanced Code Editor. + */ + +// --------------------------------------------------------------------------- +// Language registry +// --------------------------------------------------------------------------- + +export interface LanguageConfig { + id: string; + label: string; + extension: string; + monacoLanguage: string; + color: string; + defaultCode: string; +} + +const LANGUAGE_REGISTRY: LanguageConfig[] = [ + { + id: 'javascript', + label: 'JavaScript', + extension: 'js', + monacoLanguage: 'javascript', + color: '#f7df1e', + defaultCode: + '// JavaScript\nconsole.log("Hello, World!");\n', + }, + { + id: 'typescript', + label: 'TypeScript', + extension: 'ts', + monacoLanguage: 'typescript', + color: '#3178c6', + defaultCode: + '// TypeScript\nconst greeting: string = "Hello, World!";\nconsole.log(greeting);\n', + }, + { + id: 'python', + label: 'Python', + extension: 'py', + monacoLanguage: 'python', + color: '#3572A5', + defaultCode: '# Python\nprint("Hello, World!")\n', + }, + { + id: 'java', + label: 'Java', + extension: 'java', + monacoLanguage: 'java', + color: '#b07219', + defaultCode: + '// Java\npublic class Main {\n public static void main(String[] args) {\n System.out.println("Hello, World!");\n }\n}\n', + }, + { + id: 'cpp', + label: 'C++', + extension: 'cpp', + monacoLanguage: 'cpp', + color: '#f34b7d', + defaultCode: + '// C++\n#include \nint main() {\n std::cout << "Hello, World!" << std::endl;\n return 0;\n}\n', + }, + { + id: 'rust', + label: 'Rust', + extension: 'rs', + monacoLanguage: 'rust', + color: '#dea584', + defaultCode: + '// Rust\nfn main() {\n println!("Hello, World!");\n}\n', + }, + { + id: 'go', + label: 'Go', + extension: 'go', + monacoLanguage: 'go', + color: '#00ADD8', + defaultCode: + '// Go\npackage main\n\nimport "fmt"\n\nfunc main() {\n fmt.Println("Hello, World!")\n}\n', + }, + { + id: 'html', + label: 'HTML', + extension: 'html', + monacoLanguage: 'html', + color: '#e34c26', + defaultCode: + '\n\n\n \n Hello\n\n\n

Hello, World!

\n\n\n', + }, + { + id: 'css', + label: 'CSS', + extension: 'css', + monacoLanguage: 'css', + color: '#563d7c', + defaultCode: + '/* CSS */\nbody {\n font-family: sans-serif;\n color: #333;\n}\n', + }, + { + id: 'sql', + label: 'SQL', + extension: 'sql', + monacoLanguage: 'sql', + color: '#e38c00', + defaultCode: + '-- SQL\nSELECT * FROM users WHERE active = 1;\n', + }, +]; + +export const getAllLanguages = (): LanguageConfig[] => LANGUAGE_REGISTRY; + +export const getLanguageConfig = (languageId: string): LanguageConfig => { + return ( + LANGUAGE_REGISTRY.find((l) => l.id === languageId) ?? LANGUAGE_REGISTRY[0] + ); +}; + +// --------------------------------------------------------------------------- +// Auto-completion suggestions +// --------------------------------------------------------------------------- + +export interface CompletionSuggestion { + label: string; + kind: 'keyword' | 'snippet' | 'function' | 'variable'; + detail: string; + insertText: string; +} + +const SUGGESTIONS_MAP: Record = { + javascript: [ + { label: 'console.log', kind: 'function', detail: 'Log to console', insertText: 'console.log($1)' }, + { label: 'const', kind: 'keyword', detail: 'Declare constant', insertText: 'const $1 = $2;' }, + { label: 'let', kind: 'keyword', detail: 'Declare variable', insertText: 'let $1 = $2;' }, + { label: 'function', kind: 'snippet', detail: 'Function declaration', insertText: 'function $1($2) {\n $3\n}' }, + { label: 'arrow function', kind: 'snippet', detail: 'Arrow function', insertText: 'const $1 = ($2) => {\n $3\n};' }, + { label: 'if', kind: 'keyword', detail: 'If statement', insertText: 'if ($1) {\n $2\n}' }, + { label: 'for', kind: 'keyword', detail: 'For loop', insertText: 'for (let $1 = 0; $1 < $2; $1++) {\n $3\n}' }, + { label: 'forEach', kind: 'function', detail: 'Array forEach', insertText: '$1.forEach(($2) => {\n $3\n});' }, + { label: 'Promise', kind: 'snippet', detail: 'Promise constructor', insertText: 'new Promise((resolve, reject) => {\n $1\n})' }, + { label: 'async/await', kind: 'snippet', detail: 'Async function', insertText: 'async function $1($2) {\n const $3 = await $4;\n}' }, + ], + typescript: [ + { label: 'interface', kind: 'keyword', detail: 'Interface declaration', insertText: 'interface $1 {\n $2\n}' }, + { label: 'type', kind: 'keyword', detail: 'Type alias', insertText: 'type $1 = $2;' }, + { label: 'enum', kind: 'keyword', detail: 'Enum declaration', insertText: 'enum $1 {\n $2\n}' }, + { label: 'console.log', kind: 'function', detail: 'Log to console', insertText: 'console.log($1)' }, + { label: 'const', kind: 'keyword', detail: 'Declare constant', insertText: 'const $1: $2 = $3;' }, + { label: 'function', kind: 'snippet', detail: 'Function', insertText: 'function $1($2: $3): $4 {\n $5\n}' }, + { label: 'React.FC', kind: 'snippet', detail: 'React function component', insertText: 'const $1: React.FC<$2> = ($3) => {\n return (\n $4\n );\n};' }, + { label: 'useState', kind: 'function', detail: 'React useState hook', insertText: 'const [$1, set$1] = useState<$2>($3);' }, + ], + python: [ + { label: 'print', kind: 'function', detail: 'Print to stdout', insertText: 'print($1)' }, + { label: 'def', kind: 'keyword', detail: 'Function definition', insertText: 'def $1($2):\n $3' }, + { label: 'class', kind: 'keyword', detail: 'Class definition', insertText: 'class $1:\n def __init__(self):\n $2' }, + { label: 'if', kind: 'keyword', detail: 'If statement', insertText: 'if $1:\n $2' }, + { label: 'for', kind: 'keyword', detail: 'For loop', insertText: 'for $1 in $2:\n $3' }, + { label: 'import', kind: 'keyword', detail: 'Import module', insertText: 'import $1' }, + { label: 'list comprehension', kind: 'snippet', detail: 'List comprehension', insertText: '[$1 for $2 in $3]' }, + { label: 'lambda', kind: 'keyword', detail: 'Lambda function', insertText: 'lambda $1: $2' }, + ], + java: [ + { label: 'System.out.println', kind: 'function', detail: 'Print line', insertText: 'System.out.println($1);' }, + { label: 'public class', kind: 'snippet', detail: 'Class declaration', insertText: 'public class $1 {\n $2\n}' }, + { label: 'public static void main', kind: 'snippet', detail: 'Main method', insertText: 'public static void main(String[] args) {\n $1\n}' }, + { label: 'for', kind: 'keyword', detail: 'For loop', insertText: 'for (int $1 = 0; $1 < $2; $1++) {\n $3\n}' }, + { label: 'ArrayList', kind: 'snippet', detail: 'ArrayList declaration', insertText: 'ArrayList<$1> $2 = new ArrayList<>();' }, + ], + rust: [ + { label: 'println!', kind: 'function', detail: 'Print macro', insertText: 'println!("{}", $1);' }, + { label: 'fn', kind: 'keyword', detail: 'Function', insertText: 'fn $1($2) -> $3 {\n $4\n}' }, + { label: 'let', kind: 'keyword', detail: 'Variable binding', insertText: 'let $1 = $2;' }, + { label: 'let mut', kind: 'keyword', detail: 'Mutable binding', insertText: 'let mut $1 = $2;' }, + { label: 'match', kind: 'keyword', detail: 'Match expression', insertText: 'match $1 {\n $2 => $3,\n _ => $4,\n}' }, + { label: 'struct', kind: 'keyword', detail: 'Struct definition', insertText: 'struct $1 {\n $2: $3,\n}' }, + ], + go: [ + { label: 'fmt.Println', kind: 'function', detail: 'Print line', insertText: 'fmt.Println($1)' }, + { label: 'func', kind: 'keyword', detail: 'Function', insertText: 'func $1($2) $3 {\n $4\n}' }, + { label: 'var', kind: 'keyword', detail: 'Variable declaration', insertText: 'var $1 $2 = $3' }, + { label: 'for', kind: 'keyword', detail: 'For loop', insertText: 'for $1 := 0; $1 < $2; $1++ {\n $3\n}' }, + { label: 'goroutine', kind: 'snippet', detail: 'Goroutine', insertText: 'go func() {\n $1\n}()' }, + ], +}; + +export const getAutoCompletionSuggestions = ( + languageId: string, + word: string = '', +): CompletionSuggestion[] => { + const all = SUGGESTIONS_MAP[languageId] ?? SUGGESTIONS_MAP['javascript']; + if (!word) return all; + const lower = word.toLowerCase(); + return all.filter((s) => s.label.toLowerCase().startsWith(lower)); +}; + +// --------------------------------------------------------------------------- +// Code formatting +// --------------------------------------------------------------------------- + +export const formatCode = (languageId: string, code: string): string => { + if (!code) return code; + + // Universal: trim trailing whitespace per line + const lines = code.split('\n').map((line) => line.trimEnd()); + + // Remove consecutive blank lines (max 1) + const normalized: string[] = []; + let prevBlank = false; + for (const line of lines) { + const isBlank = line.trim() === ''; + if (isBlank && prevBlank) continue; + normalized.push(line); + prevBlank = isBlank; + } + + // Ensure single trailing newline + const result = normalized.join('\n').trimEnd() + '\n'; + + // Python: enforce 4-space indentation (convert tabs → spaces) + if (languageId === 'python') { + return result + .split('\n') + .map((l) => l.replace(/^\t+/, (t) => ' '.repeat(t.length))) + .join('\n'); + } + + return result; +}; + +// --------------------------------------------------------------------------- +// Code validation +// --------------------------------------------------------------------------- + +export interface ValidationResult { + isValid: boolean; + errors: Array<{ line: number; message: string }>; +} + +export const validateCode = (languageId: string, code: string): ValidationResult => { + const errors: Array<{ line: number; message: string }> = []; + + if (!code.trim()) { + return { isValid: false, errors: [{ line: 1, message: 'Code is empty' }] }; + } + + const lines = code.split('\n'); + + if (languageId === 'javascript' || languageId === 'typescript') { + lines.forEach((line, i) => { + // Very lightweight checks + if (/\bconsole\.log\s*\(/.test(line) && !line.trimEnd().endsWith(';') && !line.trimEnd().endsWith(',')) { + // not enforced as error — just a style note, skip + } + if (/^\s*eval\s*\(/.test(line)) { + errors.push({ line: i + 1, message: 'Avoid using eval() — security risk' }); + } + }); + } + + if (languageId === 'python') { + lines.forEach((line, i) => { + if (/^\t/.test(line)) { + errors.push({ line: i + 1, message: 'Use spaces instead of tabs for indentation' }); + } + }); + } + + if (languageId === 'sql') { + if (!/SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER/i.test(code)) { + errors.push({ line: 1, message: 'No recognizable SQL statement found' }); + } + } + + return { isValid: errors.length === 0, errors }; +}; + +// --------------------------------------------------------------------------- +// Simulated code execution +// --------------------------------------------------------------------------- + +export interface ExecutionResult { + stdout: string; + stderr: string; + exitCode: number; + executionTimeMs: number; +} + +const EXECUTION_TEMPLATES: Record ExecutionResult> = { + javascript: (code) => { + const match = code.match(/console\.log\s*\((['"`])(.*?)\1\)/); + return { + stdout: match ? match[2] : 'Script executed successfully.', + stderr: '', + exitCode: 0, + executionTimeMs: Math.floor(Math.random() * 50) + 10, + }; + }, + python: (code) => { + const match = code.match(/print\s*\((['"])(.*?)\1\)/); + return { + stdout: match ? match[2] : 'Script executed successfully.', + stderr: '', + exitCode: 0, + executionTimeMs: Math.floor(Math.random() * 80) + 20, + }; + }, +}; + +export const simulateCodeExecution = ( + languageId: string, + code: string, +): ExecutionResult => { + const validation = validateCode(languageId, code); + + if (!validation.isValid && validation.errors[0]?.message === 'Code is empty') { + return { + stdout: '', + stderr: 'Error: No code to execute.', + exitCode: 1, + executionTimeMs: 0, + }; + } + + const template = EXECUTION_TEMPLATES[languageId]; + if (template) return template(code); + + // Generic fallback for compiled languages (simulated) + return { + stdout: `[Simulated] ${getLanguageConfig(languageId).label} program ran successfully.\nHello, World!`, + stderr: '', + exitCode: 0, + executionTimeMs: Math.floor(Math.random() * 200) + 50, + }; +};