mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-06-06 09:29:33 +00:00
basic: fixed transcript bottom, moved teletype to file, added label field to errors
This commit is contained in:
parent
8b7b581217
commit
52a1107114
|
@ -168,6 +168,9 @@ div.has-errors {
|
||||||
div.is-busy-unused {
|
div.is-busy-unused {
|
||||||
background-color: #8888bb !important;
|
background-color: #8888bb !important;
|
||||||
}
|
}
|
||||||
|
#error_alert {
|
||||||
|
max-width: 45%;
|
||||||
|
}
|
||||||
#error_alert_msg {
|
#error_alert_msg {
|
||||||
margin-right: 2em;
|
margin-right: 2em;
|
||||||
}
|
}
|
||||||
|
@ -585,6 +588,10 @@ div.asset_toolbar {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
font-family: Verdana, Geneva, sans-serif;
|
font-family: Verdana, Geneva, sans-serif;
|
||||||
}
|
}
|
||||||
|
.transcript-bottom {
|
||||||
|
background-color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.transcript-split {
|
.transcript-split {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
background: #eee;
|
background: #eee;
|
||||||
|
|
|
@ -31,7 +31,7 @@ export enum TokenType {
|
||||||
|
|
||||||
export type ExprTypes = BinOp | UnOp | IndOp | Literal;
|
export type ExprTypes = BinOp | UnOp | IndOp | Literal;
|
||||||
|
|
||||||
export type Expr = ExprTypes & SourceLocated;
|
export type Expr = ExprTypes; // & SourceLocated;
|
||||||
|
|
||||||
export type Opcode = string;
|
export type Opcode = string;
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ export interface UnOp {
|
||||||
expr: Expr;
|
expr: Expr;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IndOp extends SourceLocated {
|
export interface IndOp {
|
||||||
name: string;
|
name: string;
|
||||||
args: Expr[];
|
args: Expr[];
|
||||||
}
|
}
|
||||||
|
@ -282,7 +282,7 @@ export class BASICParser {
|
||||||
compileError(msg: string, loc?: SourceLocation) {
|
compileError(msg: string, loc?: SourceLocation) {
|
||||||
if (!loc) loc = this.peekToken().$loc;
|
if (!loc) loc = this.peekToken().$loc;
|
||||||
// TODO: pass SourceLocation to errors
|
// 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?
|
throw new CompileError(`${msg} (line ${loc.line})`); // TODO: label too?
|
||||||
}
|
}
|
||||||
dialectError(what: string, loc?: SourceLocation) {
|
dialectError(what: string, loc?: SourceLocation) {
|
||||||
|
@ -371,13 +371,13 @@ export class BASICParser {
|
||||||
this.tokens.push({
|
this.tokens.push({
|
||||||
str: s,
|
str: s,
|
||||||
type: i,
|
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;
|
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 {
|
parse() : BASICLine {
|
||||||
var line = {label: null, stmts: []};
|
var line = {label: null, stmts: []};
|
||||||
|
@ -386,8 +386,8 @@ export class BASICParser {
|
||||||
this.parseOptLabel(line);
|
this.parseOptLabel(line);
|
||||||
if (this.tokens.length) {
|
if (this.tokens.length) {
|
||||||
line.stmts = this.parseCompoundStatement();
|
line.stmts = this.parseCompoundStatement();
|
||||||
this.curlabel = null;
|
|
||||||
}
|
}
|
||||||
|
this.curlabel = null;
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
@ -446,7 +446,7 @@ export class BASICParser {
|
||||||
this.compileError(`There should be a command here.`);
|
this.compileError(`There should be a command here.`);
|
||||||
return null;
|
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;
|
return stmt;
|
||||||
}
|
}
|
||||||
parseVarSubscriptOrFunc(): IndOp {
|
parseVarSubscriptOrFunc(): IndOp {
|
||||||
|
@ -460,7 +460,7 @@ export class BASICParser {
|
||||||
args = this.parseExprList();
|
args = this.parseExprList();
|
||||||
this.expectToken(')', `There should be another expression or a ")" here.`);
|
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:
|
default:
|
||||||
this.compileError(`There should be a variable name here.`);
|
this.compileError(`There should be a variable name here.`);
|
||||||
break;
|
break;
|
||||||
|
@ -513,9 +513,9 @@ export class BASICParser {
|
||||||
case TokenType.Int:
|
case TokenType.Int:
|
||||||
case TokenType.Float1:
|
case TokenType.Float1:
|
||||||
case TokenType.Float2:
|
case TokenType.Float2:
|
||||||
return { value: this.parseNumber(tok.str), $loc: tok.$loc };
|
return { value: this.parseNumber(tok.str)/*, $loc: tok.$loc*/ };
|
||||||
case TokenType.String:
|
case TokenType.String:
|
||||||
return { value: stripQuotes(tok.str), $loc: tok.$loc };
|
return { value: stripQuotes(tok.str)/*, $loc: tok.$loc*/ };
|
||||||
case TokenType.Ident:
|
case TokenType.Ident:
|
||||||
if (tok.str == 'NOT') {
|
if (tok.str == 'NOT') {
|
||||||
let expr = this.parsePrimary();
|
let expr = this.parsePrimary();
|
||||||
|
@ -682,7 +682,7 @@ export class BASICParser {
|
||||||
this.pushbackToken(prompt);
|
this.pushbackToken(prompt);
|
||||||
promptstr = "";
|
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 {
|
stmt__DATA() : DATA_Statement {
|
||||||
return { command:'DATA', datums:this.parseExprList() };
|
return { command:'DATA', datums:this.parseExprList() };
|
||||||
|
|
|
@ -62,7 +62,7 @@ runtime.resume = function() {
|
||||||
rl.close();
|
rl.close();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("### " + e.message);
|
console.log(`### ${e.message} (line ${runtime.getCurrentSourceLocation().label})`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
251
src/common/teletype.ts
Normal file
251
src/common/teletype.ts
Normal file
|
@ -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 = $('<div class="transcript-line"/>')[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 = $("<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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,11 +77,8 @@ export interface WorkerMessage extends WorkerBuildStep {
|
||||||
buildsteps:WorkerBuildStep[]
|
buildsteps:WorkerBuildStep[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkerError {
|
export interface WorkerError extends SourceLocation {
|
||||||
line:number,
|
|
||||||
msg:string,
|
msg:string,
|
||||||
path?:string
|
|
||||||
//TODO
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CodeListing {
|
export interface CodeListing {
|
||||||
|
|
|
@ -1069,7 +1069,7 @@ async function updateSelector() {
|
||||||
function getErrorElement(err : WorkerError) {
|
function getErrorElement(err : WorkerError) {
|
||||||
var span = $('<p/>');
|
var span = $('<p/>');
|
||||||
if (err.path != null) {
|
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 = $('<a/>').text(s);
|
var link = $('<a/>').text(s);
|
||||||
var path = err.path;
|
var path = err.path;
|
||||||
// TODO: hack because examples/foo.a only gets listed as foo.a
|
// TODO: hack because examples/foo.a only gets listed as foo.a
|
||||||
|
@ -1887,7 +1887,9 @@ function globalErrorHandler(msgevent) {
|
||||||
var err = msgevent.error;
|
var err = msgevent.error;
|
||||||
var werr : WorkerError = {msg:msg, line:0};
|
var werr : WorkerError = {msg:msg, line:0};
|
||||||
if (err instanceof EmuHalt && err.$loc) {
|
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]);
|
showErrorAlert([werr]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,309 +4,59 @@ import { PLATFORMS, AnimationTimer, EmuHalt } from "../common/emu";
|
||||||
import { loadScript } from "../ide/ui";
|
import { loadScript } from "../ide/ui";
|
||||||
import { BASICRuntime } from "../common/basic/runtime";
|
import { BASICRuntime } from "../common/basic/runtime";
|
||||||
import { BASICProgram } from "../common/basic/compiler";
|
import { BASICProgram } from "../common/basic/compiler";
|
||||||
|
import { TeleTypeWithKeyboard } from "../common/teletype";
|
||||||
|
|
||||||
const BASIC_PRESETS = [
|
const BASIC_PRESETS = [
|
||||||
{ id: 'hello.bas', name: 'Hello World' }
|
{ 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 = $('<div class="transcript-line"/>')[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 = $("<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 {
|
class BASICPlatform implements Platform {
|
||||||
mainElement: HTMLElement;
|
mainElement: HTMLElement;
|
||||||
program: BASICProgram;
|
program: BASICProgram;
|
||||||
runtime: BASICRuntime;
|
runtime: BASICRuntime;
|
||||||
timer: AnimationTimer;
|
timer: AnimationTimer;
|
||||||
tty: TeleTypeWithKeyboard;
|
tty: TeleTypeWithKeyboard;
|
||||||
ips: number = 500;
|
ips: number = 1000;
|
||||||
clock: number = 0;
|
clock: number = 0;
|
||||||
hotReload: boolean = false;
|
hotReload: boolean = false;
|
||||||
|
debugstop: boolean = false; // TODO: should be higher-level support
|
||||||
|
|
||||||
constructor(mainElement: HTMLElement) {
|
constructor(mainElement: HTMLElement) {
|
||||||
//super();
|
//super();
|
||||||
this.mainElement = mainElement;
|
this.mainElement = mainElement;
|
||||||
mainElement.style.overflowY = 'auto';
|
mainElement.style.overflowY = 'auto';
|
||||||
mainElement.style.backgroundColor = 'white';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
await loadScript('./gen/common/basic/runtime.js');
|
await loadScript('./gen/common/basic/runtime.js');
|
||||||
|
await loadScript('./gen/common/teletype.js');
|
||||||
// create runtime
|
// create runtime
|
||||||
this.runtime = new BASICRuntime();
|
this.runtime = new BASICRuntime();
|
||||||
this.runtime.reset();
|
this.runtime.reset();
|
||||||
// create divs
|
// create divs
|
||||||
var parent = this.mainElement;
|
var parent = this.mainElement;
|
||||||
// TODO: input line should be always flush left
|
// TODO: input line should be always flush left
|
||||||
var gameport = $('<div id="gameport" style="margin-top:80vh"/>').appendTo(parent);
|
var gameport = $('<div id="gameport" style="margin-top:calc(100vh - 8em)"/>').appendTo(parent);
|
||||||
var windowport = $('<div id="windowport" class="transcript transcript-style-2"/>').appendTo(gameport);
|
var windowport = $('<div id="windowport" class="transcript transcript-style-2"/>').appendTo(gameport);
|
||||||
var inputline = $('<input class="transcript-input transcript-style-2" type="text" style="max-width:95%"/>').appendTo(parent);
|
var inputport = $('<div id="inputport" class="transcript-bottom"/>').appendTo(gameport);
|
||||||
|
var inputline = $('<input class="transcript-input transcript-style-2" type="text" style="max-width:95%"/>').appendTo(inputport);
|
||||||
//var printhead = $('<div id="printhead" class="transcript-print-head"/>').appendTo(parent);
|
//var printhead = $('<div id="printhead" class="transcript-print-head"/>').appendTo(parent);
|
||||||
//var printshield = $('<div id="printhead" class="transcript-print-shield"/>').appendTo(parent);
|
//var printshield = $('<div id="printhead" class="transcript-print-shield"/>').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.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.timer = new AnimationTimer(60, this.animate.bind(this));
|
||||||
this.resize = () => {
|
this.resize = () => {
|
||||||
// set font size proportional to window width
|
// set font size proportional to window width
|
||||||
|
@ -361,6 +111,7 @@ class BASICPlatform implements Platform {
|
||||||
var didExit = this.runtime.exited;
|
var didExit = this.runtime.exited;
|
||||||
this.program = data;
|
this.program = data;
|
||||||
this.runtime.load(data);
|
this.runtime.load(data);
|
||||||
|
this.tty.uppercaseOnly = this.program.opts.uppercaseOnly;
|
||||||
// only reset if we exited, otherwise we try to resume
|
// only reset if we exited, otherwise we try to resume
|
||||||
if (!this.hotReload || didExit) this.reset();
|
if (!this.hotReload || didExit) this.reset();
|
||||||
}
|
}
|
||||||
|
@ -370,13 +121,12 @@ class BASICPlatform implements Platform {
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
var didExit = this.runtime.exited;
|
|
||||||
this.tty.clear();
|
this.tty.clear();
|
||||||
this.runtime.reset();
|
this.runtime.reset();
|
||||||
// restart program if it's finished, otherwise reset and hold
|
if (this.debugstop)
|
||||||
if (didExit) {
|
this.break();
|
||||||
|
else
|
||||||
this.resume();
|
this.resume();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pause(): void {
|
pause(): void {
|
||||||
|
@ -385,6 +135,7 @@ class BASICPlatform implements Platform {
|
||||||
|
|
||||||
resume(): void {
|
resume(): void {
|
||||||
this.clock = 0;
|
this.clock = 0;
|
||||||
|
this.debugstop = false;
|
||||||
this.timer.start();
|
this.timer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,14 +187,17 @@ class BASICPlatform implements Platform {
|
||||||
this.onBreakpointHit = null;
|
this.onBreakpointHit = null;
|
||||||
}
|
}
|
||||||
step() {
|
step() {
|
||||||
this.pause();
|
if (this.tty.waitingfor == null) {
|
||||||
this.advance();
|
this.pause();
|
||||||
this.break();
|
this.advance();
|
||||||
|
this.break();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break() {
|
break() {
|
||||||
// TODO: don't highlight text in editor
|
// TODO: why doesn't highlight go away on resume?
|
||||||
if (this.onBreakpointHit) {
|
if (this.onBreakpointHit) {
|
||||||
//TODO: this.onBreakpointHit(this.saveState());
|
this.onBreakpointHit(this.saveState());
|
||||||
|
this.debugstop = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user