From e0b0622a0aa992d1888db8f38149f2118ba38046 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Fri, 27 Mar 2026 20:58:57 -0700 Subject: [PATCH 01/40] 6502/Z80 kitchensink (commented out) - c64/kitchensink.dasm and includes - zx/kitchensink.zmac and includes --- presets/c64/kitchensink.dasm | 598 ++++++++++++++++++ presets/c64/kitchensink/kitchensink.bin | 1 + presets/c64/kitchensink2.dasm | 1 + presets/zx/kitchensink.zmac | 777 ++++++++++++++++++++++++ presets/zx/kitchensink2.zmac | 1 + presets/zx/kitchensink3.lib | 1 + src/platform/c64.ts | 3 +- src/platform/zx.ts | 1 + 8 files changed, 1382 insertions(+), 1 deletion(-) create mode 100644 presets/c64/kitchensink.dasm create mode 100644 presets/c64/kitchensink/kitchensink.bin create mode 100644 presets/c64/kitchensink2.dasm create mode 100644 presets/zx/kitchensink.zmac create mode 100644 presets/zx/kitchensink2.zmac create mode 100644 presets/zx/kitchensink3.lib diff --git a/presets/c64/kitchensink.dasm b/presets/c64/kitchensink.dasm new file mode 100644 index 000000000..ad3666935 --- /dev/null +++ b/presets/c64/kitchensink.dasm @@ -0,0 +1,598 @@ +; =========================================================================== +; kitchensync.dasm - 6502 grammar Kitchen Sink +; =========================================================================== +; 6502 parser and formatter test +; =========================================================================== +; TODO: Uncomment `;;;` lines once DASM is updated + +; ------------------------------------------------------------------- +; PROCESSOR +; ------------------------------------------------------------------- + processor 6502 + PROCESSOR 6502 + .processor 6502 + .PROCESSOR 6502 + +; ------------------------------------------------------------------- +; Segment: uninitialized variables +; ------------------------------------------------------------------- + seg.u variables +;;; .seg.u variables + SEG.U variables +;;; .SEG.U variables + org $80 +var1 ds 1 +var2 ds 2 +var3 ds.w 1 + +; ------------------------------------------------------------------- +; Segments (SEG, SEG.U) +; ------------------------------------------------------------------- + seg code + .seg code + SEG code + .SEG code + + +; =================================================================== +; Runnable entry point +; =================================================================== + org $0810 +start jmp start ; infinite loop so program can run + +; ------------------------------------------------------------------- +; Labels: global, global with colon, local (.name) +; ------------------------------------------------------------------- +GlobalLabel +GlobalColon: + SUBROUTINE +.local +.temp nop + +; ------------------------------------------------------------------- +; Instructions: every 6502 opcode (lowercase) +; ------------------------------------------------------------------- + adc #$01 + and #$FF +;;; asl a + asl + bcc .temp + bcs .temp + beq .temp + bit $44 + bmi .temp + bne .temp + bpl .temp + brk + bvc .temp + bvs .temp + clc + cld + cli + clv + cmp #$00 + cpx #$10 + cpy #$20 + dec $44 + dex + dey + eor #$AA + inc $44 + inx + iny + jmp start + jsr start + lda #$42 + ldx #$00 + ldy #$00 +;;; lsr a + nop + ora #$0F + pha + php + pla + plp +;;; rol a +;;; ror a + rti + rts + sbc #$01 + sec + sed + sei + sta $44 + stx $45 + sty $46 + tax + tay + tsx + txa + txs + tya + +; ------------------------------------------------------------------- +; Instructions: every 6502 opcode (UPPERCASE) +; ------------------------------------------------------------------- + ADC #$02 + AND #$FE +;;; ASL A + BCC .temp + BCS .temp + BEQ .temp + BIT $44 + BMI .temp + BNE .temp + BPL .temp + BRK + BVC .temp + BVS .temp + CLC + CLD + CLI + CLV + CMP #$01 + CPX #$11 + CPY #$21 + DEC $44 + DEX + DEY + EOR #$BB + INC $44 + INX + INY + JMP start + JSR start + LDA #$43 + LDX #$01 + LDY #$01 +;;; LSR A + NOP + ORA #$F0 + PHA + PHP + PLA + PLP +;;; ROL A +;;; ROR A + RTI + RTS + SBC #$02 + SEC + SED + SEI + STA $44 + STX $45 + STY $46 + TAX + TAY + TSX + TXA + TXS + TYA + +; ------------------------------------------------------------------- +; Addressing modes +; ------------------------------------------------------------------- + lda #$FF ; immediate + lda $44 ; zero page + lda $44,x ; zero page,X + ldx $44,y ; zero page,Y + lda $1234 ; absolute + lda $1234,x ; absolute,X + lda $1234,y ; absolute,Y + lda ($44,x) ; indirect,X + lda ($44),y ; indirect,Y + jmp ($FFFE) ; indirect + +; ------------------------------------------------------------------- +; Register operands (all cases) +; ------------------------------------------------------------------- +;;; asl a +;;; asl A + +; ------------------------------------------------------------------- +; Number formats +; ------------------------------------------------------------------- + lda #$FF ; hexadecimal + lda #%10101010 ; binary + lda #255 ; decimal + lda #'A ; character constant + lda #0 ; zero + +; ------------------------------------------------------------------- +; Current address (.) +; ------------------------------------------------------------------- + jmp . ; current address + +; ------------------------------------------------------------------- +; Pseudo-ops: ORG +; ------------------------------------------------------------------- + org $0900 + ORG $0A00 + +; ------------------------------------------------------------------- +; Pseudo-ops: EQU / = assignment +; ------------------------------------------------------------------- +CONST1 equ $10 +CONST2 EQU $20 +CONST3 = $30 +CONST4 = CONST1 + CONST2 + +; ------------------------------------------------------------------- +; Pseudo-ops: SET (reassignable) +; ------------------------------------------------------------------- +COUNTER set 0 +COUNTER set COUNTER + 1 +COUNTER SET COUNTER + 1 + +; ------------------------------------------------------------------- +; Pseudo-ops: EQM (expression macro) +; ------------------------------------------------------------------- +DOUBLE eqm . * 2 +DOUBLE EQM . * 2 + +; ------------------------------------------------------------------- +; Pseudo-ops: SETSTR +; ------------------------------------------------------------------- +;;;NAME setstr "Hello" +;;;NAME SETSTR "World" + +; ------------------------------------------------------------------- +; Data: BYTE / WORD / LONG (all size extensions) +; ------------------------------------------------------------------- + BYTE $EE,$FF + word $ABCD + WORD $1234 + long $12345678 + LONG $DEADBEEF + +; ------------------------------------------------------------------- +; Data: HEX +; ------------------------------------------------------------------- + hex 1A2B3C + hex FF 00 AA BB + HEX DEADBEEF + HEX 01 02 03 04 + +; ------------------------------------------------------------------- +; Data: DC (all size extensions) +; ------------------------------------------------------------------- + dc 1,2,3 + dc.b $AA,$BB + dc.w $1234,$5678 + dc.l $12345678 + dc.s "AB" + DC 4,5,6 + DC.B $CC,$DD + DC.W $9ABC,$DEF0 + DC.L $DEADBEEF + DC.S "CD" + byte 7,8,9 + +; ------------------------------------------------------------------- +; Data: DS (declare space, all size extensions) +; ------------------------------------------------------------------- + ds 4 + ds 4,$FF + ds.b 2,42 + ds.w 2,3,4 + ds.l 1 + ds.s 1 + DS 8,$00 + DS.B 3 + DS.W 3 + DS.L 5,6,7,8 + DS.S 2 + +; ------------------------------------------------------------------- +; Data: DV (declare via EQM, all size extensions) +; ------------------------------------------------------------------- +XFORM eqm .. + 1 + dv XFORM 0,1,2 + dv.b XFORM 3,4 + dv.w XFORM 5,6 + dv.l XFORM 7,8 + dv.s XFORM 7,8 + DV XFORM 9 + DV.B XFORM 10 + DV.W XFORM 11 + DV.L XFORM 12 + DV.S XFORM 12 + +; ------------------------------------------------------------------- +; Strings and characters +; ------------------------------------------------------------------- + dc "Hello, World!" + dc 'A + dc 'Z + +; ------------------------------------------------------------------- +; Include directives +; ------------------------------------------------------------------- + include "kitchensink2.dasm" + INCLUDE "kitchensink2.dasm" + .include "kitchensink2.dasm" + .INCLUDE "kitchensink2.dasm" + + incbin "kitchensink/kitchensink.bin" + INCBIN "kitchensink/kitchensink.bin",2 + .incbin "kitchensink/kitchensink.bin" + .INCBIN "kitchensink/kitchensink.bin",2 + + incdir "kitchensink/" + INCDIR "kitchensink/" + .incdir "kitchensink/" + .INCDIR "kitchensink/" + +; ------------------------------------------------------------------- +; ALIGN +; ------------------------------------------------------------------- + align 256 + ALIGN 256 + align 16,$EA + ALIGN 16,$EA + +; ------------------------------------------------------------------- +; RORG / REND +; ------------------------------------------------------------------- + rorg $C000 + nop + rend + RORG $D000 + nop + REND + + .rorg $C000 + nop + .rend + .RORG $D000 + nop + .REND + +; ------------------------------------------------------------------- +; SUBROUTINE (local label scoping) +; ------------------------------------------------------------------- +MyFunc SUBROUTINE +.loop dex + bne .loop + rts +MyFunc2 subroutine +.loop dey + bne .loop + rts +MyFunc3 .subroutine +.loop rts +MyFunc4 .SUBROUTINE +.loop rts + +; ------------------------------------------------------------------- +; ECHO +; ------------------------------------------------------------------- + echo "Build starting" + .echo "Build starting" + ECHO "Value=",CONST1 + .ECHO "Value=",CONST1 + echo "Sum=",[CONST1+CONST2] + .echo "Sum=",[CONST1+CONST2] + +; ------------------------------------------------------------------- +; LIST +; ------------------------------------------------------------------- + list on + .list on + list off + .list off + LIST ON + .LIST ON + LIST OFF + .LIST OFF + +; ------------------------------------------------------------------- +; Macros: MAC / MACRO / MEXIT / ENDM +; ------------------------------------------------------------------- + mac LoadImm + lda #{1} + mexit + endm + + .mac LoadImm + lda #{1} + .mexit + .endm + + MAC StoreZP + sta {1} + MEXIT + ENDM + + .MAC StoreZP + sta {1} + .MEXIT + .ENDM + +;;; macro LdxImm +;;; ldx #{1} +;;; endm + +;;; MACRO LdyImm +;;; ldy #{1} +;;; ENDM + +; ------------------------------------------------------------------- +; Conditionals: IF / ELSE / ENDIF / EIF +; ------------------------------------------------------------------- + if CONST1 == $10 + nop + else + brk + endif + + IF CONST2 != $10 + nop + ELSE + brk + ENDIF + + .if CONST1 == $10 + nop + .else + brk + .endif + + .IF CONST2 != $10 + nop + .ELSE + brk + .ENDIF + +; ------------------------------------------------------------------- +; Conditionals: IFCONST / IFNCONST +; ------------------------------------------------------------------- + ifconst CONST1 + nop + endif + + IFCONST CONST2 + nop + ENDIF + + .ifnconst UNDEFINED_SYMBOL + nop + .endif + + .IFNCONST UNDEFINED_SYMBOL + nop + .ENDIF + +; ------------------------------------------------------------------- +; REPEAT / REPEND +; ------------------------------------------------------------------- +I SET 0 + REPEAT 4 + dc.b I +I SET I + 1 + REPEND + +I set 0 + repeat 4 + dc.b I +I set I + 1 + repend + +I .SET 0 + .REPEAT 4 + dc.b I +I .SET I + 1 + .REPEND + +I .set 0 + .repeat 4 + dc.b I +I .set I + 1 + .repend + + repeat 2 + nop + repend + + +; ------------------------------------------------------------------- +; Expressions: arithmetic operators +; ------------------------------------------------------------------- +EXPR_ADD = 1 + 2 +EXPR_SUB = 10 - 3 +EXPR_MUL = 4 * 5 +EXPR_DIV = 20 / 4 +EXPR_MOD = 17 % 5 + +; ------------------------------------------------------------------- +; Expressions: bitwise operators +; ------------------------------------------------------------------- +EXPR_AND = $FF & $0F +EXPR_OR = $F0 | $0F +EXPR_XOR = $FF ^ $AA +EXPR_SHL = 1 << 4 +EXPR_SHR = $80 >> 3 + +; ------------------------------------------------------------------- +; Expressions: comparison operators +; ------------------------------------------------------------------- +EXPR_EQ = [1 == 1] +EXPR_NE = [1 != 2] +EXPR_LT = [1 < 2] +EXPR_GT = [2 > 1] +EXPR_LE = [1 <= 1] +EXPR_GE = [2 >= 1] + +; ------------------------------------------------------------------- +; Expressions: logical operators +; ------------------------------------------------------------------- +EXPR_LAND = [1 && 1] +EXPR_LOR = [0 || 1] + +; ------------------------------------------------------------------- +; Expressions: ternary operator +; ------------------------------------------------------------------- +EXPR_TERN = [1 ? 42] + +; ------------------------------------------------------------------- +; Expressions: unary operators +; ------------------------------------------------------------------- +EXPR_NEG = -1 +;;;EXPR_POS = +1 +EXPR_NOT = !0 +EXPR_CPL = ~$FF +EXPR_LO = <$1234 +EXPR_HI = >$1234 + +; ------------------------------------------------------------------- +; Expressions: bracket grouping +; ------------------------------------------------------------------- +EXPR_PARENS = (2 + 3) * 4 +EXPR_BRACK = [2 + 3] * 4 + +; ------------------------------------------------------------------- +; Expressions: complex nested +; ------------------------------------------------------------------- +EXPR_COMPLEX = [[1 + 2] * [3 + 4]] & $FF + +; ------------------------------------------------------------------- +; C-style block comments +; ------------------------------------------------------------------- +;;;/* This is a single-line block comment */ + +;;;/* +;;; * This is a multi-line +;;; * block comment +;;; */ + +;;; nop /* inline block comment */ + +;;; lda /* mid-instruction comment */ #$42 + +; ------------------------------------------------------------------- +; Mixed comment styles +; ------------------------------------------------------------------- + nop ; semicolon comment /* block comment */ +;;; nop /* block comment */ ; then semicolon comment + + +; ------------------------------------------------------------------- +; END +; ------------------------------------------------------------------- + end ; assembly stops here + END ; would've stopped assembly + .end ; would've stopped assembly + .END ; would've stopped assembly + +; ------------------------------------------------------------------- +; ERR +; ------------------------------------------------------------------- + err ; would've aborted assembly + ERR ; would've aborted assembly + .err ; would've aborted assembly + .ERR ; would've aborted assembly + +; =========================================================================== +; End of kitchen sink +; =========================================================================== diff --git a/presets/c64/kitchensink/kitchensink.bin b/presets/c64/kitchensink/kitchensink.bin new file mode 100644 index 000000000..c11b4462c --- /dev/null +++ b/presets/c64/kitchensink/kitchensink.bin @@ -0,0 +1 @@ +; kitchensink/kitchensink.bin diff --git a/presets/c64/kitchensink2.dasm b/presets/c64/kitchensink2.dasm new file mode 100644 index 000000000..37647d22d --- /dev/null +++ b/presets/c64/kitchensink2.dasm @@ -0,0 +1 @@ +; kitchensink2.dasm diff --git a/presets/zx/kitchensink.zmac b/presets/zx/kitchensink.zmac new file mode 100644 index 000000000..dcaafd9cc --- /dev/null +++ b/presets/zx/kitchensink.zmac @@ -0,0 +1,777 @@ +; =========================================================================== +; kitchensink.zmac - Z80 grammar Kitchen Sink +; =========================================================================== +; Z80 parser and formatter test for zmac assembler +; https://48k.ca/zmac.html +; =========================================================================== +; TODO: Uncomment `;;;` lines once ZMAC is updated + +; ------------------------------------------------------------------- +; Code generation mode +; ------------------------------------------------------------------- +;;; .z80 + +; ------------------------------------------------------------------- +; ORG +; ------------------------------------------------------------------- + org $5CCB + .org 0x5CCB + ORG $5CCB + .ORG 0x5CCB + +; ------------------------------------------------------------------- +; Labels: global, global with colon, global with double colon (public) +; ------------------------------------------------------------------- +GlobalLabel +GlobalColon: +GlobalPublic:: + +; =================================================================== +; Runnable entry point +; =================================================================== + org $8000 +start: jp start ; infinite loop so program can run + +; ------------------------------------------------------------------- +; Instructions: 8-bit load group +; ------------------------------------------------------------------- + ld a,b + ld a,c + ld a,d + ld a,e + ld a,h + ld a,l + ld a,(hl) + ld a,a + ld b,42 + ld c,$FF + ld d,10101010b + ld e,0x1A + ld h,0FFh + ld l,0 + +; ------------------------------------------------------------------- +; Instructions: 16-bit load group +; ------------------------------------------------------------------- + ld bc,$1234 + ld de,$5678 + ld hl,$9ABC + ld sp,$DEF0 + ld ix,$1111 + ld iy,$2222 + ld hl,($C000) + ld ($C000),hl + ld bc,($C002) + ld ($C002),bc + ld de,($C004) + ld ($C004),de + ld sp,($C006) + ld ($C006),sp + ld ix,($C008) + ld ($C008),ix + ld iy,($C00A) + ld ($C00A),iy + +; ------------------------------------------------------------------- +; Instructions: stack operations +; ------------------------------------------------------------------- + push af + push bc + push de + push hl + push ix + push iy + pop af + pop bc + pop de + pop hl + pop ix + pop iy + +; ------------------------------------------------------------------- +; Instructions: exchange, block transfer, search +; ------------------------------------------------------------------- + ex de,hl + ex af,af' + exx + ex (sp),hl + ex (sp),ix + ex (sp),iy + ldi + ldir + ldd + lddr + cpi + cpir + cpd + cpdr + +; ------------------------------------------------------------------- +; Instructions: 8-bit arithmetic and logic +; ------------------------------------------------------------------- + add a,b + add a,$10 + add a,(hl) + add a,(ix+5) + add a,(iy-3) + adc a,c + adc a,$20 + sub d + sub $30 + sbc a,e + sbc a,$40 + and h + and $0F + or l + or $F0 + xor a + xor $AA + cp b + cp $55 + inc a + inc b + inc (hl) + inc (ix+2) + dec c + dec (iy+4) + +; ------------------------------------------------------------------- +; Instructions: 16-bit arithmetic +; ------------------------------------------------------------------- + add hl,bc + add hl,de + add hl,hl + add hl,sp + adc hl,bc + sbc hl,de + add ix,bc + add ix,de + add ix,ix + add ix,sp + add iy,bc + add iy,de + add iy,iy + add iy,sp + inc bc + inc de + inc hl + inc sp + inc ix + inc iy + dec bc + dec de + dec hl + dec sp + dec ix + dec iy + +; ------------------------------------------------------------------- +; Instructions: general-purpose AF operations +; ------------------------------------------------------------------- + daa + cpl + neg + ccf + scf + +; ------------------------------------------------------------------- +; Instructions: rotate and shift +; ------------------------------------------------------------------- + rlca + rrca + rla + rra + rlc b + rlc (hl) + rrc c + rrc (ix+1) + rl d + rl (iy+2) + rr e + sla h + sla (hl) + sra l + srl a + srl (ix+3) + rld + rrd + +; ------------------------------------------------------------------- +; Instructions: bit set, reset, test +; ------------------------------------------------------------------- + bit 0,a + bit 7,(hl) + bit 3,(ix+1) + set 0,b + set 5,(hl) + set 2,(iy+4) + res 7,c + res 4,(hl) + res 6,(ix+2) + +; ------------------------------------------------------------------- +; Instructions: jump group +; ------------------------------------------------------------------- + jp $8000 + jp nz,$8000 + jp z,$8000 + jp nc,$8000 + jp c,$8000 + jp po,$8000 + jp pe,$8000 + jp p,$8000 + jp m,$8000 + jp (hl) + jp (ix) + jp (iy) +jr_target: + jr jr_target + jr nz,jr_target + jr z,jr_target + jr nc,jr_target + jr c,jr_target + djnz jr_target + +; ------------------------------------------------------------------- +; Instructions: call and return group +; ------------------------------------------------------------------- + call $8000 + call nz,$8000 + call z,$8000 + call nc,$8000 + call c,$8000 + call po,$8000 + call pe,$8000 + call p,$8000 + call m,$8000 + ret + ret nz + ret z + ret nc + ret c + ret po + ret pe + ret p + ret m + reti + retn + +; ------------------------------------------------------------------- +; Instructions: restart group +; ------------------------------------------------------------------- + rst $00 + rst $08 + rst $10 + rst $18 + rst $20 + rst $28 + rst $30 + rst $38 + +; ------------------------------------------------------------------- +; Instructions: input and output group +; ------------------------------------------------------------------- + in a,($FE) + in b,(c) + in c,(c) + in d,(c) + in e,(c) + in h,(c) + in l,(c) + in a,(c) + ini + inir + ind + indr + out ($FE),a + out (c),b + out (c),c + out (c),d + out (c),e + out (c),h + out (c),l + out (c),a + outi + otir + outd + otdr + +; ------------------------------------------------------------------- +; Instructions: CPU control +; ------------------------------------------------------------------- + nop + halt + di + ei + im 0 + im 1 + im 2 + ld i,a + ld a,i + ld r,a + ld a,r + +; ------------------------------------------------------------------- +; Instructions: memory-referenced loads +; ------------------------------------------------------------------- + ld a,($1234) + ld ($1234),a + ld a,(bc) + ld a,(de) + ld (bc),a + ld (de),a + ld a,(ix+0) + ld a,(iy+127) + ld (ix-128),b + ld (iy+10),c + ld sp,hl + ld sp,ix + ld sp,iy + +; ------------------------------------------------------------------- +; Undocumented instructions +; ------------------------------------------------------------------- + sl1 b ; shift left, set bit 0 + in (c) ; input but discard result + out (c),0 ; output zero + +; ------------------------------------------------------------------- +; Undocumented: IX/IY half registers +; ------------------------------------------------------------------- + ld a,ixh + ld a,ixl + ld a,iyh + ld a,iyl + ld ixh,a + ld ixl,b + ld iyh,c + ld iyl,d + add a,ixh + sub ixl + and iyh + or iyl + cp ixh + inc ixl + dec iyh + +; ------------------------------------------------------------------- +; Number formats +; ------------------------------------------------------------------- + ld a,$FF ; hex with $ prefix + ld a,255 ; decimal + + ld a,0xFF ; hex with 0x prefix + ld a,0XFF ; hex with 0X prefix + ld a,0FFh ; hex with h suffix + ld a,0FFH ; hex with H suffix + ld a,10101010b ; binary with b suffix + ld a,10101010B ; binary with B suffix + ld a,377o ; octal with o suffix + ld a,377O ; octal with O suffix + ld a,377q ; octal with q suffix + ld a,377Q ; octal with Q suffix + ld a,255d ; decimal with d suffix + ld a,255D ; decimal with D suffix + + ld a,'A' ; character constant + +; ------------------------------------------------------------------- +; Current address ($) +; ------------------------------------------------------------------- + jp $ ; current address + +; ------------------------------------------------------------------- +; Pseudo-ops: EQU (fixed constant) +; ------------------------------------------------------------------- +CONST1 equ $10 +CONST2 EQU $20 +CONST3 equ CONST1 + CONST2 + +; ------------------------------------------------------------------- +; Pseudo-ops: DEFL / SET (reassignable) +; ------------------------------------------------------------------- +COUNTER defl 0 +COUNTER DEFL COUNTER + 1 +COUNTER set COUNTER + 1 +COUNTER SET COUNTER + 1 + +; ------------------------------------------------------------------- +; Pseudo-ops: compound assignment operators +; ------------------------------------------------------------------- +COUNTER += 5 +COUNTER -= 2 +COUNTER *= 3 + +; ------------------------------------------------------------------- +; Pseudo-ops: increment/decrement +; ------------------------------------------------------------------- +COUNTER ++ +COUNTER -- + +; ------------------------------------------------------------------- +; Data: DEFB / DB / BYTE / ASCII / TEXT / DEFM / DM +; ------------------------------------------------------------------- + defb $EE,$FF + DEFB $EE,$FF + db 1,2,3 + DB 1,2,3 + byte $AA,$BB + BYTE $AA,$BB + defm "Hello, World!" + DEFM "Hello, World!" +;;; dm "Test" +;;; DM "Test" + ascii "ABC" + ASCII "ABC" + text "xyz" + TEXT "xyz" + +; ------------------------------------------------------------------- +; Data: DEFW / DW / WORD +; ------------------------------------------------------------------- + defw $ABCD + DEFW $ABCD + dw $1234,$5678 + DW $1234,$5678 + word $9ABC + WORD $9ABC + +; ------------------------------------------------------------------- +; Data: DEF3 / D3 (24-bit) +; ------------------------------------------------------------------- +;;; def3 $123456 +;;; DEF3 $123456 +;;; d3 $ABCDEF +;;; D3 $ABCDEF + +; ------------------------------------------------------------------- +; Data: DEFD / DWORD (32-bit) +; ------------------------------------------------------------------- + defd $12345678 + DEFD $12345678 + dword $DEADBEEF + DWORD $DEADBEEF + +; ------------------------------------------------------------------- +; Data: DEFS / DS / BLOCK / RMEM (reserve space) +; ------------------------------------------------------------------- + defs 4 + DEFS 4 + ds 8 + DS 8 + block 16 + BLOCK 16 + rmem 2 + RMEM 2 + +; ------------------------------------------------------------------- +; Data: DC (string with high bit set on last char / repeat fill) +; ------------------------------------------------------------------- + dc 'Hello' + DC 10,$FF + +; ------------------------------------------------------------------- +; Data: INCBIN +; ------------------------------------------------------------------- +;;; incbin "garagesink.bin" +;;; INCBIN "garagesink.bin" + +; ------------------------------------------------------------------- +; Strings and characters +; ------------------------------------------------------------------- + defb 'A' ; single char in single quotes + DEFB 'String too' ; string in single quotes + defb "Hello" ; string in double quotes + defw 1234h ; two-char constant = 'H'*256 + 'L' + DEFW 1234h ; two-char constant = 'H'*256 + 'L' +;;; defw 'LH' ; two-char constant = 'H'*256 + 'L' + +; ------------------------------------------------------------------- +; Include directives +; ------------------------------------------------------------------- + include "kitchensink2.zmac" + INCLUDE "kitchensink2.zmac" + read "kitchensink2.zmac" + READ "kitchensink2.zmac" + + include "kitchensink3.lib" + maclib "kitchensink3.lib" + MACLIB "kitchensink3.lib" + +; ------------------------------------------------------------------- +; PHASE / DEPHASE (relocatable code) +; ------------------------------------------------------------------- + phase $C000 + nop + dephase + + PHASE $C000 + nop + DEPHASE + + .phase $D000 + nop + .dephase + + .PHASE $D000 + nop + .DEPHASE + +; ------------------------------------------------------------------- +; Macros: MACRO / ENDM / EXITM / LOCAL +; ------------------------------------------------------------------- +LoadImm macro val + ld a,val + exitm + endm + +AddPair MACRO r1,r2,?skip + LOCAL dummy + add a,r1 + jr nc,?skip + inc r2 +?skip: + EXITM + ENDM + +WithLocal macro arg + local done + ld a,arg + or a + jr z,done + inc a +done: + endm + + ; Macro invocations + LoadImm 42 + AddPair b,c + WithLocal $FF + +; ------------------------------------------------------------------- +; Conditionals: IF / ELSE / ENDIF +; ------------------------------------------------------------------- + if CONST1 == $10 + nop + else + halt + endif + + IF CONST2 != $10 + nop + ELSE + halt + ENDIF + +; ------------------------------------------------------------------- +; Conditionals: IFDEF / IFNDEF +; ------------------------------------------------------------------- + ifdef CONST1 + nop + endif + + IFDEF CONST1 + ENDIF + + ifndef UNDEFINED_SYMBOL + nop + endif + +; ------------------------------------------------------------------- +; Conditionals: COND / ENDC (synonyms for IF / ENDIF) +; ------------------------------------------------------------------- + cond CONST1 + nop + endc + + COND CONST1 + nop + ENDC + +; ------------------------------------------------------------------- +; Conditionals: IFEQ / IFNE / IFLT / IFGT +; ------------------------------------------------------------------- +;;; ifeq CONST1,$10 +;;; nop +;;; endif + +;;; ifne CONST1,$20 +;;; nop +;;; endif + +;;; iflt CONST1,$20 +;;; nop +;;; endif + +;;; ifgt CONST2,$10 +;;; nop +;;; endif + +; ------------------------------------------------------------------- +; REPT (repeat block) +; ------------------------------------------------------------------- + rept 4 + nop + endm + + REPT 4 + nop + ENDM + +; ------------------------------------------------------------------- +; IRP (iterate with replacement parameter) +; ------------------------------------------------------------------- + irp reg, + inc reg + endm + + IRP reg, + inc reg + ENDM + +; ------------------------------------------------------------------- +; IRPC (iterate characters) +; ------------------------------------------------------------------- + irpc ch,ABC + defb 'ch' + endm + + IRPC ch,ABC + defb 'ch' + ENDM + +; ------------------------------------------------------------------- +; Expressions: arithmetic operators +; ------------------------------------------------------------------- +EXPR_ADD equ 1 + 2 +EXPR_SUB equ 10 - 3 +EXPR_MUL equ 4 * 5 +EXPR_DIV equ 20 / 4 +EXPR_MOD equ 17 % 5 + +; ------------------------------------------------------------------- +; Expressions: bitwise operators +; ------------------------------------------------------------------- +EXPR_AND equ $FF & $0F +EXPR_OR equ $F0 | $0F +EXPR_XOR equ $FF ^ $AA +EXPR_SHL equ 1 << 4 +EXPR_SHR equ $80 >> 3 + +; ------------------------------------------------------------------- +; Expressions: comparison operators +; ------------------------------------------------------------------- +EXPR_EQ equ [1 == 1] +EXPR_NE equ [1 != 2] +EXPR_LT equ [1 < 2] +EXPR_GT equ [2 > 1] +EXPR_LE equ [1 <= 1] +EXPR_GE equ [2 >= 1] + +; ------------------------------------------------------------------- +; Expressions: logical operators +; ------------------------------------------------------------------- +EXPR_LAND equ [1 && 1] +EXPR_LOR equ [0 || 1] + +; ------------------------------------------------------------------- +; Expressions: unary operators +; ------------------------------------------------------------------- +EXPR_NEG equ -1 +EXPR_NOT equ !0 +EXPR_CPL equ ~$FF +EXPR_LO equ low $1234 +EXPR_HI equ high $1234 + +; ------------------------------------------------------------------- +; Expressions: parentheses and bracket grouping +; ------------------------------------------------------------------- +EXPR_PARNS equ (2 + 3) * 4 +EXPR_BRACK equ [2 + 3] * 4 + +; ------------------------------------------------------------------- +; Expressions: complex nested +; ------------------------------------------------------------------- +EXPR_COMPLEX equ [[1 + 2] * [3 + 4]] & $FF + +; ------------------------------------------------------------------- +; Expressions: word synonym operators +; ------------------------------------------------------------------- +EXPR_MOD2 equ 17 mod 5 +EXPR_SHL2 equ 1 shl 4 +EXPR_SHR2 equ $80 shr 3 +EXPR_AND2 equ $FF and $0F +EXPR_OR2 equ $F0 or $0F +EXPR_XOR2 equ $FF xor $AA +EXPR_NOT2 equ not 0 +EXPR_EQ2 equ [1 eq 1] +EXPR_NE2 equ [1 ne 2] +EXPR_LT2 equ [1 lt 2] +EXPR_GT2 equ [2 gt 1] +EXPR_LE2 equ [1 le 1] +EXPR_GE2 equ [2 ge 1] + +; ------------------------------------------------------------------- +; Cycle counting: SETT / T() +; ------------------------------------------------------------------- +cycle_start: + nop + nop + nop +cycle_end: +CYCLES equ t(cycle_end) - t(cycle_start) + +; ------------------------------------------------------------------- +; ASSERT +; ------------------------------------------------------------------- + assert CONST1 == $10 + ASSERT CONST1 == $10 + +; ------------------------------------------------------------------- +; Listing pseudo-ops +; ------------------------------------------------------------------- + list 1 + LIST -1 + title 'Kitchen Sink Test' + TITLE 'Kitchen Sink Test' + +; ------------------------------------------------------------------- +; NAME +; ------------------------------------------------------------------- + name 'kitchensink' + NAME 'kitchensink' + +; ------------------------------------------------------------------- +; JR promotion +; ------------------------------------------------------------------- + jrpromote 1 ; enable JR->JP promotion + JRPROMOTE 1 ; enable JR->JP promotion + +; ------------------------------------------------------------------- +; Segments (for .rel output) +; ------------------------------------------------------------------- + aseg + cseg + dseg + + ASEG + CSEG + DSEG + +; ------------------------------------------------------------------- +; EXTERN / PUBLIC (for .rel output) +; ------------------------------------------------------------------- + extern ExtSym1,ExtSym2 + EXTERN ExtSym1,ExtSym2 + public start + PUBLIC start + +; ------------------------------------------------------------------- +; END +; ------------------------------------------------------------------- + end start ; assembly stops, entry address = start + END start ; assembly stops, entry address = start + +; =========================================================================== +; End of kitchen sink +; =========================================================================== diff --git a/presets/zx/kitchensink2.zmac b/presets/zx/kitchensink2.zmac new file mode 100644 index 000000000..25c1e872c --- /dev/null +++ b/presets/zx/kitchensink2.zmac @@ -0,0 +1 @@ +; kitchensink2.zmac \ No newline at end of file diff --git a/presets/zx/kitchensink3.lib b/presets/zx/kitchensink3.lib new file mode 100644 index 000000000..99d5ca8ba --- /dev/null +++ b/presets/zx/kitchensink3.lib @@ -0,0 +1 @@ +; kitchensink3.lib \ No newline at end of file diff --git a/src/platform/c64.ts b/src/platform/c64.ts index d72ceefe8..0e5146b6b 100644 --- a/src/platform/c64.ts +++ b/src/platform/c64.ts @@ -42,6 +42,7 @@ const C64_PRESETS : Preset[] = [ {id:'hello.dasm', name:'Hello World (DASM)', category:'Assembly Language'}, {id:'hello.acme', name:'Hello World (ACME)'}, {id:'hello.wiz', name:'Hello Wiz (Wiz)'}, + // {id:'kitchensink.dasm', name:'Kitchensink (DASM)'}, ]; const C64_MEMORY_MAP = { main:[ @@ -72,7 +73,7 @@ class C64WASMPlatform extends Base6502MachinePlatform implement readAddress(a) { return this.machine.readConst(a); } getMemoryMap() { return C64_MEMORY_MAP; } showHelp() { return "https://8bitworkshop.com/docs/platforms/c64/" } - getROMExtension(rom:Uint8Array) { + getROMExtension(rom:Uint8Array) { /* if (rom && rom[0] == 0x00 && rom[1] == 0x80 && rom[2+4] == 0xc3 && rom[2+5] == 0xc2) return ".crt"; */ diff --git a/src/platform/zx.ts b/src/platform/zx.ts index 10b703602..7fd37339d 100644 --- a/src/platform/zx.ts +++ b/src/platform/zx.ts @@ -7,6 +7,7 @@ const ZX_PRESETS = [ {id:'hello.asm', name:'Hello World (ASM)'}, {id:'bios.c', name:'BIOS Routines (C)'}, {id:'cosmic.c', name:'Cosmic Impalas (C)'}, + // {id:'kitchensink.zmac', name:'Kitchensink (ZMAC)'}, ]; const ZX_MEMORY_MAP = { main:[ From 421c7406d9985a11b9e42aeb976afe273c50c7c5 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 4 Apr 2026 14:36:14 -0700 Subject: [PATCH 02/40] Improve 6502 parsing --- src/parser/lang-6502.grammar | 78 +++++++++++---------------------- src/parser/lang-6502.ts | 28 ++++++------ src/parser/tokens-6502.ts | 84 +++++++++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 65 deletions(-) diff --git a/src/parser/lang-6502.grammar b/src/parser/lang-6502.grammar index 633af01f7..c29274324 100644 --- a/src/parser/lang-6502.grammar +++ b/src/parser/lang-6502.grammar @@ -3,6 +3,8 @@ @skip { space | Comment } Line { + MacroDef | + RepeatBlock | Label? Statement? eol } @@ -10,42 +12,27 @@ Statement { Instruction | Directive | HexDirective | - MacroDef | - MacEnd | - ControlOp | - ErrorOp + ControlOp } -Label { Identifier ":" | Identifier } +Label { (Identifier | LocalIdentifier) Colon | Identifier | LocalIdentifier } Instruction { Opcode Operand? } -Register { - @specialize -} +@external specialize {Identifier} registerSpecializer from "../../src/parser/tokens-6502" { Register } +@external specialize {Identifier} onOffSpecializer from "../../src/parser/tokens-6502" { OnOff } +@external specialize {Identifier} hexOpSpecializer from "../../src/parser/tokens-6502" { HexOp } +@external specialize {Identifier} opcodeSpecializer from "../../src/parser/tokens-6502" { Opcode } Directive { - PseudoOp (Expression)* -} - -PseudoOp { - @specialize + PseudoOp (Expression)* | + Equals (Expression)* } -HexOp { @specialize } +@external specialize {Identifier} pseudoOpSpecializer from "../../src/parser/tokens-6502" { PseudoOp } +@external specialize {Identifier} localIdentifierSpecializer from "../../src/parser/tokens-6502" { LocalIdentifier } HexDirective { HexOp HexByte* @@ -53,37 +40,20 @@ HexDirective { @external tokens hexTokenizer from "../../src/parser/tokens-6502" { HexByte } -Mac { @specialize } -MacEnd { @specialize } +@external specialize {Identifier} macSpecializer from "../../src/parser/tokens-6502" { Mac, MacEnd, Repeat, RepEnd } -ControlOp { @specialize } -ErrorOp { @specialize } +@external specialize {Identifier} controlOpSpecializer from "../../src/parser/tokens-6502" { ControlOp } MacroDef { - Mac Identifier + Mac Identifier eol Line* MacEnd eol } -CurrentAddress { - @specialize +RepeatBlock { + Repeat Expression? eol Line* RepEnd eol } -Opcode { - @specialize +CurrentAddress { + @specialize } Expression { @@ -108,13 +78,15 @@ UnaryGt { gt !un } Value { Number | Identifier | + LocalIdentifier | + OnOff | CurrentAddress | String | Char } Operand { - "#" Expression | + ImmediatePrefix Expression | "(" Expression Comma Register ")" | Expression (Comma Register)? | Register @@ -129,7 +101,7 @@ Operand { $[0-9]+ } - String { '"' (!["\\\n] | "\\" _)* '"' } + String { '"' !["\n]* '"' } Char { "'" ![\n] "'"? } @@ -139,7 +111,8 @@ Operand { eol { $[\n\r]+ } Comma { "," } - "#" + Colon { ":" } + ImmediatePrefix { "#" } "(" ")" ArithOp { "*" | "/" } @@ -153,6 +126,7 @@ Operand { LogicOp { "&&" | "||" } Not { "!" } + Equals { "=" } CompareOp { "==" | "!=" | "<=" | ">=" } lt { "<" } gt { ">" } diff --git a/src/parser/lang-6502.ts b/src/parser/lang-6502.ts index 97d65068e..2eb06f967 100644 --- a/src/parser/lang-6502.ts +++ b/src/parser/lang-6502.ts @@ -1,27 +1,29 @@ -import { LRLanguage, LanguageSupport, delimitedIndent, foldInside, foldNodeProp, indentNodeProp } from "@codemirror/language" +import { LRLanguage, LanguageSupport, foldInside, foldNodeProp } from "@codemirror/language" import { styleTags, tags as t } from "@lezer/highlight" import { parser } from "../../gen/parser/lang-6502.grammar.js" export const Lezer6502: LRLanguage = LRLanguage.define({ parser: parser.configure({ props: [ - indentNodeProp.add({ - Application: delimitedIndent({ closing: ")", align: false }) - }), foldNodeProp.add({ - Application: foldInside + MacroDef: foldInside, + RepeatBlock: foldInside }), styleTags({ Identifier: t.variableName, + LocalIdentifier: t.local(t.variableName), CurrentAddress: t.self, - PseudoOp: t.definition(t.variableName), - Opcode: t.keyword, + PseudoOp: t.keyword, + Equals: t.keyword, + Opcode: t.standard(t.keyword), Label: t.labelName, String: t.string, - Char: t.number, + Char: t.character, Number: t.number, - Register: t.typeName, - Comment: t.lineComment, + OnOff: t.bool, + Register: t.standard(t.modifier), + ImmediatePrefix: t.constant(t.modifier), + Comment: t.comment, ArithOp: t.arithmeticOperator, Plus: t.arithmeticOperator, Minus: t.arithmeticOperator, @@ -35,14 +37,16 @@ export const Lezer6502: LRLanguage = LRLanguage.define({ BinaryGt: t.compareOperator, UnaryLt: t.arithmeticOperator, UnaryGt: t.arithmeticOperator, - HexOp: t.definition(t.variableName), + HexOp: t.keyword, HexByte: t.number, Mac: t.definitionKeyword, MacEnd: t.definitionKeyword, + Repeat: t.controlKeyword, + RepEnd: t.controlKeyword, "MacroDef/Identifier": t.macroName, ControlOp: t.controlKeyword, - ErrorOp: t.keyword, Comma: t.separator, + Colon: t.separator, "( )": t.paren }) ] diff --git a/src/parser/tokens-6502.ts b/src/parser/tokens-6502.ts index c5b51aeb2..7a8981f44 100644 --- a/src/parser/tokens-6502.ts +++ b/src/parser/tokens-6502.ts @@ -1,5 +1,5 @@ import { ExternalTokenizer } from "@lezer/lr" -import { HexByte } from "../../gen/parser/lang-6502.grammar.terms" +import { HexByte, PseudoOp, Mac, MacEnd, Repeat, RepEnd, ControlOp, LocalIdentifier, Opcode, Register, OnOff, HexOp } from "../../gen/parser/lang-6502.grammar.terms" function isHexDigit(ch: number) { return (ch >= 48 && ch <= 57) || // 0-9 @@ -7,6 +7,88 @@ function isHexDigit(ch: number) { (ch >= 97 && ch <= 102) // a-f } +export const opcodes = new Set([ + "adc", "and", "asl", "bcc", "bcs", "beq", "bit", "bmi", + "bne", "bpl", "brk", "bvc", "bvs", "clc", "cld", "cli", + "clv", "cmp", "cpx", "cpy", "dec", "dex", "dey", "eor", + "inc", "inx", "iny", "jmp", "jsr", "lda", "ldx", "ldy", + "lsr", "nop", "ora", "pha", "php", "pla", "plp", "rol", + "ror", "rti", "rts", "sbc", "sec", "sed", "sei", "sta", + "stx", "sty", "tax", "tay", "tsx", "txa", "txs", "tya", +]) + +const registers = new Set(["a", "x", "y"]) + +const pseudoOps = new Set([ + "org", "rorg", "rend", + "equ", "eqm", + "end", + "seg", "seg.u", + "align", + "dc", "dc.b", "dc.w", "dc.l", "dc.s", + "ds", "ds.b", "ds.w", "ds.l", "ds.s", + "dv", "dv.b", "dv.w", "dv.l", "dv.s", + "byte", "word", "long", + "subroutine", "processor", + "include", "incbin", "incdir", + "echo", "set", + "list", + "err", +]) + +const macKeywords: Record = { + "mac": Mac, "macro": Mac, + "endm": MacEnd, + "mexit": ControlOp, + "repeat": Repeat, + "repend": RepEnd, +} + +const controlOps = new Set([ + "if", "else", "endif", "ifconst", "ifnconst", +]) + +const onOffValues = new Set(["on", "off"]) + +export function pseudoOpSpecializer(value: string) { + let normalized = value.startsWith(".") ? value.slice(1) : value + return pseudoOps.has(normalized.toLowerCase()) ? PseudoOp : -1 +} + +export function macSpecializer(value: string) { + let normalized = value.startsWith(".") ? value.slice(1) : value + return macKeywords[normalized.toLowerCase()] ?? -1 +} + +export function controlOpSpecializer(value: string) { + let normalized = value.startsWith(".") ? value.slice(1) : value + return controlOps.has(normalized.toLowerCase()) ? ControlOp : -1 +} + +export function localIdentifierSpecializer(value: string) { + if (!value.startsWith(".") || value.length <= 1) return -1 + // Don't claim dot-prefixed keywords that other specializers handle + const bare = value.slice(1).toLowerCase() + if (pseudoOps.has(bare) || bare in macKeywords || controlOps.has(bare)) return -1 + return LocalIdentifier +} + +export function opcodeSpecializer(value: string) { + return opcodes.has(value.toLowerCase()) ? Opcode : -1 +} + +export function registerSpecializer(value: string) { + return registers.has(value.toLowerCase()) ? Register : -1 +} + +export function onOffSpecializer(value: string) { + return onOffValues.has(value.toLowerCase()) ? OnOff : -1 +} + +export function hexOpSpecializer(value: string) { + return value.toLowerCase() === "hex" ? HexOp : -1 +} + export const hexTokenizer = new ExternalTokenizer((input) => { if (!isHexDigit(input.peek(0)) || !isHexDigit(input.peek(1))) return let len = 2 From 73a4e758630e5fb743a80bfc762d8d2111f11adb Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 4 Apr 2026 14:38:17 -0700 Subject: [PATCH 03/40] Improve Z80 parsing --- src/parser/lang-z80.grammar | 89 ++++++++++------------------------- src/parser/lang-z80.ts | 24 +++++++--- src/parser/tokens-z80.ts | 92 +++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 73 deletions(-) create mode 100644 src/parser/tokens-z80.ts diff --git a/src/parser/lang-z80.grammar b/src/parser/lang-z80.grammar index b727186bd..b8a2625f4 100644 --- a/src/parser/lang-z80.grammar +++ b/src/parser/lang-z80.grammar @@ -3,15 +3,18 @@ @skip { space | Comment } Line { + RepeatBlock | Label? Statement? eol } Statement { Instruction | - Directive + Directive | + MacroDef | + ControlOp } -Label { Identifier ":" | Identifier } +Label { Identifier Colon Colon? | Identifier } Instruction { Opcode Operand? @@ -21,66 +24,20 @@ Directive { PseudoOp (Expression)* } -PseudoOp { - @specialize +MacroDef { + Mac (Identifier (Comma Identifier)*)? eol Line* MacEnd } -Condition { - @specialize +RepeatBlock { + Repeat Expression? eol Line* MacEnd eol } -Register { - @specialize -} - -Opcode { - @specialize -} +@external specialize {Identifier} pseudoOpSpecializer from "../../src/parser/tokens-z80" { PseudoOp } +@external specialize {Identifier} macSpecializer from "../../src/parser/tokens-z80" { Mac, MacEnd, Repeat } +@external specialize {Identifier} controlOpSpecializer from "../../src/parser/tokens-z80" { ControlOp } +@external specialize {Identifier} opcodeSpecializer from "../../src/parser/tokens-z80" { Opcode } +@external specialize {Identifier} registerSpecializer from "../../src/parser/tokens-z80" { Register } +@external specialize {Identifier} conditionSpecializer from "../../src/parser/tokens-z80" { Condition } Expression { Expression !logic LogicOp Expression | @@ -115,18 +72,18 @@ Operand { } @tokens { - Identifier { $[a-zA-Z_] $[a-zA-Z0-9_]* } + Identifier { $[a-zA-Z_.?] $[a-zA-Z0-9_.?]* } - Hex { ("0x" | "$") $[0-9a-fA-F]+ | $[0-9] $[0-9a-fA-F]* "h" } - Bin { "%" $[01]+ | $[01]+ "b" } - Oct { "0o" $[0-7]+ | $[0-7]+ "o" } - Dec { $[0-9]+ } + Hex { ("0x" | "0X" | "$") $[0-9a-fA-F]+ | $[0-9] $[0-9a-fA-F]* $[hH] } + Bin { $[01]+ $[bB] } + Oct { ("0o" | "0O") $[0-7]+ | $[0-7]+ $[oOqQ] } + Dec { $[0-9]+ $[dD]? } Number { Hex | Bin | Oct | Dec } - String { '"' (!["\\\n] | "\\" _)* '"' } + String { '"' !["\n]* '"' } - Char { "'" !['\\\n] "'"? } + Char { "'" !['\n] "'"? } Comment { ";" ![\n]* } @@ -134,7 +91,7 @@ Operand { eol { $[\n\r]+ } Comma { "," } - ":" + Colon { ":" } "#" "(" ")" diff --git a/src/parser/lang-z80.ts b/src/parser/lang-z80.ts index ba1cf47c5..f9b5ee3a1 100644 --- a/src/parser/lang-z80.ts +++ b/src/parser/lang-z80.ts @@ -1,21 +1,25 @@ -import { LRLanguage, LanguageSupport } from "@codemirror/language" +import { LRLanguage, LanguageSupport, foldInside, foldNodeProp } from "@codemirror/language" import { styleTags, tags as t } from "@lezer/highlight" import { parser } from "../../gen/parser/lang-z80.grammar.js" export const LezerZ80: LRLanguage = LRLanguage.define({ parser: parser.configure({ props: [ + foldNodeProp.add({ + MacroDef: foldInside, + RepeatBlock: foldInside + }), styleTags({ Identifier: t.variableName, - PseudoOp: t.definition(t.variableName), - Opcode: t.keyword, - Register: t.typeName, - Condition: t.className, + PseudoOp: t.keyword, + Opcode: t.standard(t.keyword), + Register: t.standard(t.modifier), + Condition: t.standard(t.modifier), Label: t.labelName, String: t.string, - Char: t.number, + Char: t.character, Number: t.number, - Comment: t.lineComment, + Comment: t.comment, ArithOp: t.arithmeticOperator, Plus: t.arithmeticOperator, Minus: t.arithmeticOperator, @@ -29,7 +33,13 @@ export const LezerZ80: LRLanguage = LRLanguage.define({ BinaryGt: t.compareOperator, UnaryLt: t.arithmeticOperator, UnaryGt: t.arithmeticOperator, + Mac: t.definitionKeyword, + MacEnd: t.definitionKeyword, + Repeat: t.controlKeyword, + "MacroDef/Identifier": t.macroName, + ControlOp: t.controlKeyword, Comma: t.separator, + Colon: t.separator, "( )": t.paren }) ] diff --git a/src/parser/tokens-z80.ts b/src/parser/tokens-z80.ts new file mode 100644 index 000000000..a047f760a --- /dev/null +++ b/src/parser/tokens-z80.ts @@ -0,0 +1,92 @@ +import { PseudoOp, Mac, MacEnd, Repeat, ControlOp, Opcode, Register, Condition } from "../../gen/parser/lang-z80.grammar.terms" + +const pseudoOps = new Set([ + "org", "equ", "defl", "end", + "phase", "dephase", + "defb", "db", "byte", "ascii", "text", "defm", "dm", + "defw", "dw", "word", + "defd", "dword", "def3", "d3", + "defs", "ds", "block", "rmem", + "dc", "incbin", "include", "read", "maclib", "import", + "public", "global", "entry", "extern", "ext", "extrn", + "assert", "list", "nolist", "title", "name", "eject", "space", + "jrpromote", "jperror", + "irp", "irpc", "local", + "sett", "tstate", "setocf", + "rsym", "wsym", + "aseg", "cseg", "dseg", "common", + "comment", "pragma", "subttl", + "z80", "8080", "z180", + "min", "max", +]) + +const macKeywords: Record = { + "macro": Mac, + "endm": MacEnd, + "exitm": ControlOp, + "rept": Repeat, +} + +const controlOps = new Set([ + "if", "else", "endif", + "ifdef", "ifndef", + "cond", "endc", + "ifeq", "ifne", "iflt", "ifgt", +]) + +export const opcodes = new Set([ + // Z80 instructions + "ld", "push", "pop", "inc", "dec", "add", "adc", "sub", "sbc", "and", "or", "xor", + "cp", "ret", "jp", "jr", "call", "rst", "nop", "halt", "di", "ei", + "im", "ex", "exx", "neg", "cpl", "ccf", "scf", "rlca", "rla", "rrca", "rra", + "rlc", "rl", "rrc", "rr", "sla", "sra", "srl", "sl1", "bit", "set", "res", + "out", "in", "djnz", "rld", "rrd", "ldi", "ldir", "ldd", "lddr", "cpi", "cpir", "cpd", "cpdr", + "ini", "inir", "ind", "indr", "outi", "otir", "outd", "otdr", + "daa", "reti", "retn", "pfix", "pfiy", + // 8080 instructions + "mov", "mvi", "lxi", "lda", "sta", "lhld", "shld", "ldax", "stax", + "adi", "aci", "sui", "sbi", "sbb", "ana", "ani", "xra", "xri", "ora", "ori", "cmp", + "inr", "dcr", "inx", "dcx", "dad", + "cma", "stc", "cmc", "ral", "rar", + "jmp", "jnz", "jz", "jnc", "jc", "jpo", "jpe", "jm", + "cnz", "cz", "cnc", "cc", "cpo", "cpe", "cm", + "rnz", "rz", "rnc", "rc", "rpo", "rpe", "rp", "rm", + "pchl", "sphl", "xthl", "xchg", "hlt", +]) + +const registers = new Set([ + "a", "b", "c", "d", "e", "h", "l", "i", "r", + "af", "bc", "de", "hl", "ix", "iy", "sp", "pc", "psw", + "ixh", "ixl", "iyh", "iyl", "xh", "xl", "yh", "yl", "hx", "lx", "hy", "ly", +]) + +const conditions = new Set([ + "nz", "z", "nc", "c", "po", "pe", "p", "m", +]) + +export function pseudoOpSpecializer(value: string) { + let normalized = value.startsWith(".") ? value.slice(1) : value + return pseudoOps.has(normalized.toLowerCase()) ? PseudoOp : -1 +} + +export function macSpecializer(value: string) { + let normalized = value.startsWith(".") ? value.slice(1) : value + return macKeywords[normalized.toLowerCase()] ?? -1 +} + +export function controlOpSpecializer(value: string) { + let normalized = value.startsWith(".") ? value.slice(1) : value + return controlOps.has(normalized.toLowerCase()) ? ControlOp : -1 +} + +export function opcodeSpecializer(value: string) { + return opcodes.has(value.toLowerCase()) ? Opcode : -1 +} + +export function registerSpecializer(value: string) { + return registers.has(value.toLowerCase()) ? Register : -1 +} + +export function conditionSpecializer(value: string) { + return conditions.has(value.toLowerCase()) ? Condition : -1 +} From 2f20cd2bbe0e0576bbc283b06450e9f5650f42a4 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 4 Apr 2026 17:27:04 -0700 Subject: [PATCH 04/40] mbo theme syntax highlighting and whitespace - Update mbo theme after 6502/Z80 parser updates - Reduce whitespace indicator opacity --- src/themes/mbo.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/themes/mbo.ts b/src/themes/mbo.ts index 1df863da6..ad1d2a308 100644 --- a/src/themes/mbo.ts +++ b/src/themes/mbo.ts @@ -37,26 +37,28 @@ export const mboTheme = EditorView.theme({ color: "#33ff33 !important" }, ".cm-highlightSpace": { - backgroundImage: "radial-gradient(circle at 50% 55%, #aaa5 11%, transparent 5%)" + backgroundImage: "radial-gradient(circle at 50% 55%, #aaaaaa45 11%, transparent 5%)" }, ".cm-highlightTab": { - backgroundImage: `url('data:image/svg+xml,')`, + backgroundImage: `url('data:image/svg+xml,')`, backgroundPosition: "left 50%" }, }, { dark: true }); export const mboHighlightStyle = HighlightStyle.define([ - { tag: t.keyword, color: "#ffb928" }, + { tag: t.standard(t.keyword), color: "#ffb928" }, { tag: [t.name, t.standard(t.name)], color: "#ffffec" }, { tag: t.variableName, color: "#9ddfe9" }, - { tag: [t.deleted, t.macroName], color: "#00a8c6" }, // maps to cm-variable-2 - { tag: [t.processingInstruction, t.string, t.inserted], color: "#b4fdb7" }, // Updated string color - { tag: t.number, color: "#33aadd" }, // Updated number color + { tag: t.local(t.variableName), color: "#7eb8c4" }, + { tag: [t.deleted, t.macroName], color: "#00a8c6" }, + { tag: [t.processingInstruction, t.keyword, t.controlKeyword], color: "#c792ea" }, + { tag: [t.string, t.inserted], color: "#b4fdb7" }, + { tag: [t.number, t.modifier], color: "#33aadd" }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: "#00a8c6" }, { tag: t.definition(t.variableName), color: "#ffb928" }, - { tag: [t.propertyName, t.attributeName, t.tagName], color: "#9ddfe9" }, - { tag: t.definition(t.name), color: "#88eeff" }, // maps to cm-def - { tag: t.typeName, color: "#ffb928" }, // maps to cm-variable-3 + { tag: [t.propertyName, t.attributeName, t.tagName, t.self], color: "#9ddfe9" }, + { tag: t.definition(t.name), color: "#88eeff" }, + { tag: t.typeName, color: "#ffb928" }, { tag: t.bracket, color: "#fffffc", fontWeight: "bold" }, { tag: t.comment, color: "#95958a" }, { tag: t.link, color: "#f54b07" }, From dfdb3583bddde0498c424cb472d539a1e66cee9c Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 4 Apr 2026 14:39:23 -0700 Subject: [PATCH 05/40] src/parser/tokens-6809.ts --- src/parser/tokens-6809.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/parser/tokens-6809.ts diff --git a/src/parser/tokens-6809.ts b/src/parser/tokens-6809.ts new file mode 100644 index 000000000..4c069ae9b --- /dev/null +++ b/src/parser/tokens-6809.ts @@ -0,0 +1,15 @@ +export const opcodes = new Set([ + // 6809 instructions + "abx", "adca", "adcb", "adda", "addb", "addd", "anda", "andb", "andcc", "asr", "asra", + "asrb", "beq", "bge", "bgt", "bhi", "bhs", "bita", "bitb", "ble", "blo", "bls", "blt", + "bmi", "bne", "bpl", "bra", "brn", "bsr", "bvc", "bvs", "clr", "clra", "clrb", "cmpa", + "cmpb", "cmpd", "cmps", "cmpu", "cmpx", "cmpy", "com", "coma", "comb", "cwai", "daa", + "dec", "deca", "decb", "eora", "eorb", "exg", "inc", "inca", "incb", "jmp", "jsr", + "lbeq", "lbge", "lbgt", "lbhi", "lbhs", "lble", "lbls", "lblt", "lbmi", "lbne", + "lbra", "lbrn", "lbsr", "lbvc", "lbvs", "lda", "ldb", "ldd", "lds", "ldu", "ldx", + "ldy", "leas", "leau", "leax", "leay", "lpbl", "lsl", "lsla", "lslb", "lsr", "lsra", + "lsrb", "mul", "neg", "nega", "negb", "nop", "ora", "orb", "orcc", "page", "pshs", + "pshu", "puls", "pulu", "rol", "rola", "rolb", "ror", "rora", "rorb", "rti", "rts", + "sbca", "sbcb", "sex", "sta", "stb", "std", "sts", "stu", "stx", "sty", "suba", + "subb", "subd", "swi", "swi2", "swi3", "sync", "tfr", "tst", "tsta", "tstb" +]); \ No newline at end of file From c51ea4af0da7ad17ac3a9751ca21f1b5522f8741 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 4 Apr 2026 15:06:48 -0700 Subject: [PATCH 06/40] Map xasm6809 to 6809 instead of z80 --- src/ide/ui.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 3e016724e..a29ea76d8 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -126,7 +126,7 @@ const TOOL_TO_SOURCE_STYLE = { 'bataribasic': 'bataribasic', 'markdown': 'markdown', 'js': 'javascript', - 'xasm6809': 'z80', + 'xasm6809': '6809', 'cmoc': 'text/x-csrc', 'yasm': 'gas', 'smlrc': 'text/x-csrc', From 877601770dfaef5e7d72c8e0ccd3db3e6da8f99e Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Mon, 30 Mar 2026 18:30:38 -0700 Subject: [PATCH 07/40] Delete unused highlightSearch in ui.ts --- src/ide/ui.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/ide/ui.ts b/src/ide/ui.ts index a29ea76d8..e2626a4cf 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -2168,15 +2168,6 @@ function writeOutputROMFile() { alternateLocalFilesystem.setFileData(`bin/${prefix}${suffix}`, current_output); } } -export function highlightSearch(query: string) { // TODO: filename? - var wnd = projectWindows.getActive(); - if (wnd instanceof SourceEditor) { - var sc = wnd.editor.getSearchCursor(query); - if (sc.findNext()) { - wnd.editor.setSelection(sc.pos.to, sc.pos.from); - } - } -} function startUIWhenVisible() { let started = false; From af28524f5732a5d049c00a18b4400ba82b9093c8 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 09:00:38 -0700 Subject: [PATCH 08/40] Fix replaceTextRange selection --- src/ide/views/editors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index cecd53941..aff247e7f 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -319,7 +319,7 @@ export class SourceEditor implements ProjectView { this.editor.dispatch({ changes: { from, to, insert: text }, annotations: isolateHistory.of("full"), - selection: { anchor: from, head: to }, + selection: { anchor: from, head: from + text.length }, effects: [ EditorView.scrollIntoView(this.editor.state.doc.line(fromline).from, { y: "start", yMargin: 100/*pixels*/ }), ] From 8c4df4e4449dc5287472d8908ac7957e8290b04a Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 09:06:54 -0700 Subject: [PATCH 09/40] Backspace key runs deleteCharBackwardStrict Map backspace key to delete selection or single character, instead of consuming all whitespace to previous tab stop. --- src/ide/views/editors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index aff247e7f..9b46a7f7f 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -1,4 +1,4 @@ -import { defaultKeymap, history, historyKeymap, indentSelection, isolateHistory, redo, undo } from "@codemirror/commands"; +import { defaultKeymap, deleteCharBackwardStrict, history, historyKeymap, indentSelection, isolateHistory, redo, undo } from "@codemirror/commands"; import { cpp } from "@codemirror/lang-cpp"; import { markdown } from "@codemirror/lang-markdown"; import { bracketMatching, foldGutter, indentOnInput, indentService, indentUnit } from "@codemirror/language"; @@ -163,6 +163,7 @@ export class SourceEditor implements ProjectView { keymap.of([ { key: "Ctrl-Shift-i", run: indentSelection }, { key: "Cmd-Shift-i", run: indentSelection }, + { key: "Backspace", run: deleteCharBackwardStrict }, ]), keymap.of(defaultKeymap), From 6a1b418641739121735566107c1d256d6466b8fb Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 28 Mar 2026 11:23:06 -0700 Subject: [PATCH 10/40] Fix asm indention on ENTER Match previous lines' whitespace --- src/ide/views/editors.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 9b46a7f7f..0c98a95f4 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -1,9 +1,9 @@ import { defaultKeymap, deleteCharBackwardStrict, history, historyKeymap, indentSelection, isolateHistory, redo, undo } from "@codemirror/commands"; import { cpp } from "@codemirror/lang-cpp"; import { markdown } from "@codemirror/lang-markdown"; -import { bracketMatching, foldGutter, indentOnInput, indentService, indentUnit } from "@codemirror/language"; +import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language"; import { highlightSelectionMatches, search, searchKeymap } from "@codemirror/search"; -import { EditorState, Extension } from "@codemirror/state"; +import { EditorSelection, EditorState, Extension } from "@codemirror/state"; import { crosshairCursor, drawSelection, dropCursor, EditorView, highlightActiveLine, highlightActiveLineGutter, keymap, lineNumbers, rectangularSelection, ViewUpdate } from "@codemirror/view"; import { CodeAnalyzer } from "../../common/analysis"; import { hex, rpad } from "../../common/util"; @@ -144,19 +144,23 @@ export class SourceEditor implements ProjectView { doc: text, extensions: [ - // Non-asm: 2-space indent (placed before settings so it takes precedence over tabSize-based indentUnit) - isAsm ? [] : indentUnit.of(" "), - // Asm: copy previous line's indentation since asm parsers lack proper indent rules - isAsm ? indentService.of((context, pos) => { - let lineNum = context.state.doc.lineAt(pos).number; - if (lineNum >= 0) { - let prevLine = context.state.doc.line(lineNum); - if (prevLine.text.trim()) { - return context.lineIndent(prevLine.from); - } + isAsm ? keymap.of([{ + key: "Enter", + run: (view) => { + // Copy leading whitespace up to cursor pos for each cursor. + const changes = view.state.changeByRange(range => { + const line = view.state.doc.lineAt(range.head); + const indent = line.text.match(/^[ \t]*/)[0].slice(0, range.head - line.from); + const insert = "\n" + indent; + return { + changes: { from: range.from, to: range.to, insert }, + range: EditorSelection.cursor(range.from + insert.length), + }; + }); + view.dispatch(changes, { scrollIntoView: true, userEvent: "input" }); + return true; } - return 0; - }) : [], + }]) : [], // Keybindings from settings must appear before default keymap. ...settingsExtensions(loadSettings()), From a07f8aa6fbc1fae2ed8aac0d06f448da35894d79 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 09:09:57 -0700 Subject: [PATCH 11/40] Unmap ctrl/cmd-shift-i from indentSelection Rely instead on indentSelection mapping from defaultKeymap / standardKeymap. --- src/ide/views/editors.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 0c98a95f4..cad244c0b 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -1,4 +1,4 @@ -import { defaultKeymap, deleteCharBackwardStrict, history, historyKeymap, indentSelection, isolateHistory, redo, undo } from "@codemirror/commands"; +import { defaultKeymap, deleteCharBackwardStrict, history, historyKeymap, isolateHistory, redo, undo } from "@codemirror/commands"; import { cpp } from "@codemirror/lang-cpp"; import { markdown } from "@codemirror/lang-markdown"; import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language"; @@ -165,10 +165,10 @@ export class SourceEditor implements ProjectView { // Keybindings from settings must appear before default keymap. ...settingsExtensions(loadSettings()), keymap.of([ - { key: "Ctrl-Shift-i", run: indentSelection }, - { key: "Cmd-Shift-i", run: indentSelection }, { key: "Backspace", run: deleteCharBackwardStrict }, ]), + // https://codemirror.net/docs/ref/#commands.defaultKeymap includes + // https://codemirror.net/docs/ref/#commands.standardKeymap keymap.of(defaultKeymap), lineNums ? lineNumbers() : [], From eda8c199f0a21f764142763fc0180f32b4311558 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 4 Apr 2026 15:48:28 -0700 Subject: [PATCH 12/40] Reevaluate line info gutter display on resize Move gutter line info into extension/compartment, dynamically determine whether to show info gutters as window is resized. --- src/ide/views/editors.ts | 30 +++++++----------------------- src/ide/views/gutter.ts | 26 ++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index cad244c0b..62b7d1286 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -21,11 +21,11 @@ import { disassemblyTheme } from "../../themes/disassemblyTheme"; import { editorTheme } from "../../themes/editorTheme"; import { mbo } from "../../themes/mbo"; import { loadSettings, registerEditor, settingsExtensions } from "../settings"; -import { clearBreakpoint, current_project, lastDebugState, platform, qs, runToPC } from "../ui"; +import { clearBreakpoint, current_project, lastDebugState, platform, runToPC } from "../ui"; import { createAssetHeaderPlugin } from "./assetdecorations"; import { isMobileDevice, ProjectView } from "./baseviews"; import { createTextTransformFilterEffect, textTransformFilterCompartment } from "./filters"; -import { breakpointMarkers, bytes, clock, currentPcMarker, errorMarkers, offset, statusMarkers } from "./gutter"; +import { breakpointMarkers, bytes, clock, currentPcMarker, errorMarkers, gutterLineInfo, offset, statusMarkers } from "./gutter"; import { currentPc, errorMessages, errorSpans, highlightLines, showValue } from "./visuals"; // look ahead this many bytes when finding source lines for a PC @@ -42,8 +42,8 @@ const MODEDEFS = { vasm: { isAsm: true }, inform6: { theme: cobalt }, markdown: { lineWrap: true }, - fastbasic: { noGutters: true }, - basic: { noLineNumbers: true, noGutters: true }, + fastbasic: { noGutterLineInfo: true }, + basic: { noGutterLineInfo: true }, ecs: { theme: mbo }, // TODO: is actually mixed-mode, as is verilog } @@ -62,8 +62,8 @@ export class SourceEditor implements ProjectView { } path: string; mode: string; - editor; - updateTimer = null; + editor: EditorView; + updateTimer: ReturnType | null = null; dirtylisting = true; sourcefile: SourceFile; currentDebugLine: SourceLocation; @@ -97,11 +97,6 @@ export class SourceEditor implements ProjectView { var lineWrap = !!modedef.lineWrap; var theme = modedef.theme || MODEDEFS.default.theme; var lineNums = !isAsm && !modedef.noLineNumbers && !isMobileDevice; - if (qs['embed']) { - lineNums = false; // no line numbers while embedded - isAsm = false; // no opcode bytes either - } - const minimalGutters = modedef.noGutters || isMobileDevice; var parser: Extension; switch (this.mode) { @@ -209,18 +204,7 @@ export class SourceEditor implements ProjectView { currentPc.field, - !minimalGutters ? [ - offset.field, - offset.gutter, - ] : [], - - isAsm && !minimalGutters ? [ - bytes.field, - bytes.gutter, - - clock.field, - clock.gutter, - ] : [], + gutterLineInfo(isAsm, !!modedef.noGutterLineInfo), breakpointMarkers.field, statusMarkers.gutter, diff --git a/src/ide/views/gutter.ts b/src/ide/views/gutter.ts index 6f6c9fbd9..f5f1a2376 100644 --- a/src/ide/views/gutter.ts +++ b/src/ide/views/gutter.ts @@ -1,6 +1,7 @@ -import { RangeSet, StateEffect, StateField } from "@codemirror/state"; -import { gutter, GutterMarker } from "@codemirror/view"; +import { Compartment, Extension, RangeSet, StateEffect, StateField } from "@codemirror/state"; +import { gutter, GutterMarker, ViewPlugin } from "@codemirror/view"; import { hex } from "../../common/util"; +import { isMobileDevice } from "./baseviews"; const setOffset = StateEffect.define>(); const setBytes = StateEffect.define>(); @@ -347,3 +348,24 @@ export const currentPcMarker = { field: currentPcField, gutter: currentPcGutter, }; + +export function gutterLineInfo(isAsm: boolean, alwaysHide: boolean): Extension { + const compartment = new Compartment(); + + const gutters = (): Extension => { + const hide = alwaysHide || isMobileDevice; + return [ + !hide ? [offsetField, offsetGutter] : [], + isAsm && !hide ? [bytesField, bytesGutter, clockField, clockGutter] : [], + ]; + }; + + return [ + compartment.of(gutters()), + ViewPlugin.define(view => { + const onResize = () => view.dispatch({ effects: compartment.reconfigure(gutters()) }); + window.addEventListener("resize", onResize); + return { destroy() { window.removeEventListener("resize", onResize); } }; + }), + ]; +} From 415fa6f3e3a8c32ecfe886a99725bfce9f1eb0a8 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sat, 4 Apr 2026 17:27:22 -0700 Subject: [PATCH 13/40] Asm tab-stop detection and formatting Detect opcode, operand, and comment column positions --- src/common/format-asm.ts | 94 ++++++++++++++++++++++++++ src/common/tabdetect.ts | 139 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 src/common/format-asm.ts create mode 100644 src/common/tabdetect.ts diff --git a/src/common/format-asm.ts b/src/common/format-asm.ts new file mode 100644 index 000000000..edbcafd9f --- /dev/null +++ b/src/common/format-asm.ts @@ -0,0 +1,94 @@ +// Pure formatting logic — no browser or CodeMirror dependencies. + +import { AsmTabStops, columnAt, findCommentIndex } from "./tabdetect"; + +function splitFirst(text: string): [string, string] { + const i = text.search(/\s/); + if (i < 0) return [text, '']; + return [text.substring(0, i), text.substring(i).trim()]; +} + +function padToColumn(s: string, targetCol: number, indentUnit: string, tabSize: number): string { + const col = columnAt(s, s.length, tabSize); + if (col >= targetCol) return s + ' '; + if (indentUnit === '\t') { + const nextTabCol = col + tabSize - (col % tabSize); + if (nextTabCol === targetCol) return s + '\t'; + } + return s + ' '.repeat(targetCol - col); +} + +function buildFormattedLine(indented: boolean, label: string, opcode: string, operand: string, comment: string, indentUnit: string, tabSize: number, asmTabStops: AsmTabStops): string { + let result = ''; + + if (label) { + result = label; + } + + if (opcode) { + if (label || indented) { + result = padToColumn(result, asmTabStops.opcodes, indentUnit, tabSize); + } + result += opcode; + if (operand) { + if (asmTabStops.operands !== undefined) { + result = padToColumn(result, asmTabStops.operands, indentUnit, tabSize); + } else { + result += ' '; + } + result += operand; + } + } + + if (comment) { + if (result) { + if (asmTabStops.comments !== undefined) { + result = padToColumn(result, asmTabStops.comments, indentUnit, tabSize); + } else { + result += ' '; + } + } + result += comment; + } + + return result; +} + +export function formatAsmLine(raw: string, lineNum: number, indentUnit: string, tabSize: number, asmTabStops: AsmTabStops): string { + const text = raw.trimEnd(); + if (text === '') return ''; + + const firstNonSpace = text.search(/\S/); + if (text[firstNonSpace] === ';') return text; + + const indented = firstNonSpace > 0; + const commentIdx = findCommentIndex(text); + const comment = text.substring(commentIdx); + const content = text.substring(indented ? firstNonSpace : 0, commentIdx).trimEnd(); + + let label = ''; + let opcode = ''; + let operand = ''; + + if (!indented) { + [label, opcode] = splitFirst(content); + if (opcode) [opcode, operand] = splitFirst(opcode); + } else { + [opcode, operand] = splitFirst(content); + } + + const newText = buildFormattedLine(indented, label, opcode, operand, comment, indentUnit, tabSize, asmTabStops); + if (newText.replace(/\s/g, '') !== text.replace(/\s/g, '')) { + console.warn(`format: skipping line ${lineNum}, mangles non-whitespace characters\n- before: ${JSON.stringify(text)}\n- after: ${JSON.stringify(newText)}`); + return text; + } + return newText; +} + +export function formatText(text: string, tabSize: number, asmTabStops: AsmTabStops): string { + if (!asmTabStops) return text; + const indent = ' '.repeat(tabSize); + const lines = text.split('\n'); + const formatted = lines.map((line, i) => formatAsmLine(line, i + 1, indent, tabSize, asmTabStops)); + return formatted.join('\n'); +} diff --git a/src/common/tabdetect.ts b/src/common/tabdetect.ts new file mode 100644 index 000000000..53a5b2d72 --- /dev/null +++ b/src/common/tabdetect.ts @@ -0,0 +1,139 @@ +// Pure tab-detection and column utilities — no browser or CodeMirror dependencies. +// Used by both the IDE (via tabs.ts) and CLI tools (reformat.ts). + +import { opcodes as opcodes6502 } from "../parser/tokens-6502"; +import { opcodes as opcodes6809 } from "../parser/tokens-6809"; +import { opcodes as opcodesZ80 } from "../parser/tokens-z80"; + +export interface AsmTabStops { + opcodes?: number; + operands?: number; + comments?: number; +} + +export function tabStopsEquals(a: AsmTabStops, b: AsmTabStops): boolean { + return a.opcodes === b.opcodes && a.operands === b.operands && a.comments === b.comments; +} + +export function findCommentIndex(text: string): number { + let quote = ''; + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + if (quote) { + if (ch === quote) quote = ''; + } else if (ch === '"' || ch === "'") { + quote = ch; + } else if (ch === ';') { + return i; + } + } + return text.length; +} + +export function columnAt(text: string, offset: number, tabSize: number): number { + let col = 0; + for (let i = 0; i < offset; i++) { + if (text[i] === '\t') col = col + tabSize - (col % tabSize); + else col++; + } + return col; +} + +const MNEMONICS_6502 = new Set([...opcodes6502].map(s => s.toLowerCase())); +const MNEMONICS_6809 = new Set([...opcodes6809].map(s => s.toLowerCase())); +const MNEMONICS_Z80 = new Set([...opcodesZ80].map(s => s.toLowerCase())); + +const mnemonicsByMode: Record> = { + '6502': MNEMONICS_6502, + '6809': MNEMONICS_6809, + 'z80': MNEMONICS_Z80, +}; + +function mostCommon(counts: Map): number | undefined { + if (counts.size === 0) return undefined; + let best: number | undefined; + let bestCount = 0; + counts.forEach((count, val) => { + if (count > bestCount) { best = val; bestCount = count; } + }); + return best; +} + +function tally(map: Map, key: number) { + map.set(key, (map.get(key) || 0) + 1); +} + +export function detectTabStopsFromAsm(mode: string, tabSize: number, text: string): AsmTabStops { + const mnemonics = mnemonicsByMode[mode]; + if (!mnemonics) { + return {}; + } + + // Pass 1: find most common opcode column + const opcodeCols = new Map(); + const lineInfos: [number, number?, number?][] = []; // [opcodeCol, operandCol?, commentCol?] + + const lines = text.split('\n'); + for (const line of lines) { + const commentIdx = findCommentIndex(line); + const code = line.substring(0, commentIdx); + const indented = /^\s/.test(code); + + const opcodeMatch = indented ? code.match(/^\s+(\S+)/) : code.match(/^\S+\s+(\S+)/); + if (!opcodeMatch) continue; + const token = opcodeMatch[1]; + if (!mnemonics.has(token.toLowerCase())) continue; + + const col = columnAt(line, opcodeMatch[0].length - token.length, tabSize); + tally(opcodeCols, col); + const afterOpcode = opcodeMatch[0].length; + const operandMatch = code.substring(afterOpcode).match(/^\s+\S/); + const operandCol = operandMatch ? columnAt(line, afterOpcode + operandMatch[0].length - 1, tabSize) : undefined; + const commentCol = commentIdx < line.length ? columnAt(line, commentIdx, tabSize) : undefined; + lineInfos.push([col, operandCol, commentCol]); + } + + const opcodeCol = mostCommon(opcodeCols); + if (opcodeCol === undefined) return {}; + + // Pass 2: from lines with opcode at winning column, find operand and comment columns + const operandCols = new Map(); + const commentCols = new Map(); + for (const [oc, operandCol, commentCol] of lineInfos) { + if (oc !== opcodeCol) continue; + if (operandCol !== undefined) tally(operandCols, operandCol); + if (commentCol !== undefined) tally(commentCols, commentCol); + } + + const asmTabStops = { opcodes: opcodeCol } as AsmTabStops; + const operandCol = mostCommon(operandCols); + if (operandCol !== undefined) asmTabStops.operands = operandCol; + const commentCol = mostCommon(commentCols); + if (commentCol !== undefined) asmTabStops.comments = commentCol; + return asmTabStops; +} + +export function detectTabSizeFromSource(text: string): number | undefined { + const cols = new Map(); + for (const line of text.split('\n')) { + const match = line.match(/^(\s+)\S/); + if (!match) continue; + const col = columnAt(line, match[1].length, 8); + tally(cols, col); + } + // In our examples, this almost always returns the first tab stop, + // though sometimes the second tab stop competes for the top count. + let tabSize = mostCommon(cols); + // Could tabSize be the second tab stop? + if (tabSize >= 4) { + // Check candidate tab stop at the half-way point. + const halfTabSize = tabSize / 2; + const countTabSize = cols.get(tabSize); + const countHalfTabSize = cols.get(halfTabSize); + // Does the candidate have at least half as many tallies? + if (countHalfTabSize >= countTabSize / 2) { + tabSize = halfTabSize; + } + } + return tabSize; +} From f7f01230b7fcf08a5af9cbc6c03566b2a4303bbd Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 10:17:02 -0700 Subject: [PATCH 14/40] Add getModeForPath in ui.ts Export getModeForPath to support file formatting. --- src/ide/ui.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ide/ui.ts b/src/ide/ui.ts index e2626a4cf..a378e8482 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -162,6 +162,16 @@ const TOOL_TO_HELPURL = { 'acme': 'https://raw.githubusercontent.com/sehugg/acme/main/docs/QuickRef.txt', } +export function getModeForPath(path: string) { + var tool = platform.getToolForFilename(path); + // hack because .h files can be DASM or CC65 + if (tool == 'dasm' && path.endsWith(".h") && getCurrentMainFilename().endsWith(".c")) { + tool = 'cc65'; + } + var mode = tool && TOOL_TO_SOURCE_STYLE[tool]; + return mode; +} + function newWorker(): Worker { // TODO: return new Worker("https://8bitworkshop.com.s3-website-us-east-1.amazonaws.com/dev/gen/worker/bundle.js"); return new Worker("./gen/worker/bundle.js"); @@ -310,21 +320,11 @@ function refreshWindowList() { } } - function loadEditor(path: string) { - var tool = platform.getToolForFilename(path); - // hack because .h files can be DASM or CC65 - if (tool == 'dasm' && path.endsWith(".h") && getCurrentMainFilename().endsWith(".c")) { - tool = 'cc65'; - } - var mode = tool && TOOL_TO_SOURCE_STYLE[tool]; - return new SourceEditor(path, mode); - } - function addEditorItem(id: string) { addWindowItem(id, getFilenameForPath(id), () => { var data = current_project.getFile(id); if (typeof data === 'string') - return loadEditor(id); + return new SourceEditor(id, getModeForPath(id)); else if (data instanceof Uint8Array) return new BinaryFileView(id, data as Uint8Array); }); @@ -493,7 +493,7 @@ async function getSkeletonFile(fileid: string): Promise { try { return await $.get("presets/" + getBasePlatform(platform_id) + "/skeleton." + ext, 'text'); } catch (e) { - console.log(e+""); + console.log(e + ""); return null; } } From d0a7c39842ba6817b79c56d150cb67c0bcbeb604 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 10:38:04 -0700 Subject: [PATCH 15/40] Add isAsmMode in ui.ts Export isAsmMode to support file formatting. --- src/ide/ui.ts | 6 ++++++ src/ide/views/editors.ts | 9 ++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ide/ui.ts b/src/ide/ui.ts index a378e8482..338b87fe0 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -145,6 +145,12 @@ const TOOL_TO_SOURCE_STYLE = { 'oscar64': 'text/x-csrc', } +const ASM_MODES = new Set(['6502', 'z80', '6809', 'gas', 'vasm', 'jsasm']); + +export function isAsmMode(mode: string): boolean { + return ASM_MODES.has(mode); +} + // TODO: move into tool class const TOOL_TO_HELPURL = { 'dasm': 'https://raw.githubusercontent.com/sehugg/dasm/master/doc/dasm.txt', diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 62b7d1286..27a5e1b22 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -21,7 +21,7 @@ import { disassemblyTheme } from "../../themes/disassemblyTheme"; import { editorTheme } from "../../themes/editorTheme"; import { mbo } from "../../themes/mbo"; import { loadSettings, registerEditor, settingsExtensions } from "../settings"; -import { clearBreakpoint, current_project, lastDebugState, platform, runToPC } from "../ui"; +import { clearBreakpoint, current_project, isAsmMode, lastDebugState, platform, runToPC } from "../ui"; import { createAssetHeaderPlugin } from "./assetdecorations"; import { isMobileDevice, ProjectView } from "./baseviews"; import { createTextTransformFilterEffect, textTransformFilterCompartment } from "./filters"; @@ -35,11 +35,6 @@ const MAX_ERRORS = 200; const MODEDEFS = { default: { theme: mbo }, // NOTE: Not merged w/ other modes - '6502': { isAsm: true }, - z80: { isAsm: true }, - jsasm: { isAsm: true }, - gas: { isAsm: true }, - vasm: { isAsm: true }, inform6: { theme: cobalt }, markdown: { lineWrap: true }, fastbasic: { noGutterLineInfo: true }, @@ -93,7 +88,7 @@ export class SourceEditor implements ProjectView { newEditor(parent: HTMLElement, text: string, isAsmOverride?: boolean) { var modedef = MODEDEFS[this.mode] || MODEDEFS.default; - var isAsm = isAsmOverride || modedef.isAsm; + var isAsm = isAsmOverride || isAsmMode(this.mode); var lineWrap = !!modedef.lineWrap; var theme = modedef.theme || MODEDEFS.default.theme; var lineNums = !isAsm && !modedef.noLineNumbers && !isMobileDevice; From a117ca3350d2dcdd26d838a283d1701dbd347faa Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 11:11:25 -0700 Subject: [PATCH 16/40] Verilog __asm/__endasm gutter info override Limit effects of verilog files containing __asm and __endasm markers to showing gutter info, no longer change Shift-Alt-F and Enter behavior for the entire file. --- src/ide/views/editors.ts | 11 ++++++----- src/ide/views/gutter.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 27a5e1b22..26cd6b81f 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -69,8 +69,9 @@ export class SourceEditor implements ProjectView { div.setAttribute("class", "editor"); parent.appendChild(div); var text = current_project.getFile(this.path) as string; - var asmOverride = text && this.mode == 'verilog' && /__asm\b([\s\S]+?)\b__endasm\b/.test(text); - this.newEditor(div, text, asmOverride); + // TODO support mixed language parsing: https://codemirror.net/examples/mixed-language/ + var includesAsm = text && this.mode == 'verilog' && /__asm\b([\s\S]+?)\b__endasm\b/.test(text); + this.newEditor(div, text, includesAsm); this.editor.dispatch({ effects: createTextTransformFilterEffect(textMapFunctions), }); @@ -86,9 +87,9 @@ export class SourceEditor implements ProjectView { } } - newEditor(parent: HTMLElement, text: string, isAsmOverride?: boolean) { + newEditor(parent: HTMLElement, text: string, includesAsm?: boolean) { var modedef = MODEDEFS[this.mode] || MODEDEFS.default; - var isAsm = isAsmOverride || isAsmMode(this.mode); + var isAsm = isAsmMode(this.mode); var lineWrap = !!modedef.lineWrap; var theme = modedef.theme || MODEDEFS.default.theme; var lineNums = !isAsm && !modedef.noLineNumbers && !isMobileDevice; @@ -199,7 +200,7 @@ export class SourceEditor implements ProjectView { currentPc.field, - gutterLineInfo(isAsm, !!modedef.noGutterLineInfo), + gutterLineInfo(isAsm || includesAsm, !!modedef.noGutterLineInfo), breakpointMarkers.field, statusMarkers.gutter, diff --git a/src/ide/views/gutter.ts b/src/ide/views/gutter.ts index f5f1a2376..3a7e89590 100644 --- a/src/ide/views/gutter.ts +++ b/src/ide/views/gutter.ts @@ -349,14 +349,14 @@ export const currentPcMarker = { gutter: currentPcGutter, }; -export function gutterLineInfo(isAsm: boolean, alwaysHide: boolean): Extension { +export function gutterLineInfo(isOrIncludesAsm: boolean, alwaysHide: boolean): Extension { const compartment = new Compartment(); const gutters = (): Extension => { const hide = alwaysHide || isMobileDevice; return [ !hide ? [offsetField, offsetGutter] : [], - isAsm && !hide ? [bytesField, bytesGutter, clockField, clockGutter] : [], + isOrIncludesAsm && !hide ? [bytesField, bytesGutter, clockField, clockGutter] : [], ]; }; From d316a4b5ec761705acfe7bc72112145e4082814f Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 14:32:23 -0700 Subject: [PATCH 17/40] setting.ts saveAndApplySettings --- src/ide/settings.ts | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index 7fede98c0..dfed95b1c 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -16,6 +16,16 @@ export const closeBracketsCompartment = new Compartment(); export const tabsToSpacesCompartment = new Compartment(); export const debugHighlightTagsCompartment = new Compartment(); +const editors: Set = new Set(); + +export function registerEditor(editor: EditorView) { + editors.add(editor); +} + +export function unregisterEditor(editor: EditorView) { + editors.delete(editor); +} + export interface EditorSettings { tabSize: number; tabsToSpaces: boolean; @@ -40,7 +50,7 @@ const defaultSettings: EditorSettings = { export function loadSettings(): EditorSettings { try { - var stored = localStorage.getItem(SETTINGS_KEY); + const stored = localStorage.getItem(SETTINGS_KEY); if (stored) { return { ...defaultSettings, ...JSON.parse(stored) }; } @@ -48,8 +58,12 @@ export function loadSettings(): EditorSettings { return { ...defaultSettings }; } -export function saveSettings(settings: EditorSettings) { +export function saveAndApplySettings(settings: EditorSettings) { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); + const effects = compartmentValues.map(([c, fn]) => c.reconfigure(fn(settings))); + for (const editor of editors) { + editor.dispatch({ effects }); + } } const compartmentValues: [Compartment, (s: EditorSettings) => Extension][] = [ @@ -66,24 +80,6 @@ export function settingsExtensions(settings: EditorSettings): Extension[] { return compartmentValues.map(([c, fn]) => c.of(fn(settings))); } -// Track all active editor views so we can reconfigure them -const activeEditors: Set = new Set(); - -export function registerEditor(editor: EditorView) { - activeEditors.add(editor); -} - -export function unregisterEditor(editor: EditorView) { - activeEditors.delete(editor); -} - -export function applySettingsToAll(settings: EditorSettings) { - var effects = compartmentValues.map(([c, fn]) => c.reconfigure(fn(settings))); - for (var editor of activeEditors) { - editor.dispatch({ effects }); - } -} - export function openSettings() { var settings = loadSettings(); bootbox.dialog({ @@ -117,8 +113,7 @@ export function openSettings() { settings.highlightTrailingWhitespace = $('#setting_highlightTrailingWhitespace').is(':checked'); settings.closeBrackets = $('#setting_closeBrackets').is(':checked'); settings.debugHighlightTags = $('#setting_debugHighlightTags').is(':checked'); - saveSettings(settings); - applySettingsToAll(settings); + saveAndApplySettings(settings); } } } From aa7b216e4a6b0fc460cba950cb87ba4488ea21bd Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 14:59:24 -0700 Subject: [PATCH 18/40] Minor updates to settings - Rephrase settings descriptions - Highlight special chars by default - Hightlight trailing whitespace by default - Sort highlight related settings - Title, headings, CSS --- css/ui.css | 3 +++ src/ide/settings.ts | 30 +++++++++++++++--------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/css/ui.css b/css/ui.css index 31cf87698..73da7c44a 100644 --- a/css/ui.css +++ b/css/ui.css @@ -880,6 +880,9 @@ div.scripting-cell button:hover { div.scripting-cell button.scripting-enabled { background-color: #339999; } +#settingsForm h5:not(:first-child) { + margin-top: 24px; +} .dialog-help { color: #6666ff; } \ No newline at end of file diff --git a/src/ide/settings.ts b/src/ide/settings.ts index dfed95b1c..6e01b1238 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -8,12 +8,12 @@ import { insertTabKeymap, smartIndentKeymap } from "./views/tabs"; declare var bootbox; declare var $: JQueryStatic; +export const tabSizeCompartment = new Compartment(); +export const tabsToSpacesCompartment = new Compartment(); export const highlightSpecialCharsCompartment = new Compartment(); -export const highlightWhitespaceCompartment = new Compartment(); export const highlightTrailingWhitespaceCompartment = new Compartment(); -export const tabSizeCompartment = new Compartment(); +export const highlightWhitespaceCompartment = new Compartment(); export const closeBracketsCompartment = new Compartment(); -export const tabsToSpacesCompartment = new Compartment(); export const debugHighlightTagsCompartment = new Compartment(); const editors: Set = new Set(); @@ -30,8 +30,8 @@ export interface EditorSettings { tabSize: number; tabsToSpaces: boolean; highlightSpecialChars: boolean; - highlightWhitespace: boolean; highlightTrailingWhitespace: boolean; + highlightWhitespace: boolean; closeBrackets: boolean; debugHighlightTags: boolean; } @@ -41,9 +41,9 @@ const SETTINGS_KEY = "8bitworkshop/editorSettings"; const defaultSettings: EditorSettings = { tabSize: 8, tabsToSpaces: true, - highlightSpecialChars: false, + highlightSpecialChars: true, + highlightTrailingWhitespace: true, highlightWhitespace: false, - highlightTrailingWhitespace: false, closeBrackets: false, debugHighlightTags: false, }; @@ -70,8 +70,8 @@ const compartmentValues: [Compartment, (s: EditorSettings) => Extension][] = [ [tabSizeCompartment, s => [EditorState.tabSize.of(s.tabSize), indentUnit.of(" ".repeat(s.tabSize))]], [tabsToSpacesCompartment, s => keymap.of(s.tabsToSpaces ? smartIndentKeymap : insertTabKeymap)], [highlightSpecialCharsCompartment, s => s.highlightSpecialChars ? highlightSpecialChars() : []], - [highlightWhitespaceCompartment, s => s.highlightWhitespace ? highlightWhitespace() : []], [highlightTrailingWhitespaceCompartment, s => s.highlightTrailingWhitespace ? highlightTrailingWhitespace() : []], + [highlightWhitespaceCompartment, s => s.highlightWhitespace ? highlightWhitespace() : []], [closeBracketsCompartment, s => s.closeBrackets ? [closeBrackets(), keymap.of([{ key: "Backspace", run: deleteBracketPair }])] : []], [debugHighlightTagsCompartment, s => s.debugHighlightTags ? debugHighlightTagsTooltip : []], ]; @@ -84,18 +84,18 @@ export function openSettings() { var settings = loadSettings(); bootbox.dialog({ onEscape: true, - title: "Settings", + // title: "Settings", message: `
-
Editor preferences
+
Editor settings
-
-
-
+
+
+
-
+
8bitworkshop IDE internal settings
-
+
`, buttons: { cancel: { @@ -109,8 +109,8 @@ export function openSettings() { settings.tabSize = parseInt($('#setting_tabSize').val() as string) || 8; settings.tabsToSpaces = $('#setting_tabsToSpaces').is(':checked'); settings.highlightSpecialChars = $('#setting_highlightSpecialChars').is(':checked'); - settings.highlightWhitespace = $('#setting_highlightWhitespace').is(':checked'); settings.highlightTrailingWhitespace = $('#setting_highlightTrailingWhitespace').is(':checked'); + settings.highlightWhitespace = $('#setting_highlightWhitespace').is(':checked'); settings.closeBrackets = $('#setting_closeBrackets').is(':checked'); settings.debugHighlightTags = $('#setting_debugHighlightTags').is(':checked'); saveAndApplySettings(settings); From 34de4f651d46ddc8257215e019542c60b8c4c4d4 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 15:08:37 -0700 Subject: [PATCH 19/40] Show line numbers setting Defaults to off on mobile devices. --- src/ide/settings.ts | 9 ++++++++- src/ide/views/editors.ts | 7 ++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index 6e01b1238..5cfbb3eff 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -1,7 +1,8 @@ import { closeBrackets, deleteBracketPair } from "@codemirror/autocomplete"; import { indentUnit } from "@codemirror/language"; import { Compartment, EditorState, Extension } from "@codemirror/state"; -import { EditorView, highlightSpecialChars, highlightTrailingWhitespace, highlightWhitespace, keymap } from "@codemirror/view"; +import { EditorView, highlightSpecialChars, highlightTrailingWhitespace, highlightWhitespace, keymap, lineNumbers } from "@codemirror/view"; +import { isMobileDevice } from "./views/baseviews"; import { debugHighlightTagsTooltip } from "./views/debug"; import { insertTabKeymap, smartIndentKeymap } from "./views/tabs"; @@ -10,6 +11,7 @@ declare var $: JQueryStatic; export const tabSizeCompartment = new Compartment(); export const tabsToSpacesCompartment = new Compartment(); +export const showLineNumbersCompartment = new Compartment(); export const highlightSpecialCharsCompartment = new Compartment(); export const highlightTrailingWhitespaceCompartment = new Compartment(); export const highlightWhitespaceCompartment = new Compartment(); @@ -29,6 +31,7 @@ export function unregisterEditor(editor: EditorView) { export interface EditorSettings { tabSize: number; tabsToSpaces: boolean; + showLineNumbers: boolean; highlightSpecialChars: boolean; highlightTrailingWhitespace: boolean; highlightWhitespace: boolean; @@ -41,6 +44,7 @@ const SETTINGS_KEY = "8bitworkshop/editorSettings"; const defaultSettings: EditorSettings = { tabSize: 8, tabsToSpaces: true, + showLineNumbers: !isMobileDevice, highlightSpecialChars: true, highlightTrailingWhitespace: true, highlightWhitespace: false, @@ -69,6 +73,7 @@ export function saveAndApplySettings(settings: EditorSettings) { const compartmentValues: [Compartment, (s: EditorSettings) => Extension][] = [ [tabSizeCompartment, s => [EditorState.tabSize.of(s.tabSize), indentUnit.of(" ".repeat(s.tabSize))]], [tabsToSpacesCompartment, s => keymap.of(s.tabsToSpaces ? smartIndentKeymap : insertTabKeymap)], + [showLineNumbersCompartment, s => s.showLineNumbers ? lineNumbers() : []], [highlightSpecialCharsCompartment, s => s.highlightSpecialChars ? highlightSpecialChars() : []], [highlightTrailingWhitespaceCompartment, s => s.highlightTrailingWhitespace ? highlightTrailingWhitespace() : []], [highlightWhitespaceCompartment, s => s.highlightWhitespace ? highlightWhitespace() : []], @@ -89,6 +94,7 @@ export function openSettings() {
Editor settings
+
@@ -108,6 +114,7 @@ export function openSettings() { callback: () => { settings.tabSize = parseInt($('#setting_tabSize').val() as string) || 8; settings.tabsToSpaces = $('#setting_tabsToSpaces').is(':checked'); + settings.showLineNumbers = $('#setting_showLineNumbers').is(':checked'); settings.highlightSpecialChars = $('#setting_highlightSpecialChars').is(':checked'); settings.highlightTrailingWhitespace = $('#setting_highlightTrailingWhitespace').is(':checked'); settings.highlightWhitespace = $('#setting_highlightWhitespace').is(':checked'); diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 26cd6b81f..617eee71a 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -4,7 +4,7 @@ import { markdown } from "@codemirror/lang-markdown"; import { bracketMatching, foldGutter, indentOnInput } from "@codemirror/language"; import { highlightSelectionMatches, search, searchKeymap } from "@codemirror/search"; import { EditorSelection, EditorState, Extension } from "@codemirror/state"; -import { crosshairCursor, drawSelection, dropCursor, EditorView, highlightActiveLine, highlightActiveLineGutter, keymap, lineNumbers, rectangularSelection, ViewUpdate } from "@codemirror/view"; +import { crosshairCursor, drawSelection, dropCursor, EditorView, highlightActiveLine, highlightActiveLineGutter, keymap, rectangularSelection, ViewUpdate } from "@codemirror/view"; import { CodeAnalyzer } from "../../common/analysis"; import { hex, rpad } from "../../common/util"; import { SourceFile, SourceLocation, WorkerError } from "../../common/workertypes"; @@ -23,7 +23,7 @@ import { mbo } from "../../themes/mbo"; import { loadSettings, registerEditor, settingsExtensions } from "../settings"; import { clearBreakpoint, current_project, isAsmMode, lastDebugState, platform, runToPC } from "../ui"; import { createAssetHeaderPlugin } from "./assetdecorations"; -import { isMobileDevice, ProjectView } from "./baseviews"; +import { ProjectView } from "./baseviews"; import { createTextTransformFilterEffect, textTransformFilterCompartment } from "./filters"; import { breakpointMarkers, bytes, clock, currentPcMarker, errorMarkers, gutterLineInfo, offset, statusMarkers } from "./gutter"; import { currentPc, errorMessages, errorSpans, highlightLines, showValue } from "./visuals"; @@ -92,7 +92,6 @@ export class SourceEditor implements ProjectView { var isAsm = isAsmMode(this.mode); var lineWrap = !!modedef.lineWrap; var theme = modedef.theme || MODEDEFS.default.theme; - var lineNums = !isAsm && !modedef.noLineNumbers && !isMobileDevice; var parser: Extension; switch (this.mode) { @@ -162,8 +161,6 @@ export class SourceEditor implements ProjectView { // https://codemirror.net/docs/ref/#commands.standardKeymap keymap.of(defaultKeymap), - lineNums ? lineNumbers() : [], - // Undo history. history(), keymap.of(historyKeymap), From 204c47fcd9f737f3ebcffe226b9fa296451c7d86 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 16:50:20 -0700 Subject: [PATCH 20/40] Settings dialog: Enter key saves and closes --- src/ide/settings.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index 5cfbb3eff..14d55b2c6 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -87,7 +87,7 @@ export function settingsExtensions(settings: EditorSettings): Extension[] { export function openSettings() { var settings = loadSettings(); - bootbox.dialog({ + const dialog = bootbox.dialog({ onEscape: true, // title: "Settings", message: `
@@ -108,7 +108,7 @@ export function openSettings() { label: "Cancel", className: "btn-default" }, - ok: { + save: { label: "SAVE", className: "btn-primary", callback: () => { @@ -125,4 +125,10 @@ export function openSettings() { } } }); + dialog.on('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + dialog.find('.modal-footer .btn-primary').trigger('click'); + } + }); } From 818a5a08c0fa8e36d20353385ae50670014278db Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 17:30:05 -0700 Subject: [PATCH 21/40] Reformat settings.ts --- src/ide/settings.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index 14d55b2c6..f91c6b247 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -91,18 +91,17 @@ export function openSettings() { onEscape: true, // title: "Settings", message: ` -
Editor settings
-
-
-
-
-
-
-
- -
8bitworkshop IDE internal settings
-
- `, +
Editor settings
+
+
+
+
+
+
+
+
8bitworkshop IDE internal settings
+
+ `, buttons: { cancel: { label: "Cancel", From 25daa7d87f10c4cd5599f5b9a98ce241f88e71a5 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 18:26:14 -0700 Subject: [PATCH 22/40] Compartment for tab related extensions --- css/ui.css | 9 ++++++++- src/ide/settings.ts | 33 +++++++++++++++++++++------------ src/ide/views/tabs.ts | 25 ++++++++++++++----------- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/css/ui.css b/css/ui.css index 73da7c44a..26c62cd07 100644 --- a/css/ui.css +++ b/css/ui.css @@ -885,4 +885,11 @@ div.scripting-cell button.scripting-enabled { } .dialog-help { color: #6666ff; -} \ No newline at end of file +} +#settingsForm label { + font-weight: normal; +} +#settingsForm label.main { + font-weight: bold; + margin-right: 4px; +} diff --git a/src/ide/settings.ts b/src/ide/settings.ts index f91c6b247..55d52c864 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -1,16 +1,18 @@ import { closeBrackets, deleteBracketPair } from "@codemirror/autocomplete"; -import { indentUnit } from "@codemirror/language"; -import { Compartment, EditorState, Extension } from "@codemirror/state"; +import { Compartment, Extension } from "@codemirror/state"; import { EditorView, highlightSpecialChars, highlightTrailingWhitespace, highlightWhitespace, keymap, lineNumbers } from "@codemirror/view"; import { isMobileDevice } from "./views/baseviews"; import { debugHighlightTagsTooltip } from "./views/debug"; -import { insertTabKeymap, smartIndentKeymap } from "./views/tabs"; +import { tabExtension } from "./views/tabs"; + +const MIN_TAB_SIZE = 1; +const MAX_TAB_SIZE = 40; +const DEFAULT_TAB_SIZE = 8; declare var bootbox; declare var $: JQueryStatic; -export const tabSizeCompartment = new Compartment(); -export const tabsToSpacesCompartment = new Compartment(); +export const tabCompartment = new Compartment(); export const showLineNumbersCompartment = new Compartment(); export const highlightSpecialCharsCompartment = new Compartment(); export const highlightTrailingWhitespaceCompartment = new Compartment(); @@ -42,7 +44,7 @@ export interface EditorSettings { const SETTINGS_KEY = "8bitworkshop/editorSettings"; const defaultSettings: EditorSettings = { - tabSize: 8, + tabSize: DEFAULT_TAB_SIZE, tabsToSpaces: true, showLineNumbers: !isMobileDevice, highlightSpecialChars: true, @@ -71,8 +73,7 @@ export function saveAndApplySettings(settings: EditorSettings) { } const compartmentValues: [Compartment, (s: EditorSettings) => Extension][] = [ - [tabSizeCompartment, s => [EditorState.tabSize.of(s.tabSize), indentUnit.of(" ".repeat(s.tabSize))]], - [tabsToSpacesCompartment, s => keymap.of(s.tabsToSpaces ? smartIndentKeymap : insertTabKeymap)], + [tabCompartment, s => tabExtension(s.tabSize, s.tabsToSpaces)], [showLineNumbersCompartment, s => s.showLineNumbers ? lineNumbers() : []], [highlightSpecialCharsCompartment, s => s.highlightSpecialChars ? highlightSpecialChars() : []], [highlightTrailingWhitespaceCompartment, s => s.highlightTrailingWhitespace ? highlightTrailingWhitespace() : []], @@ -92,13 +93,21 @@ export function openSettings() { // title: "Settings", message: `
Editor settings
-
-
+
+ +
+
+ + + +
+
+
8bitworkshop IDE internal settings
`, @@ -111,8 +120,8 @@ export function openSettings() { label: "SAVE", className: "btn-primary", callback: () => { - settings.tabSize = parseInt($('#setting_tabSize').val() as string) || 8; - settings.tabsToSpaces = $('#setting_tabsToSpaces').is(':checked'); + settings.tabSize = Math.min(MAX_TAB_SIZE, Math.max(MIN_TAB_SIZE, parseInt($('#setting_tabSize').val() as string) || MIN_TAB_SIZE)); + settings.tabsToSpaces = $('#setting_tabInsertsSpaces').is(':checked'); settings.showLineNumbers = $('#setting_showLineNumbers').is(':checked'); settings.highlightSpecialChars = $('#setting_highlightSpecialChars').is(':checked'); settings.highlightTrailingWhitespace = $('#setting_highlightTrailingWhitespace').is(':checked'); diff --git a/src/ide/views/tabs.ts b/src/ide/views/tabs.ts index c173e9242..7fed49c26 100644 --- a/src/ide/views/tabs.ts +++ b/src/ide/views/tabs.ts @@ -1,12 +1,15 @@ -import { indentLess, indentMore, insertTab } from "@codemirror/commands"; -import { KeyBinding } from "@codemirror/view"; +import { indentLess, insertTab } from "@codemirror/commands"; +import { indentUnit } from "@codemirror/language"; +import { EditorState, Extension } from "@codemirror/state"; +import { keymap } from "@codemirror/view"; -export const smartIndentKeymap: KeyBinding[] = [ - { key: "Tab", run: indentMore }, - { key: "Shift-Tab", run: indentLess }, -]; - -export const insertTabKeymap: KeyBinding[] = [ - { key: "Tab", run: insertTab }, - { key: "Shift-Tab", run: indentLess }, -]; +export function tabExtension(tabSize: number, tabsToSpaces: boolean): Extension { + return [ + EditorState.tabSize.of(tabSize), + indentUnit.of(tabsToSpaces ? " ".repeat(tabSize) : "\t"), + keymap.of([ + { key: "Tab", run: insertTab }, + { key: "Shift-Tab", run: indentLess } + ]), + ]; +} From 8284e3945bb2fc88e3c257a5dc5245f27dd2a0da Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 17:44:49 -0700 Subject: [PATCH 23/40] settings.ts, apply settings to UI in updateUI --- src/ide/settings.ts | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index 55d52c864..fd11f69d3 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -87,29 +87,42 @@ export function settingsExtensions(settings: EditorSettings): Extension[] { } export function openSettings() { - var settings = loadSettings(); + function updateUI(s: EditorSettings) { + $('#setting_tabSize').val(s.tabSize); + $('#setting_tabInsertsTabs').prop('checked', !s.tabsToSpaces); + $('#setting_tabInsertsSpaces').prop('checked', s.tabsToSpaces); + $('#setting_showLineNumbers').prop('checked', s.showLineNumbers); + $('#setting_highlightSpecialChars').prop('checked', s.highlightSpecialChars); + $('#setting_highlightTrailingWhitespace').prop('checked', s.highlightTrailingWhitespace); + $('#setting_highlightWhitespace').prop('checked', s.highlightWhitespace); + $('#setting_closeBrackets').prop('checked', s.closeBrackets); + $('#setting_debugHighlightTags').prop('checked', s.debugHighlightTags); + $('input[name="tabMode"]').first().trigger('change'); + } + + let settings = loadSettings(); const dialog = bootbox.dialog({ onEscape: true, // title: "Settings", message: `
Editor settings
- +
- - + +
-
-
-
-
-
+
+
+
+
+
8bitworkshop IDE internal settings
-
+
`, buttons: { cancel: { @@ -133,6 +146,9 @@ export function openSettings() { } } }); + dialog.on('shown.bs.modal', () => { + updateUI(settings); + }); dialog.on('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); From a6f8477b0166ba5e53e7d3554acf02cacdfec308 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 17:46:35 -0700 Subject: [PATCH 24/40] Settings 'Reset' button restores defaults --- src/ide/settings.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index fd11f69d3..f18552ab0 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -125,6 +125,15 @@ export function openSettings() {
`, buttons: { + reset: { + label: "Reset", + className: "btn-default", + callback: () => { + settings = { ...defaultSettings }; + updateUI(settings); + return false; + } + }, cancel: { label: "Cancel", className: "btn-default" From 1098baae9101d11e17752ad8639a9af5504d4714 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 17:50:03 -0700 Subject: [PATCH 25/40] Settings for opcode, operand, comments columns --- css/ui.css | 9 +++++++++ src/ide/settings.ts | 23 +++++++++++++++++++++-- src/ide/views/tabs.ts | 4 +++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/css/ui.css b/css/ui.css index 26c62cd07..ebe750f54 100644 --- a/css/ui.css +++ b/css/ui.css @@ -893,3 +893,12 @@ div.scripting-cell button.scripting-enabled { font-weight: bold; margin-right: 4px; } +#settingsForm .tab-stops .tab-stop { + margin-left: 0.5em; +} +#settingsForm .tab-stops input[type="text"] { + width: 2em; +} +#settingsForm .disabled { + opacity: 0.5; +} diff --git a/src/ide/settings.ts b/src/ide/settings.ts index f18552ab0..f9de58f10 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -1,6 +1,7 @@ import { closeBrackets, deleteBracketPair } from "@codemirror/autocomplete"; -import { Compartment, Extension } from "@codemirror/state"; +import { Compartment, Extension, Facet } from "@codemirror/state"; import { EditorView, highlightSpecialChars, highlightTrailingWhitespace, highlightWhitespace, keymap, lineNumbers } from "@codemirror/view"; +import { AsmTabStops } from "../common/tabdetect"; import { isMobileDevice } from "./views/baseviews"; import { debugHighlightTagsTooltip } from "./views/debug"; import { tabExtension } from "./views/tabs"; @@ -30,9 +31,14 @@ export function unregisterEditor(editor: EditorView) { editors.delete(editor); } +export const tabStopsFacet = Facet.define({ + combine: values => values[0], +}); + export interface EditorSettings { tabSize: number; tabsToSpaces: boolean; + asmTabStops: AsmTabStops; showLineNumbers: boolean; highlightSpecialChars: boolean; highlightTrailingWhitespace: boolean; @@ -46,6 +52,7 @@ const SETTINGS_KEY = "8bitworkshop/editorSettings"; const defaultSettings: EditorSettings = { tabSize: DEFAULT_TAB_SIZE, tabsToSpaces: true, + asmTabStops: {}, showLineNumbers: !isMobileDevice, highlightSpecialChars: true, highlightTrailingWhitespace: true, @@ -73,7 +80,7 @@ export function saveAndApplySettings(settings: EditorSettings) { } const compartmentValues: [Compartment, (s: EditorSettings) => Extension][] = [ - [tabCompartment, s => tabExtension(s.tabSize, s.tabsToSpaces)], + [tabCompartment, s => tabExtension(s.tabSize, s.tabsToSpaces, s.asmTabStops)], [showLineNumbersCompartment, s => s.showLineNumbers ? lineNumbers() : []], [highlightSpecialCharsCompartment, s => s.highlightSpecialChars ? highlightSpecialChars() : []], [highlightTrailingWhitespaceCompartment, s => s.highlightTrailingWhitespace ? highlightTrailingWhitespace() : []], @@ -91,6 +98,9 @@ export function openSettings() { $('#setting_tabSize').val(s.tabSize); $('#setting_tabInsertsTabs').prop('checked', !s.tabsToSpaces); $('#setting_tabInsertsSpaces').prop('checked', s.tabsToSpaces); + $('#setting_asmOpcodes').val(s.asmTabStops.opcodes || ""); + $('#setting_asmOperands').val(s.asmTabStops.operands || ""); + $('#setting_asmComments').val(s.asmTabStops.comments || ""); $('#setting_showLineNumbers').prop('checked', s.showLineNumbers); $('#setting_highlightSpecialChars').prop('checked', s.highlightSpecialChars); $('#setting_highlightTrailingWhitespace').prop('checked', s.highlightTrailingWhitespace); @@ -114,6 +124,12 @@ export function openSettings() { +
+ + : + : + : +
@@ -144,6 +160,9 @@ export function openSettings() { callback: () => { settings.tabSize = Math.min(MAX_TAB_SIZE, Math.max(MIN_TAB_SIZE, parseInt($('#setting_tabSize').val() as string) || MIN_TAB_SIZE)); settings.tabsToSpaces = $('#setting_tabInsertsSpaces').is(':checked'); + settings.asmTabStops.opcodes = parseInt($('#setting_asmOpcodes').val() as string) || undefined; + settings.asmTabStops.operands = parseInt($('#setting_asmOperands').val() as string) || undefined; + settings.asmTabStops.comments = parseInt($('#setting_asmComments').val() as string) || undefined; settings.showLineNumbers = $('#setting_showLineNumbers').is(':checked'); settings.highlightSpecialChars = $('#setting_highlightSpecialChars').is(':checked'); settings.highlightTrailingWhitespace = $('#setting_highlightTrailingWhitespace').is(':checked'); diff --git a/src/ide/views/tabs.ts b/src/ide/views/tabs.ts index 7fed49c26..dc6b4015b 100644 --- a/src/ide/views/tabs.ts +++ b/src/ide/views/tabs.ts @@ -2,8 +2,9 @@ import { indentLess, insertTab } from "@codemirror/commands"; import { indentUnit } from "@codemirror/language"; import { EditorState, Extension } from "@codemirror/state"; import { keymap } from "@codemirror/view"; +import { tabStopsFacet } from "../settings"; -export function tabExtension(tabSize: number, tabsToSpaces: boolean): Extension { +export function tabExtension(tabSize: number, tabsToSpaces: boolean, asmTabStops: AsmTabStops): Extension { return [ EditorState.tabSize.of(tabSize), indentUnit.of(tabsToSpaces ? " ".repeat(tabSize) : "\t"), @@ -11,5 +12,6 @@ export function tabExtension(tabSize: number, tabsToSpaces: boolean): Extension { key: "Tab", run: insertTab }, { key: "Shift-Tab", run: indentLess } ]), + tabStopsFacet.of(asmTabStops), ]; } From 7ae13bf58a33814d0eceaf17ec159c926994eedf Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 17:53:08 -0700 Subject: [PATCH 26/40] Call detectAndApplyAsmTabStops from loadMainWindow --- src/ide/settings.ts | 14 +++++++++++++- src/ide/ui.ts | 6 ++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index f9de58f10..dd406ca76 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -1,7 +1,8 @@ import { closeBrackets, deleteBracketPair } from "@codemirror/autocomplete"; import { Compartment, Extension, Facet } from "@codemirror/state"; import { EditorView, highlightSpecialChars, highlightTrailingWhitespace, highlightWhitespace, keymap, lineNumbers } from "@codemirror/view"; -import { AsmTabStops } from "../common/tabdetect"; +import { AsmTabStops, detectTabStopsFromAsm } from "../common/tabdetect"; +import { getModeForPath, isAsmMode } from "./ui"; import { isMobileDevice } from "./views/baseviews"; import { debugHighlightTagsTooltip } from "./views/debug"; import { tabExtension } from "./views/tabs"; @@ -93,6 +94,17 @@ export function settingsExtensions(settings: EditorSettings): Extension[] { return compartmentValues.map(([c, fn]) => c.of(fn(settings))); } +export function detectTabStops(mode: string, tabSize: number, text: string) { + return isAsmMode(mode) ? detectTabStopsFromAsm(mode, tabSize, text) : {}; +} + +// Called from loadMainWindow to set initial tab stops. +export function detectAndApplyAsmTabStops(filename: string, text: string) { + const settings = loadSettings(); + settings.asmTabStops = detectTabStops(getModeForPath(filename), settings.tabSize, text); + saveAndApplySettings(settings); +} + export function openSettings() { function updateUI(s: EditorSettings) { $('#setting_tabSize').val(s.tabSize); diff --git a/src/ide/ui.ts b/src/ide/ui.ts index 338b87fe0..891406fab 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -13,16 +13,16 @@ import { FileData, WorkerError, WorkerResult } from "../common/workertypes"; import { importPlatform } from "../platform/_index"; import { gaEvent, gaPageView } from "./analytics"; import { alertError, alertInfo, fatalError, setWaitDialog } from "./dialogs"; -import { openSettings } from "./settings"; import { CodeProject, createNewPersistentStore, LocalForageFilesystem, OverlayFilesystem, ProjectFilesystem, WebPresetsFileSystem } from "./project"; import { getRepos, parseGithubURL } from "./services"; +import { detectAndApplyAsmTabStops, openSettings } from "./settings"; import { _downloadAllFilesZipFile, _downloadCassetteFile, _downloadProjectZipFile, _downloadROMImage, _downloadSourceFile, _downloadSymFile, _getCassetteFunction, _recordVideo, _shareEmbedLink } from "./shareexport"; import { _importProjectFromGithub, _loginToGithub, _logoutOfGithub, _publishProjectToGithub, _pullProjectFromGithub, _pushProjectToGithub, _removeRepository, importProjectFromGithub } from "./sync"; import { Toolbar } from "./toolbar"; import { AssetEditorView } from "./views/asseteditor"; import { isMobileDevice } from "./views/baseviews"; import { AddressHeatMapView, BinaryFileView, MemoryMapView, MemoryView, ProbeLogView, ProbeSymbolView, RasterStackMapView, ScanlineIOView, VRAMMemoryView } from "./views/debugviews"; -import { DisassemblerView, ListingView, PC_LINE_LOOKAHEAD, SourceEditor, setUppercaseOnly } from "./views/editors"; +import { DisassemblerView, ListingView, PC_LINE_LOOKAHEAD, setUppercaseOnly, SourceEditor } from "./views/editors"; import { CallStackView, DebugBrowserView } from "./views/treeviews"; import { ProjectWindows } from "./windows"; import Split = require('split.js'); @@ -435,6 +435,8 @@ async function loadMainWindow(preset_id: string) { var maindata = current_project.getFile(preset_id); if (typeof maindata === 'string') { await current_project.loadFileDependencies(maindata); + // Update column settings for asm {opcode, operand, comments}. + detectAndApplyAsmTabStops(preset_id, maindata); } // we need this to build create functions for the editor refreshWindowList(); From b827c760e73716ff3312f5422bab2321340798cc Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 18:06:46 -0700 Subject: [PATCH 27/40] Settings 'Reset' re-detects asm tab stops --- src/ide/settings.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index dd406ca76..801d3ffea 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -2,9 +2,10 @@ import { closeBrackets, deleteBracketPair } from "@codemirror/autocomplete"; import { Compartment, Extension, Facet } from "@codemirror/state"; import { EditorView, highlightSpecialChars, highlightTrailingWhitespace, highlightWhitespace, keymap, lineNumbers } from "@codemirror/view"; import { AsmTabStops, detectTabStopsFromAsm } from "../common/tabdetect"; -import { getModeForPath, isAsmMode } from "./ui"; +import { current_project, getModeForPath, isAsmMode, projectWindows } from "./ui"; import { isMobileDevice } from "./views/baseviews"; import { debugHighlightTagsTooltip } from "./views/debug"; +import { SourceEditor } from "./views/editors"; import { tabExtension } from "./views/tabs"; const MIN_TAB_SIZE = 1; @@ -106,6 +107,11 @@ export function detectAndApplyAsmTabStops(filename: string, text: string) { } export function openSettings() { + const view = projectWindows.id2window[current_project.mainPath]; + const editor = (view as SourceEditor).editor; + const text = editor.state.doc.toString(); + const mode = getModeForPath(current_project.mainPath); + function updateUI(s: EditorSettings) { $('#setting_tabSize').val(s.tabSize); $('#setting_tabInsertsTabs').prop('checked', !s.tabsToSpaces); @@ -158,6 +164,7 @@ export function openSettings() { className: "btn-default", callback: () => { settings = { ...defaultSettings }; + settings.asmTabStops = detectTabStopsFromAsm(mode, defaultSettings.tabSize, text); updateUI(settings); return false; } From 3206c30aba9d1a3b91f1feff119e184c06ae29d6 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 18:07:36 -0700 Subject: [PATCH 28/40] Auto focus and select tab size for easy editing --- src/ide/settings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index 801d3ffea..900e65de3 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -195,6 +195,7 @@ export function openSettings() { }); dialog.on('shown.bs.modal', () => { updateUI(settings); + $('#setting_tabSize').focus().select(); }); dialog.on('keydown', (e) => { if (e.key === 'Enter') { From 6c2a4d1a45e57809bbabdc81935e56eefacde42a Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 18:08:19 -0700 Subject: [PATCH 29/40] Changing tab size redetects asm tab stops Asm {operands, opcodes, comments} columns update in real time. Useful for files that contain tab chars. --- src/ide/settings.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index 900e65de3..60c113938 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -195,7 +195,14 @@ export function openSettings() { }); dialog.on('shown.bs.modal', () => { updateUI(settings); - $('#setting_tabSize').focus().select(); + $('#setting_tabSize').focus().select().on('input', () => { + settings.tabSize = parseInt($('#setting_tabSize').val() as string) || MIN_TAB_SIZE; + // Re-detect tab stops based on new tab size. + settings.asmTabStops = detectTabStops(mode, settings.tabSize, editor.state.doc.toString()); + $('#setting_asmOpcodes').val(settings.asmTabStops.opcodes || ""); + $('#setting_asmOperands').val(settings.asmTabStops.operands || ""); + $('#setting_asmComments').val(settings.asmTabStops.comments || ""); + }); }); dialog.on('keydown', (e) => { if (e.key === 'Enter') { From db8e3cde47825976c8ebb582ce78a03201a155f7 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 22:38:52 -0700 Subject: [PATCH 30/40] Tab/Shift-Tab indentTabStop/deindentTabStop --- src/ide/views/tabs.ts | 137 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/src/ide/views/tabs.ts b/src/ide/views/tabs.ts index dc6b4015b..54c24b5dd 100644 --- a/src/ide/views/tabs.ts +++ b/src/ide/views/tabs.ts @@ -1,16 +1,143 @@ -import { indentLess, insertTab } from "@codemirror/commands"; import { indentUnit } from "@codemirror/language"; -import { EditorState, Extension } from "@codemirror/state"; -import { keymap } from "@codemirror/view"; +import { EditorSelection, EditorState, Extension } from "@codemirror/state"; +import { EditorView, keymap } from "@codemirror/view"; +import { AsmTabStops, columnAt } from "../../common/tabdetect"; import { tabStopsFacet } from "../settings"; +export const MAX_COLS = 300; + +export function tabStopsToColumns(asmTabStops: AsmTabStops, tabSize: number, useTabs?: boolean): number[] { + if (!useTabs) { + const asmCols = [asmTabStops.opcodes, asmTabStops.operands, asmTabStops.comments].filter((n): n is number => n > 0); + if (asmCols.length > 0) return asmCols; + } + const cols: number[] = []; + for (let col = tabSize; col < MAX_COLS; col += tabSize) { + cols.push(col); + } + return cols; +} + +function nextTabStop(col: number, columns: number[]): number { + for (const stop of columns) { + if (stop > col) return stop; + } + return col; +} + +function prevTabStop(col: number, columns: number[]): number { + for (let i = columns.length - 1; i >= 0; i--) { + if (columns[i] < col) return columns[i]; + } + return 0; +} + +function indentTabStop(view: EditorView): boolean { + const useTabs = view.state.facet(indentUnit) === '\t'; + const asmTabStop = view.state.facet(tabStopsFacet); + const tabSize = view.state.facet(EditorState.tabSize); + const columns = tabStopsToColumns(asmTabStop, tabSize, useTabs); + + // Indents lines for selected ranges. + if (view.state.selection.ranges.some(r => !r.empty)) { + const changes: { from: number; to: number; insert: string }[] = []; + const seen = new Set(); + for (const range of view.state.selection.ranges) { + const fromLine = view.state.doc.lineAt(range.from).number; + const toLine = view.state.doc.lineAt(range.to).number; + for (let ln = fromLine; ln <= toLine; ln++) { + if (seen.has(ln)) continue; + seen.add(ln); + const line = view.state.doc.line(ln); + let wsEnd = 0, wsCol = 0; + for (; wsEnd < line.text.length; wsEnd++) { + if (line.text[wsEnd] === '\t') wsCol = wsCol + tabSize - (wsCol % tabSize); + else if (line.text[wsEnd] === ' ') wsCol++; + else break; + } + if (useTabs) { + const insertAt = line.from + wsEnd; + changes.push({ from: insertAt, to: insertAt, insert: '\t' }); + } else { + const targetCol = nextTabStop(wsCol, columns); + if (targetCol > wsCol) { + changes.push({ from: line.from, to: line.from + wsEnd, insert: ' '.repeat(targetCol) }); + } + } + } + } + if (changes.length > 0) { + view.dispatch({ changes }); + } + return true; + } + + // Otherwise, inserts whitespace to next tab stop. + view.dispatch(view.state.changeByRange(range => { + const line = view.state.doc.lineAt(range.head); + const col = columnAt(line.text, range.head - line.from, tabSize); + const insert = useTabs ? '\t' : ' '.repeat(nextTabStop(col, columns) - col); + return { + changes: { from: range.head, insert }, + range: EditorSelection.cursor(range.head + insert.length) + }; + })); + return true; +} + +function deindentTabStop(view: EditorView): boolean { + const useTabs = view.state.facet(indentUnit) === '\t'; + const asmTabStop = view.state.facet(tabStopsFacet); + const tabSize = view.state.facet(EditorState.tabSize); + const columns = tabStopsToColumns(asmTabStop, tabSize, useTabs); + + // Dedents lines for all ranges, regardless of selection. + const changes: { from: number; to: number; insert: string }[] = []; + const seen = new Set(); + for (const range of view.state.selection.ranges) { + const fromLine = view.state.doc.lineAt(range.from).number; + const toLine = view.state.doc.lineAt(range.to).number; + for (let ln = fromLine; ln <= toLine; ln++) { + if (seen.has(ln)) continue; + seen.add(ln); + const line = view.state.doc.line(ln); + let wsEnd = 0, wsCol = 0; + for (; wsEnd < line.text.length; wsEnd++) { + if (line.text[wsEnd] === '\t') wsCol = wsCol + tabSize - (wsCol % tabSize); + else if (line.text[wsEnd] === ' ') wsCol++; + else break; + } + if (wsCol === 0) continue; + const targetCol = prevTabStop(wsCol, columns); + // Walk forward to find the offset where we reach or pass targetCol. + let col = 0, prevCol = 0, off = 0; + for (; off < wsEnd && col < targetCol; off++) { + prevCol = col; + if (line.text[off] === '\t') col = col + tabSize - (col % tabSize); + else col++; + } + if (col > targetCol) { + // A tab overshot targetCol — replace it with spaces to land exactly on targetCol. + changes.push({ from: line.from + off - 1, to: line.from + wsEnd, insert: ' '.repeat(targetCol - prevCol) }); + } else { + // Landed exactly on targetCol — just delete the rest. + changes.push({ from: line.from + off, to: line.from + wsEnd, insert: '' }); + } + } + } + if (changes.length > 0) { + view.dispatch({ changes }); + } + return true; +} + export function tabExtension(tabSize: number, tabsToSpaces: boolean, asmTabStops: AsmTabStops): Extension { return [ EditorState.tabSize.of(tabSize), indentUnit.of(tabsToSpaces ? " ".repeat(tabSize) : "\t"), keymap.of([ - { key: "Tab", run: insertTab }, - { key: "Shift-Tab", run: indentLess } + { key: "Tab", run: indentTabStop }, + { key: "Shift-Tab", run: deindentTabStop } ]), tabStopsFacet.of(asmTabStops), ]; From bad389736f7b92861eb77acf44feef8704441909 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 18:40:11 -0700 Subject: [PATCH 31/40] Disable tab stop settings for non-asm files --- src/ide/settings.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ide/settings.ts b/src/ide/settings.ts index 60c113938..667e8a938 100644 --- a/src/ide/settings.ts +++ b/src/ide/settings.ts @@ -194,6 +194,10 @@ export function openSettings() { } }); dialog.on('shown.bs.modal', () => { + if (!isAsmMode(mode)) { + $('#setting_asmColumns').addClass('disabled'); + $('#setting_asmOpcodes, #setting_asmOperands, #setting_asmComments').prop('disabled', true); + } updateUI(settings); $('#setting_tabSize').focus().select().on('input', () => { settings.tabSize = parseInt($('#setting_tabSize').val() as string) || MIN_TAB_SIZE; From b31f07ff3c3c84cd510d719e8d3ae25bab2a9494 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 20:11:44 -0700 Subject: [PATCH 32/40] Add visual tab stop ruler - Visual ruler at the top of the editor shows tab stops. - When 'Tab key inserts spaces' is set, the ruler shows the {opcodes, operands, comments} virutal tab stops. - In all other cases, fixed tab positions are shown based on the currently configured tab size. - Clicking the visual ruler at the top of the editor is a shortcut to opening the settings dialog. --- src/ide/views/editors.ts | 2 ++ src/ide/views/tabstopruler.ts | 59 +++++++++++++++++++++++++++++++++++ src/themes/editorTheme.ts | 8 ++++- src/themes/mbo.ts | 8 +++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/ide/views/tabstopruler.ts diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 617eee71a..8dec33fbb 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -26,6 +26,7 @@ import { createAssetHeaderPlugin } from "./assetdecorations"; import { ProjectView } from "./baseviews"; import { createTextTransformFilterEffect, textTransformFilterCompartment } from "./filters"; import { breakpointMarkers, bytes, clock, currentPcMarker, errorMarkers, gutterLineInfo, offset, statusMarkers } from "./gutter"; +import { tabStopRuler } from "./tabstopruler"; import { currentPc, errorMessages, errorSpans, highlightLines, showValue } from "./visuals"; // look ahead this many bytes when finding source lines for a PC @@ -193,6 +194,7 @@ export class SourceEditor implements ProjectView { parser || [], theme, editorTheme, + tabStopRuler, lineWrap ? EditorView.lineWrapping : [], currentPc.field, diff --git a/src/ide/views/tabstopruler.ts b/src/ide/views/tabstopruler.ts new file mode 100644 index 000000000..5ef36b153 --- /dev/null +++ b/src/ide/views/tabstopruler.ts @@ -0,0 +1,59 @@ +import { indentUnit } from "@codemirror/language"; +import { EditorState } from "@codemirror/state"; +import { EditorView, Panel, showPanel } from "@codemirror/view"; +import { AsmTabStops, tabStopsEquals } from "../../common/tabdetect"; +import { openSettings, tabStopsFacet } from "../settings"; +import { MAX_COLS, tabStopsToColumns } from "../views/tabs"; + +function buildContent(asmTabStops: AsmTabStops, tabSize: number, useTabs: boolean): string { + const chars = new Array(MAX_COLS); + chars.fill(useTabs ? " " : " "); + for (const col of tabStopsToColumns(asmTabStops, tabSize, useTabs)) { + chars[col] = "▾"; + } + return chars.join(""); +} + +function rulerPanel(view: EditorView): Panel { + const dom = document.createElement("div"); + dom.className = "tab-stop-ruler"; + dom.setAttribute("aria-hidden", "true"); + let currentAsmTabStops: AsmTabStops = {}; + let currentTabSize = 0; + let currentIndent = ""; + + function rebuild() { + const asmTabStops = view.state.facet(tabStopsFacet); + const tabSize = view.state.facet(EditorState.tabSize); + const indent = view.state.facet(indentUnit); + if (tabStopsEquals(asmTabStops, currentAsmTabStops) && tabSize === currentTabSize && indent === currentIndent) return; + currentAsmTabStops = asmTabStops; + currentTabSize = tabSize; + currentIndent = indent; + dom.innerHTML = buildContent(asmTabStops, tabSize, indent === '\t'); + } + + function sync() { + const contentStyle = getComputedStyle(view.contentDOM); + dom.style.font = contentStyle.font; + const left = view.contentDOM.getBoundingClientRect().left - view.dom.getBoundingClientRect().left; + dom.style.marginLeft = left + "px"; + const line = view.contentDOM.querySelector(".cm-line"); + if (line) dom.style.paddingLeft = getComputedStyle(line).paddingLeft; + } + + rebuild(); + dom.addEventListener("click", openSettings); + sync(); + + return { + dom, + top: true, + update() { + rebuild(); + sync(); + } + }; +} + +export const tabStopRuler = showPanel.of(rulerPanel); diff --git a/src/themes/editorTheme.ts b/src/themes/editorTheme.ts index e42948589..5e351ef91 100644 --- a/src/themes/editorTheme.ts +++ b/src/themes/editorTheme.ts @@ -38,4 +38,10 @@ export const editorTheme = EditorView.theme({ "& .cm-lineNumbers .cm-gutterElement": { color: "#99cc99", }, -}); \ No newline at end of file + ".tab-stop-ruler": { + whiteSpace: "pre", + overflow: "hidden", + userSelect: "none", + cursor: "pointer", + } +}); diff --git a/src/themes/mbo.ts b/src/themes/mbo.ts index ad1d2a308..333304298 100644 --- a/src/themes/mbo.ts +++ b/src/themes/mbo.ts @@ -43,6 +43,14 @@ export const mboTheme = EditorView.theme({ backgroundImage: `url('data:image/svg+xml,')`, backgroundPosition: "left 50%" }, + ".cm-panels": { + backgroundColor: "#4e4e4e" /* color left of tab stop ruler */, + }, + ".tab-stop-ruler": { + color: "rgba(255,255,255,0.25)", + backgroundColor: "#202020", + borderBottom: "2px solid #4e4e4e", + }, }, { dark: true }); export const mboHighlightStyle = HighlightStyle.define([ From 3c020f857c129682ee9deb93324ab2a79c6671e6 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 20:13:51 -0700 Subject: [PATCH 33/40] Alt-Shift-f formats asm and C-like source files - Formats lines within selection range(s) - Formats entire doc if nothing is selected - Shift-Alt-F reformats the entire file, or just the line covered by the currently selected ranges(s): - C-like source is reindented by `cpp()` - asm source is formatted according to the current {opcodes, operands, comments} column settings - Affected lines have trailing whitespace removed - When formatting the entire file, Excess blank lines at the end of the file --- src/ide/format.ts | 134 +++++++++++++++++++++++++++++++++++++++ src/ide/views/editors.ts | 12 ++++ 2 files changed, 146 insertions(+) create mode 100644 src/ide/format.ts diff --git a/src/ide/format.ts b/src/ide/format.ts new file mode 100644 index 000000000..76e49ad97 --- /dev/null +++ b/src/ide/format.ts @@ -0,0 +1,134 @@ +import { indentRange, indentUnit } from "@codemirror/language"; +import { ChangeSet, EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { formatAsmLine } from "../common/format-asm"; +import { tabStopsFacet } from "./settings"; + +export function formatDocument(view: EditorView, isAsm: boolean) { + let state = view.state; + const changeSets: ChangeSet[] = []; + + function apply(cs: ChangeSet) { + changeSets.push(cs); + // Update state so subsequent passes have correct line positions. + state = state.update({ changes: cs }).state; + } + + function selectedLines(): Set { + const lines = new Set(); + for (const range of sel.ranges) { + const fromLine = state.doc.lineAt(range.from).number; + const toLine = state.doc.lineAt(range.to).number; + for (let i = fromLine; i <= toLine; i++) { + lines.add(i); + } + } + return lines; + } + + function isTargetLine(lineNum: number): boolean { + return targetLines === null || targetLines.has(lineNum); + } + + // Determine which lines to format based on selections. + const sel = state.selection; + const hasSelection = sel.ranges.some(r => !r.empty); + const targetLines = hasSelection ? selectedLines() : null; + + // If 'Tab key insert spaces' is set, convert tabs to spaces. + const indent = state.facet(indentUnit); + const tabSize = state.facet(EditorState.tabSize); + if (indent !== '\t') { + const specs: { from: number, to: number, insert: string }[] = []; + for (let i = 1; i <= state.doc.lines; i++) { + if (!isTargetLine(i)) continue; + const line = state.doc.line(i); + if (line.text.indexOf('\t') >= 0) { + let col = 0; + let newText = ""; + for (const char of line.text) { + if (char === '\t') { + const nextTab = col + tabSize - (col % tabSize); + newText += ' '.repeat(nextTab - col); + col = nextTab; + } else { + newText += char; + col++; + } + } + specs.push({ from: line.from, to: line.to, insert: newText }); + } + } + if (specs.length > 0) { + apply(state.changes(specs)); + } + } + + if (isAsm) { + // Format asm languages using custom tab stops. + const stops = state.facet(tabStopsFacet); + if (stops.opcodes > 0 || stops.operands > 0 || stops.comments > 0) { + const specs: { from: number, to: number, insert: string }[] = []; + for (let i = 1; i <= state.doc.lines; i++) { + if (!isTargetLine(i)) continue; + const line = state.doc.line(i); + const formatted = formatAsmLine(line.text, i, indent, tabSize, stops); + if (formatted !== line.text) { + specs.push({ from: line.from, to: line.to, insert: formatted }); + } + } + if (specs.length > 0) { + apply(state.changes(specs)); + } + } + } else { + // Format non-asm languages using CodeMirror's built-in indentation. + if (hasSelection) { + for (const range of sel.ranges) { + const fromLine = state.doc.lineAt(range.from); + const toLine = state.doc.lineAt(range.to); + const indentChanges = indentRange(state, fromLine.from, toLine.to); + if (!indentChanges.empty) { + apply(indentChanges); + } + } + } else { + const indentChanges = indentRange(state, 0, state.doc.length); + if (!indentChanges.empty) { + apply(indentChanges); + } + } + } + + // Strip trailing whitespace. + const trimSpecs: { from: number, to: number, insert: string }[] = []; + for (let i = 1; i <= state.doc.lines; i++) { + if (!isTargetLine(i)) continue; + const line = state.doc.line(i); + const trimmed = line.text.replace(/\s+$/, ""); + if (trimmed !== line.text) { + trimSpecs.push({ from: line.from + trimmed.length, to: line.to, insert: "" }); + } + } + if (trimSpecs.length > 0) { + apply(state.changes(trimSpecs)); + } + + // Remove excess blank lines at end of file when formatting entire file. + if (!hasSelection) { + let lastNonEmpty = state.doc.lines; + while (lastNonEmpty > 1 && state.doc.line(lastNonEmpty).text === '') { + lastNonEmpty--; + } + if (lastNonEmpty < state.doc.lines) { + const keepFrom = state.doc.line(lastNonEmpty + 1).to; + const deleteTo = state.doc.length; + apply(state.changes({ from: keepFrom, to: deleteTo })); + } + } + + // Compose and dispatch all changes; selections are mapped automatically. + if (changeSets.length > 0) { + view.dispatch({ changes: changeSets.reduce((a, b) => a.compose(b)) }); + } +} diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index 8dec33fbb..e83f06ea9 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -20,6 +20,7 @@ import { cobalt } from "../../themes/cobalt"; import { disassemblyTheme } from "../../themes/disassemblyTheme"; import { editorTheme } from "../../themes/editorTheme"; import { mbo } from "../../themes/mbo"; +import { formatDocument } from "../format"; import { loadSettings, registerEditor, settingsExtensions } from "../settings"; import { clearBreakpoint, current_project, isAsmMode, lastDebugState, platform, runToPC } from "../ui"; import { createAssetHeaderPlugin } from "./assetdecorations"; @@ -135,6 +136,17 @@ export class SourceEditor implements ProjectView { doc: text, extensions: [ + // Use domEventHandler instead of keymap.of with "Shift-Alt-f" + // to prevent macOS intercepting and inserting `Ï`. + EditorView.domEventHandlers({ + keydown(event, view) { + if (event.shiftKey && event.altKey && event.code === 'KeyF') { + event.preventDefault(); + formatDocument(view, isAsm); + } + } + }), + isAsm ? keymap.of([{ key: "Enter", run: (view) => { From 97f692379e9a8baebde373e77f1d8226459f3f01 Mon Sep 17 00:00:00 2001 From: Fred Sauer Date: Sun, 5 Apr 2026 13:24:21 -0700 Subject: [PATCH 34/40] Keyboard shortcuts help menu Document built-in keyboard shortcuts --- css/ui.css | 13 +++++++++++++ index.html | 1 + src/ide/ui.ts | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/css/ui.css b/css/ui.css index ebe750f54..1f1d14d77 100644 --- a/css/ui.css +++ b/css/ui.css @@ -1,6 +1,19 @@ .dbg_info { font-size: 0.8em; } +.help th { + padding-top: 8px; +} +.help td { + vertical-align: top; + padding: 0 6px; +} +.help td:first-child { + text-align: right; + padding-right: 12px; + padding-bottom: 2px; + min-width: 160px; +} .tooltipbox { position: relative; display: inline-block; diff --git a/index.html b/index.html index fd751263d..58a57be5a 100644 --- a/index.html +++ b/index.html @@ -117,6 +117,7 @@ Help