Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
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<string | null>(null);
const [currentFile, setCurrentFile] = useState<string>('');
const [content, setContent] = useState<string>('');
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?')) {

Check warning on line 19 in src/renderer/App.tsx

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

Unexpected confirm

Check warning on line 19 in src/renderer/App.tsx

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

Unexpected confirm
return;
}
}
Expand All @@ -21,7 +26,7 @@
setContent(text);
setIsDirty(false);
} catch (err) {
console.error(err);

Check warning on line 29 in src/renderer/App.tsx

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

Unexpected console statement

Check warning on line 29 in src/renderer/App.tsx

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

Unexpected console statement
}
};

Expand Down Expand Up @@ -68,6 +73,7 @@
<CodeEditor
value={content}
onChange={handleContentChange}
syntaxStrategy={strategy}
/>
</>
) : (
Expand Down
26 changes: 13 additions & 13 deletions src/renderer/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div style={{ height: '100%', width: '100%', overflow: 'auto' }}>
<CodeMirror
value={value}
height="100%"
theme={oneDark}
extensions={[javascript({ jsx: true })]}
onChange={(val) => onChange(val)}
/>
</div>
<CodeMirror
value={value}
theme={'none'}
extensions={syntaxStrategy.getLanguageParser()}
onChange={(val) => onChange(val)}
/>
);
}
Original file line number Diff line number Diff line change
@@ -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';
}
}
125 changes: 125 additions & 0 deletions src/renderer/strategy/syntax-highlighting/JavaSyntaxStrategy.ts
Original file line number Diff line number Diff line change
@@ -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';
}
}
Original file line number Diff line number Diff line change
@@ -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';
}
}
30 changes: 30 additions & 0 deletions src/renderer/strategy/syntax-highlighting/SyntaxFactory.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
10 changes: 10 additions & 0 deletions src/renderer/strategy/syntax-highlighting/SyntaxStrategy.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading