1 var _REPL_dbg_level = "debug";
2 #var _REPL_dbg_level = "alert";
6 whitespace: [" ", "\t", "\n", "\r"],
7 end_statement: [";", ","],
9 "for", "foreach", "forindex",
10 "while", "else", "func", "if", "elsif"
12 operators_binary_unary: [
13 "~", "+", "-", "*", "/",
14 "!", "?", ":", ".", ",",
15 "<", ">", "=", "|", "&", "^"
22 str_chars: ["'", '"', "`"],
24 brackets_start: [], brackets_end: [],
25 new: func(placement, name="<repl>", keep_history=1, namespace=nil) {
26 if (namespace == nil) namespace = {};
27 elsif (typeof(namespace) == 'scalar') namespace = globals[namespace];
28 if (typeof(namespace) != 'hash') die("bad namespace!");
33 keep_history: keep_history,
41 var code = string.join("\n", me.current.line);
45 printlog(_REPL_dbg_level, "compiling code..."~debug.string(code));
47 var fn = call(func compile(code, me.name), nil, var err=[]);
50 var prefix = "Parse error: ";
51 if (substr(msg, 0, size(prefix)) == prefix)
52 msg = substr(msg, size(prefix));
53 var (msg, line) = split(" at line ", msg);
55 me.placement.handle_parse_error(msg, me.name, line); # message, (file)name, line number
58 var res = call(bind(fn, globals), nil, nil, me.namespace, err);
60 me.placement.handle_runtime_error(err); # err vec
63 me.placement.display_result(res);
66 _is_str_char: func(char) {
67 foreach (var c; me.str_chars)
68 if (c == char or c[0] == char) return 1;
71 _handle_level: func(level, str, line_number) {
73 var str = substr(str, 0, 1);
74 if (contains(me.brackets, str)) {
76 printlog(_REPL_dbg_level, "> level add "~str);
78 } elsif (contains(me.brackets_rev, str)) {
81 me.placement.handle_parse_error("extra closing bracket "'"'~str~'"', me.name, line_number);
83 } elsif (me.brackets[l] != str) {
84 me.placement.handle_parse_error("bracket mismatch: "~me.brackets[l]~" vs "~str, me.name, line_number);
87 printlog(_REPL_dbg_level, "< level pop "~str);
94 var lines = me.placement.get_line();
95 if (lines == nil or string.trim(lines) == "") return me.df_status;
96 var ls = split("\n", lines); var lines = [];
97 foreach (var l; ls) lines ~= split("\r", l);
98 foreach (var line; lines) {
100 if (me.current == nil)
106 statement_level: nil,
109 for (var i=0; i<len; i+=1) {
110 if (string.isxspace(line[i])) continue;
111 if (size(me.current.level) and me._is_str_char(me.current.level[-1])) {
112 me.current.last_operator = nil;
113 if (line[i] == `\\`) {
114 i += 1; # skip the next character
115 printlog(_REPL_dbg_level, " skip backslash");
116 } elsif (line[i] == me.current.level[-1][0]) {
117 printlog(_REPL_dbg_level, "< out of string with "~me.current.level[-1]);
118 pop(me.current.level);
122 if (line[i] == `#`) {
123 while(i<len and line[i] != `\n` and line[i] != `\r`) i+=1;
126 if (me.current.statement != nil) {
127 me.current.last_operator = nil;
128 if (me.current.statement_level == size(me.current.level) and
129 (line[i] == `;` or line[i] == `,`)) {
130 printlog(_REPL_dbg_level, "statement ended by ;/,");
131 me.current.statement = nil;
132 me.current.statement_level = nil;
134 var ret = me._handle_level(me.current.level, chr(line[i]), size(me.current.line)+1);
135 if (ret == nil) {# error
138 } elsif (me.current.statement_level > size(me.current.level)) {
139 printlog(_REPL_dbg_level, "statement ended by level below");
140 # cancel out of statement
141 me.current.statement = nil;
142 me.current.statement_level = nil;
143 } elsif (line[i] == `{`) {
144 # cancel out of looking for `;`, because we have a real block here
145 printlog(_REPL_dbg_level, "statement ended by braces");
146 me.current.statement = nil;
147 me.current.statement_level = nil;
151 } elsif (string.isalpha(line[i])) {
152 me.current.last_operator = nil;
153 foreach (var stmt; me.statement_types) {
154 if (substr(line, i, size(stmt)) == stmt and
156 or !string.isalnum(line[i+size(stmt)])
157 and line[i+size(stmt)] != `_`)) {
158 printlog(_REPL_dbg_level, "found: "~stmt);
159 me.current.statement = stmt;
160 me.current.statement_level = size(me.current.level);
165 } elsif (me._is_str_char(line[i])) {
166 me.current.last_operator = nil;
167 append(me.current.level, chr(line[i]));
168 printlog(_REPL_dbg_level, "> into string with "~me.current.level[-1]);
170 var ret = me._handle_level(me.current.level, chr(line[i]), size(me.current.line)+1);
171 me.current.last_operator = nil;
172 if (ret == nil) # error
175 foreach (var o; me.operators_binary_unary)
177 { me.current.last_operator = o; printlog(_REPL_dbg_level, "found operator "~o); break }
181 append(me.current.line, line);
188 var execute = (me.current.statement == nil and me.current.last_operator == nil and !size(me.current.level));
193 return (me.df_status = -1);
196 foreach (var b; keys(REPL.brackets)) {
197 var v = REPL.brackets[b];
198 append(REPL.brackets_start, b);
199 append(REPL.brackets_end, v);
200 REPL.brackets_rev[v] = b;
203 var CanvasPlacement = {
205 current_instance: nil,
207 "ESC", "Exit/close this dialog",
208 "Ctrl-d", "Same as ESC",
209 "Ctrl-v", "Insert text (at the end of the current line)",
210 "Ctrl-c", "Copy the current line of text",
211 "Ctrl-x", "Copy and delete the current line of text",
212 "Up", "Previous line in history",
213 "Down", "Next line in history",
220 "bad-result": "[Error: cannot display output]",
221 "key-not-mapped": "[Not Implemented]",
222 "help": "Welcome to the Nasal REPL Interpreter. Press any key to "
223 "exit this message, ESC to exit the dialog (at any time "
224 "afterwards), and type away to test code :).\n\nNote: "
225 "this dialog will capture nearly all key-presses, so don't "
226 "try to fly with the keyboard while this is open!"
227 "\n\nImportant keys:",
233 window_style: "default",
235 max_output_chars: 200,
237 # TODO: integrate colors from debug.nas?
240 background: [0.1,0.06,0.4,0.3],
244 font_file: "LiberationFonts/LiberationMono-Bold.ttf",
245 font_aspect_ratio: 1.5,
248 "transparent-blue": {
253 max_output_chars: 200,
257 background: [0.1,0.06,0.4,0.3],
261 font_file: "LiberationFonts/LiberationMono-Bold.ttf",
262 font_aspect_ratio: 1.5,
270 max_output_chars: 200,
274 background: [0.8,0.06,0.07,0.4],
278 font_file: "LiberationFonts/LiberationMono-Bold.ttf",
279 font_aspect_ratio: 1.5,
285 window_style: "default",
287 max_output_chars: 87,
289 text: [0.8,0.86,0.8],
291 background: [0.05,0.03,0.2],
295 font_file: "LiberationFonts/LiberationMono-Bold.ttf",
296 font_aspect_ratio: 1.5,
298 #font_max_width: 588,
301 new: func(name="<canvas-repl>", style="canvas-default") {
302 if (typeof(style) == 'scalar') {
303 style = CanvasPlacement.styles[style];
305 if (typeof(style) != 'hash') die("bad style");
307 parents: [CanvasPlacement, style],
310 window: canvas.Window.new(style.size, style.window_style, "REPL-interpreter-"~name),
315 #tabs: [], # TODO: support multiple tabs
317 m.window.set("title", "Nasal REPL Interpreter");
318 #debug.dump(m.window._node);
319 m.window.del = func() {
321 me.del(); # inherited canvas.Window.del();
325 if (m.window_style != nil) m.window.setBool("resize", 1);
326 m.canvas = m.window.createCanvas()
327 .setColorBackground(m.colors.background);
328 m.group = m.canvas.createGroup("content");
329 m.vbox = canvas.VBoxLayout.new();
330 m.window.setLayout(m.vbox);
331 m.scroll = canvas.gui.widgets
332 .ScrollArea.new(m.group, canvas.style, {});
333 m.scroll.setColorBackground(m.colors.background);
334 m.vbox.addItem(m.scroll);
335 m.group = m.scroll.getContent();
337 m.text_group = m.group.createChild("group", "text-display");
339 m.cursor = m.group.createChild("path")
340 .moveTo(0, -m.padding)
341 .lineTo(0, -11-m.padding)
342 .setStrokeLineWidth(2)
343 .setColor(m.colors.text)
345 m.repl = REPL.new(placement:m, name:name);
346 m.window.addEventListener("keydown", func(event) {
348 "shift":event.shiftKey,
349 "ctrl":event.ctrlKey,
353 if (m.handle_key(event.key, modifiers, event.keyCode))
354 #keyN.setValue(-1); # drop key event
357 append(CanvasPlacement.instances, m);
361 if (me.window != nil)
362 { me.window.del(); me.window = nil }
363 foreach (var l; me.listeners)
365 setsize(me.listeners, 0);
366 forindex (var i; CanvasPlacement.instances)
367 if (CanvasPlacement.instances[i] == me) {
368 CanvasPlacement.instances[i] = CanvasPlacement.instances[-1];
369 pop(CanvasPlacement.instances);
373 add_char: func(char, reset_view=0) {
374 me.reset_input_from_history();
375 me.input ~= chr(char);
376 me.text.appendText(chr(char));
377 if (reset_view) me.reset_view();
380 add_text: func(text, reset_view=0) {
381 me.reset_input_from_history();
383 me.text.appendText(text);
384 if (reset_view) me.reset_view();
387 remove_char: func(reset_view=0) {
388 me.reset_input_from_history();
389 me.input = substr(me.input, 0, size(me.input) - 1);
390 var t = me.text.get("text");
391 if (size(t) <= me.text.stop) return nil;
392 me.text.setText(substr(t, 0, size(t)-1));
393 if (reset_view) me.reset_view();
396 clear_input: func(reset_view=0) {
397 me.reset_input_from_history();
400 var t = me.text.get("text");
401 me.text.setText(substr(t, 0, me.text.stop));
402 if (reset_view) me.reset_view();
405 replace_line: func(replacement, replace_input=1, reset_view=0) {
406 if (replace_input) me.input = replacement;
407 var t = me.text.get("text");
408 me.text.setText(substr(t, 0, me.text.stop)~replacement);
409 if (reset_view) me.reset_view();
412 add_line: func(text, reset_text=1, reset_view=0) {
413 me.create_line(reset_text);
414 me.text.appendText(text);
415 if (reset_view) me.reset_view();
419 me.text.stop = size(me.text.get("text"));
421 continue_line: func(reset_text=1) {
422 me.add_line("... ", reset_text);
423 me.text.stop = size(me.text.get("text"));
425 reset_input_from_history: func(reset_view=0) {
426 if (me.curr < size(me.history)) {
427 me.input = me.history[me.curr];
428 me.curr = size(me.history);
430 if (reset_view) me.reset_view();
434 me.scroll.scrollToLeft().scrollToBottom();
436 set_line_color: func(color) {
437 if (me.separate_lines)
438 # Only change colors if this is its own line
439 me.text.setColor(color);
441 set_line_font: func(font) {
442 if (me.separate_lines)
443 # Only change font if this is its own line
444 me.text.setFont(font);
448 foreach (var t; me.lines_of_text)
450 setsize(me.history, 0);
454 setsize(me.lines_of_text, 0);
458 # Text drawing mode: text and maybe a bounding box
459 var draw_mode = canvas.Text.TEXT + (me.colors.text_fill != nil ? canvas.Text.FILLEDBOUNDINGBOX : 0);
461 me.msg = me.group.createChild("group", "startup-message");
462 me.msg.text = me.msg.createChild("text", "help")
463 .setTranslation(me.padding, me.padding+10)
464 .setAlignment("left-baseline")
465 .setFontSize(me.font_size, me.font_aspect_ratio)
466 .setFont(me.font_file)
467 .setColor(me.colors.text)
468 .setDrawMode(draw_mode)
469 .setMaxWidth(me.window.get("content-size[0]") - me.padding)
470 .setText(me.gettranslation("help"));
471 if (me.colors.text_fill != nil)
472 me.msg.text.setColorFill(me.colors.text_fill);
473 me.msg.text.update();
474 #debug.dump(me.msg.text.getTransformedBounds());
475 me.msg.left_col = me.msg.createChild("text", "keys")
476 .setTranslation(me.padding, me.msg.text.getTransformedBounds()[3] + 30)
477 .setAlignment("left-baseline")
478 .setFontSize(me.font_size, me.font_aspect_ratio)
479 .setFont(me.font_file)
480 .setColor(me.colors.text)
481 .setDrawMode(draw_mode);
482 if (me.colors.text_fill != nil)
483 me.msg.left_col.setColorFill(me.colors.text_fill);
484 me.msg.left_col.update();
485 for (var i=0; i<size(me.keys); i+=2) {
486 if (i) me.msg.left_col.appendText("\n");
487 me.msg.left_col.appendText("- "~me.keys[i]);
489 #debug.dump(me.msg.left_col.getTransformedBounds());
490 me.msg.right_col = me.msg.createChild("text", "keys")
491 .setTranslation(me.msg.left_col.getTransformedBounds()[2] + 20,
492 me.msg.text.getTransformedBounds()[3] + 30)
493 .setAlignment("left-baseline")
494 .setFontSize(me.font_size, me.font_aspect_ratio)
495 .setFont(me.font_file)
496 .setColor(me.colors.text)
497 .setDrawMode(draw_mode);
498 if (me.colors.text_fill != nil)
499 me.msg.right_col.setColorFill(me.colors.text_fill);
500 for (var i=0; i<size(me.keys); i+=2) {
501 if (i) me.msg.right_col.appendText("\n");
503 if (desc == nil) desc = me.gettranslation("key-not-mapped");
504 elsif (desc[-1] != `.`) desc ~= ".";
505 me.msg.right_col.appendText(desc);
508 create_line: func(reset_text=1) {
509 # c.f. above, in me.create_msg()
510 var draw_mode = canvas.Text.TEXT + (me.colors.text_fill != nil ? canvas.Text.FILLEDBOUNDINGBOX : 0);
512 if (reset_text) me.input = "";
513 # If we only use one line, and one exists, things are simple:
514 if (!me.separate_lines and me.text != nil) {
515 me.text.appendText("\n");
518 # Else, we have to create a new line
520 append(me.lines_of_text, me.text);
521 me.text = me.text_group
522 .createChild("text", "input"~size(me.lines_of_text))
523 .setAlignment("left-baseline")
524 .setFontSize(me.font_size, me.font_aspect_ratio)
525 .setFont(me.font_file)
526 .setColor(me.colors.text)
527 .setDrawMode(draw_mode)
528 .setText(size(me.lines_of_text) ? ">" : ""); # FIXME: hack, canvas::Text needs a printing character
529 # on the first line in order to recognize the newlines ?
530 if (me.colors.text_fill != nil)
531 me.text.setColorFill(me.colors.text_fill);
532 if (me.font_max_width != nil)
533 if (me.font_max_width < 0)
534 me.text.setMaxWidth(me.window.get("content-size[0]") - me.font_max_width);
536 me.text.setMaxWidth(me.font_max_width);
538 foreach (var t; me.lines_of_text)
539 me.text.appendText("\n");
542 #debug.dump(me.text.getTransformedBounds());
543 if (me.state != "startup")
544 me.cursor.setTranslation(
545 me.text.getTransformedBounds()[2] + 6,
546 me.text.getTransformedBounds()[3] + 5
550 handle_key: func(key, modifiers, keyCode) {
551 var modifier_str = "";
552 foreach (var m; keys(modifiers)) {
554 modifier_str ~= substr(m,0,1);
556 if (me.state == "startup") {
557 me.msg.del(); me.msg = nil;
558 me.new_prompt(); # initialize a new line
559 me.text.stop = size(me.text.get("text"));
560 me.state = "accepting input";
562 } elsif (!contains({"s":,"c":,"":}, modifier_str)) {
563 return 0; # had extra modifiers, reject this event
565 } elsif (modifiers.ctrl) {
566 if (keyCode == `c`) {
567 printlog(_REPL_dbg_level, "ctrl+c: "~debug.string(me.input));
568 me.reset_input_from_history();
569 if( size(me.input) and !clipboard.setText(me.input) )
570 print("Failed to write to clipboard");
571 } elsif (keyCode == `x`) {
572 printlog(_REPL_dbg_level, "ctrl+x");
573 me.reset_input_from_history();
574 if( size(me.input) and !clipboard.setText(me.clear_input()) )
575 print("Failed to write to clipboard");
576 } elsif (keyCode == `v`) {
577 var input = clipboard.getText();
578 printlog(_REPL_dbg_level, "ctrl+v: "~debug.string(input));
579 me.reset_input_from_history();
580 var abnormal = func string.iscntrl(input[j]) or (string.isxspace(input[j]) and input[j] != ` `) or !string.isascii(input[j]);
582 while (i<size(input)) {
583 for (var j=i; j<size(input); j+=1)
584 if (abnormal()) break;
585 if (j != i) me.add_text(substr(input, i, j-i));
586 while (j<size(input) and abnormal()) {
587 # replace tabs with spaces
588 if (input[j] == `\t`)
590 # handle newlines like they're shift+space, i.e. continue don't evaluate
591 elsif (input[j] == `\n` or input[j] == `\r`) {
592 if (j<size(input)-1 and input[j+1] == `\n`)
594 me.input ~= "\n"; me.continue_line(reset_text:0);
596 # skip other non-ascii characters
601 } elsif (keyCode == `d`) { # ctrl-D/EOF
602 printlog(_REPL_dbg_level, "EOF");
607 } elsif (key == "Enter") {
608 printlog(_REPL_dbg_level, "return (key: "~key~", shift: "~modifiers.shift~")");
609 me.reset_input_from_history();
611 if (modifiers.shift) {
616 if (size(string.trim(me.input))) {
617 append(me.history, me.input);
618 me.curr += 1; # simplified version of: me.curr = size(me.history);
619 if (me.curr != size(me.history)) die(me.curr~" vs "~size(me.history));
621 CanvasPlacement.current_instance = me;
622 var res = me.repl.get_input();
623 CanvasPlacement.current_instance = nil;
624 printlog(_REPL_dbg_level, "return code: "~debug.string(res));
627 me.continue_line(reset_text:reset_text);
628 else me.new_prompt();
630 } elsif (key == "Backspace") { # backspace
631 printlog(_REPL_dbg_level, "back");
632 me.reset_input_from_history();
633 if (me.remove_char() == nil) return 1; # nothing happened, since the input
634 # field was blank, but capture the event
635 me.completion_pos = -1;
637 } elsif (key == "Up") { # up
638 printlog(_REPL_dbg_level, "up");
639 if (me.curr == 0) return 1;
641 if (me.curr == size(me.history))
642 me.replace_line(me.input, 0);
644 me.replace_line(me.history[me.curr], 0);
645 me.completion_pos = -1;
647 } elsif (key == "Down") { # down
648 printlog(_REPL_dbg_level, "down");
649 if (me.curr == size(me.history)) return 1;
651 if (me.curr == size(me.history))
652 me.replace_line(me.input, 0);
654 me.replace_line(me.history[me.curr], 0);
655 me.completion_pos = -1;
657 } elsif (key == "Escape") { # escape -> cancel
658 printlog(_REPL_dbg_level, "esc");
662 } elsif (key == "Tab") { # tab
663 printlog(_REPL_dbg_level, "tab");
665 me.reset_input_from_history();
666 if (size(text) and text[0] == `/`) {
667 me.input = me.complete(me.input, modifiers.shift ? -1 : 1);
670 } elsif (size(key) > 1 or !string.isprint(key[0])) {
671 printlog(_REPL_dbg_level, "other key: "~key);
672 return 0; # pass other funny events
675 printlog(_REPL_dbg_level, "key: "~key[0]~" (`"~key~"`)");
677 me.completion_pos = -1;
680 #printlog(_REPL_dbg_level, " -> "~me.input);
685 return 1; # discard key event
690 display_result: func(res=nil) {
691 if (res == nil) return 1; # don't display NULL results
692 var res = call(debug.string, [res, 0], var err=[]);
694 me.add_line(me.gettranslation("bad-result"));
695 me.set_line_color(me.colors.error);
696 if (me.font_file == "LiberationFonts/LiberationMono-Bold.ttf")
697 me.set_line_font("LiberationFonts/LiberationMono-BoldItalic.ttf");
700 if (size(res) > me.max_output_chars)
701 res = substr(res, 0, me.max_output_chars-5)~". . .";
705 handle_runtime_error: func(err) {
706 debug.printerror(err);
707 me.add_line("Runtime error: "~err[0]);
708 me.set_line_color(me.colors.error);
709 for (var i=1; i<size(err); i+=2) {
710 me.add_line(" at "~err[i]~", line "~err[i+1]);
713 handle_parse_error: func(msg, file, line) {
714 print("Parse error: "~msg~" on line "~line~" in "~file);
715 me.add_line("Parse error: "~msg~" on line "~line~" in "~file);
716 me.set_line_color(me.colors.error);
718 gettranslation: func(k) me.translations[k] or "[Error: no translation for key "~k~"]",
721 var print2 = func(i) {
722 console.CanvasPlacement.current_instance.display_result(i);
723 return nil; # just to suppress output
725 #CanvasPlacement.new("<styled-canvas-repl>", "canvas-default");