feat(editor): Monaco syntax highlight for 8080/Z80 assembly

When the editor opens a .s or .asm file (the chip-program files routed
to /api/compile-rom), Monaco now colorizes 8080/Z80 mnemonics, registers,
hex/binary literals, comments, and directives. Same highlighter covers
both ISAs since most mnemonics overlap.

- frontend/src/components/editor/retroAsmLanguage.ts: a Monarch tokenizer
  + LanguageConfiguration + idempotent registration helper. Recognises
  the full 8080 ISA, all the Z80 additions (LD/JR/DJNZ/EXX/EX/IM/LDIR/
  bit ops/index ops), the directives ORG/DB/DW/EQU/END, and registers
  including condition codes (NZ/Z/NC/etc.) and IX/IY.

- CodeEditor.tsx: maps `.s` and `.asm` to the new `retro-asm` language
  and calls `registerRetroAsm(monaco)` in beforeMount so the language
  exists by the time the editor first paints. Other extensions
  (.ino/.cpp/.c/.py/.json/.md) behave exactly as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero Crespo 2026-05-19 00:49:51 -03:00
parent 0e2f0790db
commit 321997715d
2 changed files with 149 additions and 0 deletions

View File

@ -1,12 +1,15 @@
import Editor from '@monaco-editor/react';
import { useEditorStore } from '../../store/useEditorStore';
import { registerRetroAsm, LANGUAGE_ID as RETRO_ASM_ID } from './retroAsmLanguage';
function getLanguage(filename: string): string {
const ext = filename.split('.').pop()?.toLowerCase() ?? '';
if (ext === 's' || ext === 'asm') return RETRO_ASM_ID;
if (['ino', 'cpp', 'c', 'cc', 'h', 'hpp'].includes(ext)) return 'cpp';
if (ext === 'py') return 'python';
if (ext === 'json') return 'json';
if (ext === 'md') return 'markdown';
if (ext === 'hex') return 'plaintext';
return 'plaintext';
}
@ -23,6 +26,11 @@ export const CodeEditor = () => {
language={activeFile ? getLanguage(activeFile.name) : 'cpp'}
theme={theme}
value={activeFile?.content ?? ''}
beforeMount={(monaco) => {
// Register the 8080/Z80 assembly language once so Monaco knows how
// to tokenize .s / .asm files when they're opened.
registerRetroAsm(monaco);
}}
onChange={(value) => {
if (activeFileId) setFileContent(activeFileId, value || '');
}}

View File

@ -0,0 +1,141 @@
/**
* Monaco language definition for retro CPU assembly (Intel 8080 + Zilog Z80).
*
* Used when the editor opens a .s / .asm file in a Velxio project the
* `/api/compile-rom` backend then assembles it into a ROM image for the
* programmable i8080-cpu / z80-cpu chip.
*
* Covers the union of 8080 + Z80 mnemonics so a single highlighter works
* for both targets. Directives (ORG / DB / DW / EQU), labels, hex/binary
* literals, character constants, and ; comments are recognised.
*/
import type * as monacoNs from 'monaco-editor';
export const LANGUAGE_ID = 'retro-asm';
const MNEMONICS_8080 = [
// Loads / moves
'MOV', 'MVI', 'LXI', 'LDA', 'STA', 'LDAX', 'STAX', 'LHLD', 'SHLD', 'XCHG',
// Stack / sp
'PUSH', 'POP', 'XTHL', 'SPHL',
// ALU r / immediate
'ADD', 'ADC', 'SUB', 'SBB', 'ANA', 'XRA', 'ORA', 'CMP',
'ADI', 'ACI', 'SUI', 'SBI', 'ANI', 'XRI', 'ORI', 'CPI',
// Inc/Dec
'INR', 'DCR', 'INX', 'DCX', 'DAD',
// Rotates / misc
'RLC', 'RRC', 'RAL', 'RAR', 'CMA', 'STC', 'CMC', 'DAA',
// Control flow
'JMP', 'JC', 'JNC', 'JZ', 'JNZ', 'JP', 'JM', 'JPO', 'JPE', 'PCHL',
'CALL', 'CC', 'CNC', 'CZ', 'CNZ', 'CP', 'CM', 'CPO', 'CPE',
'RET', 'RC', 'RNC', 'RZ', 'RNZ', 'RP', 'RM', 'RPO', 'RPE',
'RST', 'NOP', 'HLT',
// Interrupts / IO
'EI', 'DI', 'IN', 'OUT',
];
const MNEMONICS_Z80_EXTRA = [
// Z80-only additions on top of 8080-equivalent shapes
'LD', 'JR', 'DJNZ', 'EX', 'EXX', 'HALT',
// CB-prefix bit ops
'BIT', 'SET', 'RES', 'RL', 'RR', 'SLA', 'SRA', 'SRL',
// ED-prefix
'NEG', 'RETI', 'RETN', 'LDI', 'LDIR', 'LDD', 'LDDR', 'CPI', 'CPIR', 'CPD', 'CPDR',
'INI', 'INIR', 'IND', 'INDR', 'OUTI', 'OTIR', 'OUTD', 'OTDR',
'IM',
// Index-register helpers (DD/FD prefix)
'IX', 'IY',
];
const DIRECTIVES = ['ORG', 'DB', 'DW', 'DS', 'EQU', 'END', 'INCLUDE', 'MACRO', 'ENDM'];
const REGISTERS = [
// 8-bit
'A', 'B', 'C', 'D', 'E', 'H', 'L', 'F',
// 8080 alt
'M', 'PSW',
// 16-bit
'AF', 'BC', 'DE', 'HL', 'SP', 'PC', 'IX', 'IY', 'IR',
// Z80 condition codes (also keywords in some contexts)
'NZ', 'Z', 'NC', 'PO', 'PE',
];
export const LANGUAGE_CONFIG: monacoNs.languages.LanguageConfiguration = {
comments: { lineComment: ';' },
brackets: [['(', ')']],
autoClosingPairs: [
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string'] },
{ open: "'", close: "'", notIn: ['string'] },
],
surroundingPairs: [
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
};
export const MONARCH_TOKENIZER: monacoNs.languages.IMonarchLanguage = {
ignoreCase: true,
defaultToken: '',
tokenPostfix: '.retro-asm',
keywords: [...MNEMONICS_8080, ...MNEMONICS_Z80_EXTRA],
directives: DIRECTIVES,
registers: REGISTERS,
tokenizer: {
root: [
// Label at start of line.
[/^[ \t]*([A-Za-z_$][\w$]*)\s*:/, 'type.identifier'],
// Comments.
[/;.*$/, 'comment'],
// Char literal: 'X' (single char).
[/'(?:\\.|[^'])'/, 'string'],
// String literal in DB.
[/"[^"]*"/, 'string'],
// Hex literals — both 0xNN and NNh.
[/0[xX][0-9A-Fa-f]+/, 'number.hex'],
[/[0-9A-Fa-f]+[hH]\b/, 'number.hex'],
// Binary literals — 0b... or NNb
[/0[bB][01]+/, 'number.binary'],
[/[01]+[bB]\b/, 'number.binary'],
// Decimal literals.
[/\d+/, 'number'],
// Identifiers — disambiguate keyword vs register vs directive vs label use.
[/[A-Za-z_$][\w$]*/, {
cases: {
'@keywords': 'keyword',
'@directives': 'keyword.control',
'@registers': 'variable.predefined',
'@default': 'identifier',
},
}],
// Symbols + operators
[/[,:()+\-*/&|^~!]/, 'delimiter'],
[/[ \t]+/, ''],
],
},
};
/** One-shot registration. Call once on app boot from Monaco's beforeMount. */
export function registerRetroAsm(monaco: typeof monacoNs): void {
// Languages are global — don't re-register if already there.
const langs = monaco.languages.getLanguages();
if (langs.some((l) => l.id === LANGUAGE_ID)) return;
monaco.languages.register({
id: LANGUAGE_ID,
extensions: ['.s', '.asm'],
aliases: ['Retro Assembly', 'retro-asm', '8080 asm', 'Z80 asm'],
});
monaco.languages.setLanguageConfiguration(LANGUAGE_ID, LANGUAGE_CONFIG);
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, MONARCH_TOKENIZER);
}