From 52a110711495056d369ae515e8d48841a0e0eb18 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sun, 9 Aug 2020 20:40:17 -0500 Subject: [PATCH] basic: fixed transcript bottom, moved teletype to file, added label field to errors --- css/ui.css | 7 + src/common/basic/compiler.ts | 22 +-- src/common/basic/run.ts | 2 +- src/common/teletype.ts | 251 ++++++++++++++++++++++++++++ src/common/workertypes.ts | 5 +- src/ide/ui.ts | 6 +- src/platform/basic.ts | 314 ++++------------------------------- 7 files changed, 309 insertions(+), 298 deletions(-) create mode 100644 src/common/teletype.ts diff --git a/css/ui.css b/css/ui.css index 6d985967..30d52329 100644 --- a/css/ui.css +++ b/css/ui.css @@ -168,6 +168,9 @@ div.has-errors { div.is-busy-unused { background-color: #8888bb !important; } +#error_alert { + max-width: 45%; +} #error_alert_msg { margin-right: 2em; } @@ -585,6 +588,10 @@ div.asset_toolbar { user-select: text; font-family: Verdana, Geneva, sans-serif; } +.transcript-bottom { + background-color: #fff; + width: 100%; +} .transcript-split { padding: 0.5em; background: #eee; diff --git a/src/common/basic/compiler.ts b/src/common/basic/compiler.ts index cf107bae..e9ab587c 100644 --- a/src/common/basic/compiler.ts +++ b/src/common/basic/compiler.ts @@ -31,7 +31,7 @@ export enum TokenType { export type ExprTypes = BinOp | UnOp | IndOp | Literal; -export type Expr = ExprTypes & SourceLocated; +export type Expr = ExprTypes; // & SourceLocated; export type Opcode = string; @@ -52,7 +52,7 @@ export interface UnOp { expr: Expr; } -export interface IndOp extends SourceLocated { +export interface IndOp { name: string; args: Expr[]; } @@ -282,7 +282,7 @@ export class BASICParser { compileError(msg: string, loc?: SourceLocation) { if (!loc) loc = this.peekToken().$loc; // TODO: pass SourceLocation to errors - this.errors.push({path:loc.path, line:loc.line, msg:msg}); + this.errors.push({path:loc.path, line:loc.line, label:loc.label, msg:msg}); throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too? } dialectError(what: string, loc?: SourceLocation) { @@ -371,13 +371,13 @@ export class BASICParser { this.tokens.push({ str: s, type: i, - $loc: { path: this.path, line: this.lineno, start: m.index, end: m.index+s.length } + $loc: { path: this.path, line: this.lineno, start: m.index, end: m.index+s.length, label: this.curlabel } }); break; } } } - this.eol = { type: TokenType.EOL, str: "", $loc: { path: this.path, line: this.lineno, start: line.length } }; + this.eol = { type: TokenType.EOL, str: "", $loc: { path: this.path, line: this.lineno, start: line.length, label: this.curlabel } }; } parse() : BASICLine { var line = {label: null, stmts: []}; @@ -386,8 +386,8 @@ export class BASICParser { this.parseOptLabel(line); if (this.tokens.length) { line.stmts = this.parseCompoundStatement(); - this.curlabel = null; } + this.curlabel = null; } return line; } @@ -446,7 +446,7 @@ export class BASICParser { this.compileError(`There should be a command here.`); return null; } - if (stmt) stmt.$loc = { path: cmdtok.$loc.path, line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start }; + if (stmt) stmt.$loc = { path: cmdtok.$loc.path, line: cmdtok.$loc.line, start: cmdtok.$loc.start, end: this.peekToken().$loc.start, label: this.curlabel }; return stmt; } parseVarSubscriptOrFunc(): IndOp { @@ -460,7 +460,7 @@ export class BASICParser { args = this.parseExprList(); this.expectToken(')', `There should be another expression or a ")" here.`); } - return { name: tok.str, args: args, $loc: tok.$loc }; + return { name: tok.str, args: args }; default: this.compileError(`There should be a variable name here.`); break; @@ -513,9 +513,9 @@ export class BASICParser { case TokenType.Int: case TokenType.Float1: case TokenType.Float2: - return { value: this.parseNumber(tok.str), $loc: tok.$loc }; + return { value: this.parseNumber(tok.str)/*, $loc: tok.$loc*/ }; case TokenType.String: - return { value: stripQuotes(tok.str), $loc: tok.$loc }; + return { value: stripQuotes(tok.str)/*, $loc: tok.$loc*/ }; case TokenType.Ident: if (tok.str == 'NOT') { let expr = this.parsePrimary(); @@ -682,7 +682,7 @@ export class BASICParser { this.pushbackToken(prompt); promptstr = ""; } - return { command:'INPUT', prompt:{ value: promptstr, $loc: prompt.$loc }, args:this.parseLexprList() }; + return { command:'INPUT', prompt:{ value: promptstr }, args:this.parseLexprList() }; } stmt__DATA() : DATA_Statement { return { command:'DATA', datums:this.parseExprList() }; diff --git a/src/common/basic/run.ts b/src/common/basic/run.ts index fbc8e46a..042f4893 100644 --- a/src/common/basic/run.ts +++ b/src/common/basic/run.ts @@ -62,7 +62,7 @@ runtime.resume = function() { rl.close(); } } catch (e) { - console.log("### " + e.message); + console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`); process.exit(1); } }); diff --git a/src/common/teletype.ts b/src/common/teletype.ts new file mode 100644 index 00000000..eda435f5 --- /dev/null +++ b/src/common/teletype.ts @@ -0,0 +1,251 @@ + +export class TeleType { + page: HTMLElement; + fixed: boolean; + scrolldiv: HTMLElement; + + curline: HTMLElement; + curstyle: number; + reverse: boolean; + col: number; + row: number; + lines: HTMLElement[]; + ncharsout : number; + + constructor(page: HTMLElement, fixed: boolean) { + this.page = page; + this.fixed = fixed; + this.clear(); + } + clear() { + this.curline = null; + this.curstyle = 0; + this.reverse = false; + this.col = 0; + this.row = -1; + this.lines = []; + this.ncharsout = 0; + $(this.page).empty(); + this.showPrintHead(true); + } + ensureline() { + if (this.curline == null) { + this.curline = this.lines[++this.row]; + if (this.curline == null) { + this.curline = $('
')[0]; + this.page.appendChild(this.curline); + this.lines[this.row] = this.curline; + this.scrollToBottom(); + } + } + } + flushline() { + this.curline = null; + } + // TODO: support fixed-width window (use CSS grid?) + addtext(line: string, style: number) { + this.ensureline(); + if (line.length) { + // in fixed mode, only do characters + if (this.fixed && line.length > 1) { + for (var i = 0; i < line.length; i++) + this.addtext(line[i], style); + return; + } + var span = $("").text(line); + for (var i = 0; i < 8; i++) { + if (style & (1 << i)) + span.addClass("transcript-style-" + (1 << i)); + } + if (this.reverse) span.addClass("transcript-reverse"); + //span.data('vmip', this.vm.pc); + // in fixed mode, we can overwrite individual characters + if (this.fixed && line.length == 1 && this.col < this.curline.childNodes.length) { + this.curline.replaceChild(span[0], this.curline.childNodes[this.col]); + } else { + span.appendTo(this.curline); + } + this.col += line.length; + // TODO: wrap @ 80 columns + this.ncharsout += line.length; + this.movePrintHead(true); + } + } + newline() { + this.flushline(); + this.col = 0; + this.movePrintHead(false); + } + // TODO: bug in interpreter where it tracks cursor position but maybe doesn't do newlines? + print(val: string) { + // split by newlines + var lines = val.split("\n"); + for (var i = 0; i < lines.length; i++) { + if (i > 0) this.newline(); + this.addtext(lines[i], this.curstyle); + } + } + move_cursor(col: number, row: number) { + if (!this.fixed) return; // fixed windows only + // ensure enough row elements + while (this.lines.length <= row) { + this.flushline(); + this.ensureline(); + } + // select row element + this.curline = this.lines[row]; + this.row = row; + // get children in row (individual text cells) + var children = $(this.curline).children(); + // add whitespace to line? + if (children.length > col) { + this.col = col; + } else { + while (this.col < col) + this.addtext(' ', this.curstyle); + } + } + setrows(size: number) { + if (!this.fixed) return; // fixed windows only + // truncate rows? + var allrows = $(this.page).children(); + if (allrows.length > size) { + this.flushline(); + allrows.slice(size).remove(); + this.lines = this.lines.slice(0, size); + //this.move_cursor(0,0); + } + } + formfeed() { + this.newline(); + } + scrollToBottom() { + this.curline.scrollIntoView(); + } + movePrintHead(printing: boolean) { + /* + var ph = $("#printhead"); // TODO: speed? + var x = $(this.page).position().left + this.col * ($(this.page).width() / 80) - 200; + ph.stop().animate({left: x}, {duration:20}); + //ph.offset({left: x}); + if (printing) ph.addClass("printing"); + else ph.removeClass("printing"); + */ + } + showPrintHead(show: boolean) { + /* + var ph = $("#printhead"); // TODO: speed? + if (show) ph.show(); else ph.hide(); + */ + } +} + +export class TeleTypeWithKeyboard extends TeleType { + input : HTMLInputElement; + keepinput : boolean = true; + + focused : boolean = true; + scrolling : number = 0; + waitingfor : string; + resolveInput; + uppercaseOnly : boolean; + + constructor(page: HTMLElement, fixed: boolean, input: HTMLInputElement) { + super(page, fixed); + this.input = input; + this.input.onkeypress = (e) => { + this.sendkey(e); + }; + this.input.onfocus = (e) => { + this.focused = true; + console.log('inputline gained focus'); + }; + $("#workspace").on('click', (e) => { + this.focused = false; + console.log('inputline lost focus'); + }); + this.page.onclick = (e) => { + this.input.focus(); + }; + this.hideinput(); + } + clear() { + super.clear(); + this.hideinput(); + } + focusinput() { + this.ensureline(); + this.showPrintHead(false); + // don't steal focus while editing + if (this.keepinput) + $(this.input).css('visibility', 'visible'); + else + $(this.input).appendTo(this.curline).show()[0]; + this.scrollToBottom(); + if (this.focused) { + $(this.input).focus(); + } + // change size + if (this.waitingfor == 'char') + $(this.input).addClass('transcript-input-char') + else + $(this.input).removeClass('transcript-input-char') + } + hideinput() { + this.showPrintHead(true); + if (this.keepinput) + $(this.input).css('visibility','hidden'); + else + $(this.input).appendTo($(this.page).parent()).hide(); + } + clearinput() { + this.input.value = ''; + this.waitingfor = null; + } + sendkey(e: KeyboardEvent) { + if (this.waitingfor == 'line') { + if (e.key == "Enter") { + this.sendinput(this.input.value.toString()); + } + } else if (this.waitingfor == 'char') { + this.sendchar(e.keyCode); + e.preventDefault(); + } + } + sendinput(s: string) { + if (this.resolveInput) { + if (this.uppercaseOnly) + s = s.toUpperCase(); + this.addtext(s, 4); + this.flushline(); + this.resolveInput(s.split(',')); // TODO: should parse quotes, etc + this.resolveInput = null; + } + this.clearinput(); + this.hideinput(); // keep from losing input handlers + } + sendchar(code: number) { + this.sendinput(String.fromCharCode(code)); + } + ensureline() { + if (!this.keepinput) $(this.input).hide(); + super.ensureline(); + } + scrollToBottom() { + // TODO: fails when lots of lines are scrolled + if (this.scrolldiv) { + this.scrolling++; + var top = $(this.page).height() + $(this.input).height(); + $(this.scrolldiv).stop().animate({scrollTop: top}, 200, 'swing', () => { + this.scrolling = 0; + this.ncharsout = 0; + }); + } else { + this.input.scrollIntoView(); + } + } + isBusy() { + // stop execution when scrolling and printing non-newlines + return this.scrolling > 0 && this.ncharsout > 0; + } +} diff --git a/src/common/workertypes.ts b/src/common/workertypes.ts index e635b4e3..cd29e23f 100644 --- a/src/common/workertypes.ts +++ b/src/common/workertypes.ts @@ -77,11 +77,8 @@ export interface WorkerMessage extends WorkerBuildStep { buildsteps:WorkerBuildStep[] } -export interface WorkerError { - line:number, +export interface WorkerError extends SourceLocation { msg:string, - path?:string - //TODO } export interface CodeListing { diff --git a/src/ide/ui.ts b/src/ide/ui.ts index a2eb32d6..2e7d39e1 100644 --- a/src/ide/ui.ts +++ b/src/ide/ui.ts @@ -1069,7 +1069,7 @@ async function updateSelector() { function getErrorElement(err : WorkerError) { var span = $('

'); if (err.path != null) { - var s = err.line ? `(${err.path}:${err.line})` : `(${err.path})` + var s = err.line ? err.label ? `(${err.path} @ ${err.label})` : `(${err.path}:${err.line})` : `(${err.path})` var link = $('').text(s); var path = err.path; // TODO: hack because examples/foo.a only gets listed as foo.a @@ -1887,7 +1887,9 @@ function globalErrorHandler(msgevent) { var err = msgevent.error; var werr : WorkerError = {msg:msg, line:0}; if (err instanceof EmuHalt && err.$loc) { - werr = {msg:msg, path:err.$loc.path, line:err.$loc.line}; // TODO: get start/end columns + werr = Object.create(err.$loc); + werr.msg = msg; + console.log(werr); } showErrorAlert([werr]); } diff --git a/src/platform/basic.ts b/src/platform/basic.ts index d1f6a182..e450fe48 100644 --- a/src/platform/basic.ts +++ b/src/platform/basic.ts @@ -4,309 +4,59 @@ import { PLATFORMS, AnimationTimer, EmuHalt } from "../common/emu"; import { loadScript } from "../ide/ui"; import { BASICRuntime } from "../common/basic/runtime"; import { BASICProgram } from "../common/basic/compiler"; +import { TeleTypeWithKeyboard } from "../common/teletype"; const BASIC_PRESETS = [ { id: 'hello.bas', name: 'Hello World' } ]; -class TeleType { - page: HTMLElement; - fixed: boolean; - scrolldiv: HTMLElement; - - curline: HTMLElement; - curstyle: number; - reverse: boolean; - col: number; - row: number; - lines: HTMLElement[]; - ncharsout : number; - - constructor(page: HTMLElement, fixed: boolean) { - this.page = page; - this.fixed = fixed; - this.clear(); - } - clear() { - this.curline = null; - this.curstyle = 0; - this.reverse = false; - this.col = 0; - this.row = -1; - this.lines = []; - this.ncharsout = 0; - $(this.page).empty(); - this.showPrintHead(true); - } - ensureline() { - if (this.curline == null) { - this.curline = this.lines[++this.row]; - if (this.curline == null) { - this.curline = $('

')[0]; - this.page.appendChild(this.curline); - this.lines[this.row] = this.curline; - this.scrollToBottom(); - } - } - } - flushline() { - this.curline = null; - } - // TODO: support fixed-width window (use CSS grid?) - addtext(line: string, style: number) { - this.ensureline(); - if (line.length) { - // in fixed mode, only do characters - if (this.fixed && line.length > 1) { - for (var i = 0; i < line.length; i++) - this.addtext(line[i], style); - return; - } - var span = $("").text(line); - for (var i = 0; i < 8; i++) { - if (style & (1 << i)) - span.addClass("transcript-style-" + (1 << i)); - } - if (this.reverse) span.addClass("transcript-reverse"); - //span.data('vmip', this.vm.pc); - // in fixed mode, we can overwrite individual characters - if (this.fixed && line.length == 1 && this.col < this.curline.childNodes.length) { - this.curline.replaceChild(span[0], this.curline.childNodes[this.col]); - } else { - span.appendTo(this.curline); - } - this.col += line.length; - // TODO: wrap @ 80 columns - this.ncharsout += line.length; - this.movePrintHead(true); - } - } - newline() { - this.flushline(); - this.col = 0; - this.movePrintHead(false); - } - // TODO: bug in interpreter where it tracks cursor position but maybe doesn't do newlines? - print(val: string) { - // split by newlines - var lines = val.split("\n"); - for (var i = 0; i < lines.length; i++) { - if (i > 0) this.newline(); - this.addtext(lines[i], this.curstyle); - } - } - move_cursor(col: number, row: number) { - if (!this.fixed) return; // fixed windows only - // ensure enough row elements - while (this.lines.length <= row) { - this.flushline(); - this.ensureline(); - } - // select row element - this.curline = this.lines[row]; - this.row = row; - // get children in row (individual text cells) - var children = $(this.curline).children(); - // add whitespace to line? - if (children.length > col) { - this.col = col; - } else { - while (this.col < col) - this.addtext(' ', this.curstyle); - } - } - setrows(size: number) { - if (!this.fixed) return; // fixed windows only - // truncate rows? - var allrows = $(this.page).children(); - if (allrows.length > size) { - this.flushline(); - allrows.slice(size).remove(); - this.lines = this.lines.slice(0, size); - //this.move_cursor(0,0); - } - } - formfeed() { - this.newline(); - } - scrollToBottom() { - this.curline.scrollIntoView(); - } - movePrintHead(printing: boolean) { - /* - var ph = $("#printhead"); // TODO: speed? - var x = $(this.page).position().left + this.col * ($(this.page).width() / 80) - 200; - ph.stop().animate({left: x}, {duration:20}); - //ph.offset({left: x}); - if (printing) ph.addClass("printing"); - else ph.removeClass("printing"); - */ - } - showPrintHead(show: boolean) { - /* - var ph = $("#printhead"); // TODO: speed? - if (show) ph.show(); else ph.hide(); - */ - } -} - -class TeleTypeWithKeyboard extends TeleType { - input : HTMLInputElement; - runtime : BASICRuntime; - platform : BASICPlatform; - keepinput : boolean = true; - - focused : boolean = true; - scrolling : number = 0; - waitingfor : string; - resolveInput; - - constructor(page: HTMLElement, fixed: boolean, input: HTMLInputElement, platform: BASICPlatform) { - super(page, fixed); - this.input = input; - this.platform = platform; - this.runtime = platform.runtime; - this.runtime.input = async (prompt:string, nargs:number) => { - return new Promise( (resolve, reject) => { - if (prompt != null) { - this.addtext(prompt, 0); - this.addtext('? ', 0); - this.waitingfor = 'line'; - } else { - this.waitingfor = 'char'; - } - this.focusinput(); - this.resolveInput = resolve; - }); - } - this.input.onkeypress = (e) => { - this.sendkey(e); - }; - this.input.onfocus = (e) => { - this.focused = true; - console.log('inputline gained focus'); - }; - $("#workspace").on('click', (e) => { - this.focused = false; - console.log('inputline lost focus'); - }); - this.page.onclick = (e) => { - this.input.focus(); - }; - this.hideinput(); - } - clear() { - super.clear(); - this.hideinput(); - } - focusinput() { - this.ensureline(); - this.showPrintHead(false); - // don't steal focus while editing - if (this.keepinput) - $(this.input).css('visibility', 'visible'); - else - $(this.input).appendTo(this.curline).show()[0]; - this.scrollToBottom(); - if (this.focused) { - $(this.input).focus(); - } - // change size - if (this.waitingfor == 'char') - $(this.input).addClass('transcript-input-char') - else - $(this.input).removeClass('transcript-input-char') - } - hideinput() { - this.showPrintHead(true); - if (this.keepinput) - $(this.input).css('visibility','hidden'); - else - $(this.input).appendTo($(this.page).parent()).hide(); - } - clearinput() { - this.input.value = ''; - this.waitingfor = null; - } - sendkey(e: KeyboardEvent) { - if (this.waitingfor == 'line') { - if (e.key == "Enter") { - this.sendinput(this.input.value.toString()); - } - } else if (this.waitingfor == 'char') { - this.sendchar(e.keyCode); - e.preventDefault(); - } - } - sendinput(s: string) { - if (this.resolveInput) { - if (this.platform.program.opts.uppercaseOnly) - s = s.toUpperCase(); - this.addtext(s, 4); - this.flushline(); - this.resolveInput(s.split(',')); // TODO: should parse quotes, etc - this.resolveInput = null; - } - this.clearinput(); - this.hideinput(); // keep from losing input handlers - } - sendchar(code: number) { - this.sendinput(String.fromCharCode(code)); - } - ensureline() { - if (!this.keepinput) $(this.input).hide(); - super.ensureline(); - } - scrollToBottom() { - // TODO: fails when lots of lines are scrolled - if (this.scrolldiv) { - this.scrolling++; - $(this.scrolldiv).stop().animate({scrollTop: $(this.page).height()}, 200, 'swing', () => { - this.scrolling = 0; - this.ncharsout = 0; - }); - } else { - this.input.scrollIntoView(); - } - } - isBusy() { - // stop execution when scrolling and printing non-newlines - return this.scrolling > 0 && this.ncharsout > 0; - } -} - class BASICPlatform implements Platform { mainElement: HTMLElement; program: BASICProgram; runtime: BASICRuntime; timer: AnimationTimer; tty: TeleTypeWithKeyboard; - ips: number = 500; + ips: number = 1000; clock: number = 0; hotReload: boolean = false; + debugstop: boolean = false; // TODO: should be higher-level support constructor(mainElement: HTMLElement) { //super(); this.mainElement = mainElement; mainElement.style.overflowY = 'auto'; - mainElement.style.backgroundColor = 'white'; } async start() { await loadScript('./gen/common/basic/runtime.js'); + await loadScript('./gen/common/teletype.js'); // create runtime this.runtime = new BASICRuntime(); this.runtime.reset(); // create divs var parent = this.mainElement; // TODO: input line should be always flush left - var gameport = $('
').appendTo(parent); + var gameport = $('
').appendTo(parent); var windowport = $('
').appendTo(gameport); - var inputline = $('').appendTo(parent); + var inputport = $('
').appendTo(gameport); + var inputline = $('').appendTo(inputport); //var printhead = $('
').appendTo(parent); //var printshield = $('
').appendTo(parent); - this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement, this); + this.tty = new TeleTypeWithKeyboard(windowport[0], true, inputline[0] as HTMLInputElement); this.tty.scrolldiv = parent; + this.runtime.input = async (prompt:string, nargs:number) => { + return new Promise( (resolve, reject) => { + if (prompt != null) { + this.tty.addtext(prompt, 0); + this.tty.addtext('? ', 0); + this.tty.waitingfor = 'line'; + } else { + this.tty.waitingfor = 'char'; + } + this.tty.focusinput(); + this.tty.resolveInput = resolve; + }); + } this.timer = new AnimationTimer(60, this.animate.bind(this)); this.resize = () => { // set font size proportional to window width @@ -361,6 +111,7 @@ class BASICPlatform implements Platform { var didExit = this.runtime.exited; this.program = data; this.runtime.load(data); + this.tty.uppercaseOnly = this.program.opts.uppercaseOnly; // only reset if we exited, otherwise we try to resume if (!this.hotReload || didExit) this.reset(); } @@ -370,13 +121,12 @@ class BASICPlatform implements Platform { } reset(): void { - var didExit = this.runtime.exited; this.tty.clear(); this.runtime.reset(); - // restart program if it's finished, otherwise reset and hold - if (didExit) { + if (this.debugstop) + this.break(); + else this.resume(); - } } pause(): void { @@ -385,6 +135,7 @@ class BASICPlatform implements Platform { resume(): void { this.clock = 0; + this.debugstop = false; this.timer.start(); } @@ -436,14 +187,17 @@ class BASICPlatform implements Platform { this.onBreakpointHit = null; } step() { - this.pause(); - this.advance(); - this.break(); + if (this.tty.waitingfor == null) { + this.pause(); + this.advance(); + this.break(); + } } break() { - // TODO: don't highlight text in editor + // TODO: why doesn't highlight go away on resume? if (this.onBreakpointHit) { - //TODO: this.onBreakpointHit(this.saveState()); + this.onBreakpointHit(this.saveState()); + this.debugstop = true; } } }