diff --git a/package.json b/package.json index 6e8211a..68f59c4 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ }, "dependencies": { "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/language": "^6.10.0", "@codemirror/theme-one-dark": "^6.1.3", "@electron/notarize": "^3.0.0", "@uiw/react-codemirror": "^4.25.3", diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 4e2aff5..cde2682 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,14 +1,19 @@ import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import './App.css'; import FileExplorer from './components/FileExplorer'; import CodeEditor from './components/CodeEditor'; +import { SyntaxFactory } from './strategy/syntax-highlighting/SyntaxFactory'; function EditorLayout() { - const [currentFile, setCurrentFile] = useState(null); + const [currentFile, setCurrentFile] = useState(''); const [content, setContent] = useState(''); const [isDirty, setIsDirty] = useState(false); + const strategy = useMemo(() => { + return SyntaxFactory.createSyntaxStrategy(currentFile); + }, [currentFile]); + const handleSelectFile = async (path: string) => { if (isDirty) { if (!window.confirm('You have unsaved changes. Discard them?')) { @@ -68,6 +73,7 @@ function EditorLayout() { ) : ( diff --git a/src/renderer/components/CodeEditor.tsx b/src/renderer/components/CodeEditor.tsx index eacbe58..6ad1914 100644 --- a/src/renderer/components/CodeEditor.tsx +++ b/src/renderer/components/CodeEditor.tsx @@ -1,23 +1,23 @@ -import React from 'react'; import CodeMirror from '@uiw/react-codemirror'; -import { javascript } from '@codemirror/lang-javascript'; -import { oneDark } from '@codemirror/theme-one-dark'; +import { SyntaxStrategy } from '../strategy/syntax-highlighting/SyntaxStrategy'; interface CodeEditorProps { value: string; onChange: (val: string) => void; + syntaxStrategy: SyntaxStrategy; } -export default function CodeEditor({ value, onChange }: CodeEditorProps) { +export default function CodeEditor({ + value, + onChange, + syntaxStrategy, +}: CodeEditorProps) { return ( -
- onChange(val)} - /> -
+ onChange(val)} + /> ); } diff --git a/src/renderer/strategy/syntax-highlighting/AssemblySyntaxStrategy.ts b/src/renderer/strategy/syntax-highlighting/AssemblySyntaxStrategy.ts new file mode 100644 index 0000000..c04ab4e --- /dev/null +++ b/src/renderer/strategy/syntax-highlighting/AssemblySyntaxStrategy.ts @@ -0,0 +1,54 @@ +import { Extension } from '@codemirror/state'; +import { StreamLanguage, StringStream } from '@codemirror/language'; +import { SyntaxStrategy } from './SyntaxStrategy'; + +export class AssemblySyntaxStrategy implements SyntaxStrategy { + parser = { + token(stream: StringStream) { + if (stream.eatSpace()) return null; + + // Comments + if (stream.peek() === ';' || stream.peek() === '#') { + stream.skipToEnd(); + return 'comment'; + } + + // Numbers (hex and decimal) + if (stream.match(/^0x[0-9a-fA-F]+/) || stream.match(/^\d+/)) { + return 'number'; + } + + // Strings + if (stream.eat('"')) { + while (!stream.eol()) { + if (stream.eat('"')) break; + stream.next(); + } + return 'string'; + } + + // Registers and Instructions + // Simple heuristic: 2-4 letter words are likely instructions or registers + if (stream.match(/^[a-z_][\w\.]*/i)) { + return 'keyword'; + } + + // Labels + if (stream.peek() === ':') { + stream.next(); + return 'labelName'; + } + + stream.next(); + return null; + }, + }; + + public getLanguageParser(): Extension[] { + return [StreamLanguage.define(this.parser)]; + } + + public getLanguage(): string { + return 'Assembly'; + } +} diff --git a/src/renderer/strategy/syntax-highlighting/JavaSyntaxStrategy.ts b/src/renderer/strategy/syntax-highlighting/JavaSyntaxStrategy.ts new file mode 100644 index 0000000..2152ac7 --- /dev/null +++ b/src/renderer/strategy/syntax-highlighting/JavaSyntaxStrategy.ts @@ -0,0 +1,125 @@ +import { Extension } from '@codemirror/state'; +import { SyntaxStrategy } from './SyntaxStrategy'; +import { StreamLanguage, StringStream } from '@codemirror/language'; + +export class JavaSyntaxStrategy implements SyntaxStrategy { + parser = { + token(stream: StringStream) { + if (stream.eatSpace()) return null; + + // --- 1. Comments (Simplified: only line comments, ignores /** */ Javadoc) --- + if (stream.match('//')) { + stream.skipToEnd(); + return 'comment'; + } + + // --- 2. Strings (Simplified: only double quotes, ignores escapes and multi-line strings) --- + if (stream.eat('"')) { + while (!stream.eol() && stream.next() !== '"') {} + return 'string'; + } + + // --- 3. Characters (Single quotes) --- + if (stream.eat("'")) { + // Usually consumes an optional escape character and then the character and the closing quote + stream.next(); + if (stream.peek() === '\\') stream.next(); // simple escape + stream.next(); // the closing ' + return 'string-2'; // CodeMirror often uses 'string-2' for char literals + } + + // --- 4. Numbers (Decimal, Hex, Octal, Floats) --- + if ( + stream.match( + /^(?:0x[0-9a-fA-F]+|0[0-7]+|\d*\.?\d+(?:e[+-]?\d+)?)[lLfF]?/i, + ) + ) { + // The [lLfF]? handles optional suffixes for long, float, and double + return 'number'; + } + + // --- 5. Keywords, Identifiers, and Built-ins --- + // Match any word-like token + if (stream.match(/^[a-zA-Z_$][\w$]*/)) { + const word = stream.current(); + + // A. Keywords + const keywords = [ + 'public', + 'protected', + 'private', + 'class', + 'interface', + 'abstract', + 'final', + 'static', + 'void', + 'new', + 'return', + 'if', + 'else', + 'for', + 'while', + 'do', + 'try', + 'catch', + 'finally', + 'throw', + 'throws', + 'package', + 'import', + 'instanceof', + 'super', + 'this', + 'enum', + 'record', + ]; + if (keywords.includes(word)) { + return 'keyword'; + } + + // B. Primitive Types + const primitiveTypes = [ + 'int', + 'long', + 'short', + 'byte', + 'float', + 'double', + 'boolean', + 'char', + ]; + if (primitiveTypes.includes(word)) { + return 'typeName'; // CodeMirror class for data types + } + + // C. Atoms (Literals) + const atoms = ['true', 'false', 'null']; + if (atoms.includes(word)) { + return 'atom'; + } + + // D. Everything else is a variable, class name, or method name + return 'variable'; + } + + // --- 6. Operators and Punctuation (Single and Multi-char) --- + // This handles things like ==, ++, {, }, ;, etc. + if (stream.match(/^(?:[+\-*\/%&|^!~=<>?]{1,3}|[\[\]{}();,.:@])/)) { + return 'operator'; + } + + // --- 7. Fallback --- + stream.next(); + return null; + }, + }; + + public getLanguageParser(): Extension[] { + return [StreamLanguage.define(this.parser)]; + } + + public getLanguage(): string { + return 'Java'; + } +} diff --git a/src/renderer/strategy/syntax-highlighting/JavascriptSyntaxStrategy.ts b/src/renderer/strategy/syntax-highlighting/JavascriptSyntaxStrategy.ts new file mode 100644 index 0000000..c28a7bf --- /dev/null +++ b/src/renderer/strategy/syntax-highlighting/JavascriptSyntaxStrategy.ts @@ -0,0 +1,96 @@ +import { Extension } from '@codemirror/state'; +import { SyntaxStrategy } from './SyntaxStrategy'; +import { StreamLanguage, StringStream } from '@codemirror/language'; + +export class JavascriptSyntaxStrategy implements SyntaxStrategy { + parser = { + token(stream: StringStream) { + if (stream.eatSpace()) return null; + + // --- 1. Comments (Simplified: only line comments) --- + if (stream.match('//')) { + stream.skipToEnd(); + return 'comment'; + } + + // --- 2. Strings (Simplified: only " and ' - ignores escapes and template literals) --- + // Double-quoted strings + if (stream.eat('"')) { + while (!stream.eol() && stream.next() !== '"') {} + return 'string'; + } + // Single-quoted strings + if (stream.eat("'")) { + while (!stream.eol() && stream.next() !== "'") {} + return 'string'; + } + + // --- 3. Numbers (Hex, Binary, Decimal) --- + if ( + stream.match( + /^(?:0x[0-9a-fA-F]+|0b[01]+|\d*\.?\d+(?:e[+-]?\d+)?)/i, + ) + ) { + return 'number'; + } + + // --- 4. Keywords, Identifiers, and Built-ins --- + // Match any word-like token + if (stream.match(/^[a-zA-Z_$][\w$]*/)) { + const word = stream.current(); + + // A. Keywords + const keywords = [ + 'function', + 'var', + 'const', + 'let', + 'return', + 'if', + 'else', + 'for', + 'while', + 'class', + 'new', + 'this', + 'import', + 'export', + 'await', + 'async', + ]; + if (keywords.includes(word)) { + return 'keyword'; + } + + // B. Atoms (Literals) + const atoms = ['true', 'false', 'null', 'undefined']; + if (atoms.includes(word)) { + return 'atom'; + } + + // C. Everything else is a variable/identifier + return 'variable'; + } + + // --- 5. Operators and Punctuation (Single and Multi-char) --- + // This handles things like ===, +=, ++, {, }, ;, etc. + if (stream.match(/^(?:[+\-*\/%&|^!~=<>?]{1,3}|[\[\]{}();,.:])/)) { + // A more specific tokenizer might assign classes like 'operator', 'bracket', 'punctuation' + return 'operator'; + } + + // --- 6. Fallback --- + // Consume one character and stop. + stream.next(); + return null; + }, + }; + + public getLanguageParser(): Extension[] { + return [StreamLanguage.define(this.parser)]; + } + + public getLanguage(): string { + return 'Javascript/Typescript'; + } +} diff --git a/src/renderer/strategy/syntax-highlighting/SyntaxFactory.ts b/src/renderer/strategy/syntax-highlighting/SyntaxFactory.ts new file mode 100644 index 0000000..75237dc --- /dev/null +++ b/src/renderer/strategy/syntax-highlighting/SyntaxFactory.ts @@ -0,0 +1,30 @@ +import { SyntaxStrategy } from './SyntaxStrategy'; +import { JavascriptSyntaxStrategy } from './JavascriptSyntaxStrategy'; +import { AssemblySyntaxStrategy } from './AssemblySyntaxStrategy'; +import { TxtSyntaxStrategy } from './TxtSyntaxStrategy'; +import { JavaSyntaxStrategy } from './JavaSyntaxStrategy'; + +export class SyntaxFactory { + public static createSyntaxStrategy(fileName: string): SyntaxStrategy { + let strategy: SyntaxStrategy; + + if ( + fileName.endsWith('.js') || + fileName.endsWith('.jsx') || + fileName.endsWith('.ts') || + fileName.endsWith('.tsx') + ) { + strategy = new JavascriptSyntaxStrategy(); + } else if (fileName.endsWith('.asm') || fileName.endsWith('.s')) { + strategy = new AssemblySyntaxStrategy(); + } else if (fileName.endsWith('.txt')) { + strategy = new TxtSyntaxStrategy(); + } else if (fileName.endsWith('.java')) { + strategy = new JavaSyntaxStrategy(); + } else { + strategy = new TxtSyntaxStrategy(); + } + + return strategy; + } +} diff --git a/src/renderer/strategy/syntax-highlighting/SyntaxStrategy.ts b/src/renderer/strategy/syntax-highlighting/SyntaxStrategy.ts new file mode 100644 index 0000000..317b8bd --- /dev/null +++ b/src/renderer/strategy/syntax-highlighting/SyntaxStrategy.ts @@ -0,0 +1,10 @@ +import { StringStream } from '@codemirror/language'; +import { Extension } from '@codemirror/state'; + +export interface SyntaxStrategy { + parser: { + token(stream: StringStream): string | null; + }; + getLanguageParser(): Extension[]; + getLanguage(): string; +} diff --git a/src/renderer/strategy/syntax-highlighting/TxtSyntaxStrategy.ts b/src/renderer/strategy/syntax-highlighting/TxtSyntaxStrategy.ts new file mode 100644 index 0000000..e74401f --- /dev/null +++ b/src/renderer/strategy/syntax-highlighting/TxtSyntaxStrategy.ts @@ -0,0 +1,22 @@ +import { Extension } from '@codemirror/state'; +import { StreamLanguage, StringStream } from '@codemirror/language'; +import { SyntaxStrategy } from './SyntaxStrategy'; + +export class TxtSyntaxStrategy implements SyntaxStrategy { + parser = { + token(stream: StringStream) { + if (stream.eatSpace()) return null; + + stream.next(); + return null; + }, + }; + + public getLanguageParser(): Extension[] { + return [StreamLanguage.define(this.parser)]; + } + + public getLanguage(): string { + return 'Txt'; + } +}