var _REPL_dbg_level = LOG_DEBUG; #var _REPL_dbg_level = LOG_ALERT; var REPL = { df_status: 0, whitespace: [" ", "\t", "\n", "\r"], end_statement: [";", ","], statement_types: [ "for", "foreach", "forindex", "while", "else", "func", "if", "elsif" ], operators_binary_unary: [ "~", "+", "-", "*", "/", "!", "?", ":", ".", ",", "<", ">", "=", "|", "&", "^" ], brackets: { "(":")", "[":"]", "{":"}", }, str_chars: ["'", '"', "`"], brackets_rev: {}, brackets_start: [], brackets_end: [], new: func(placement, name="", keep_history=1, namespace=nil) { if (namespace == nil) namespace = {}; elsif (typeof(namespace) == 'scalar') namespace = globals[namespace]; if (typeof(namespace) != 'hash') die("bad namespace!"); var m = { parents: [REPL], placement: placement, name: name, keep_history: keep_history, namespace: namespace, history: [], current: nil, }; return m; }, execute: func() { var code = string.join("\n", me.current.line); me.current = nil; logprint(_REPL_dbg_level, "compiling code..."~debug.string(code)); var fn = call(func compile(code, me.name), nil, var err=[]); if (size(err)) { var msg = err[0]; var prefix = "Parse error: "; if (substr(msg, 0, size(prefix)) == prefix) msg = substr(msg, size(prefix)); var (msg, line) = split(" at line ", msg); #debug.dump(err); me.placement.handle_parse_error(msg, me.name, line); # message, (file)name, line number return 0; } var res = call(bind(fn, globals), nil, nil, me.namespace, err); if (size(err)) { me.placement.handle_runtime_error(err); # err vec return 0; } me.placement.display_result(res); return 1; }, _is_str_char: func(char) { foreach (var c; me.str_chars) if (c == char or c[0] == char) return 1; return 0; }, _handle_level: func(level, str, line_number) { if (size(str) != 1) var str = substr(str, 0, 1); if (contains(me.brackets, str)) { append(level, str); logprint(_REPL_dbg_level, "> level add "~str); return 1; } elsif (contains(me.brackets_rev, str)) { var l = pop(level); if (l == nil) { me.placement.handle_parse_error("extra closing bracket "'"'~str~'"', me.name, line_number); return nil; } elsif (me.brackets[l] != str) { me.placement.handle_parse_error("bracket mismatch: "~me.brackets[l]~" vs "~str, me.name, line_number); return nil; } else { logprint(_REPL_dbg_level, "< level pop "~str); return 1; } } return 0; }, get_input: func() { var lines = me.placement.get_line(); if (lines == nil or string.trim(lines) == "") return me.df_status; var ls = split("\n", lines); var lines = []; foreach (var l; ls) lines ~= split("\r", l); foreach (var line; lines) { var len = size(line); if (me.current == nil) me.current = { line: [], brackets: [], level: [], statement: nil, statement_level: nil, last_operator: nil, }; for (var i=0; i size(me.current.level)) { logprint(_REPL_dbg_level, "statement ended by level below"); # cancel out of statement me.current.statement = nil; me.current.statement_level = nil; } elsif (line[i] == `{`) { # cancel out of looking for `;`, because we have a real block here logprint(_REPL_dbg_level, "statement ended by braces"); me.current.statement = nil; me.current.statement_level = nil; } } continue; } elsif (string.isalpha(line[i])) { me.current.last_operator = nil; foreach (var stmt; me.statement_types) { if (substr(line, i, size(stmt)) == stmt and (i+size(stmt) >= len or !string.isalnum(line[i+size(stmt)]) and line[i+size(stmt)] != `_`)) { logprint(_REPL_dbg_level, "found: "~stmt); me.current.statement = stmt; me.current.statement_level = size(me.current.level); i += size(stmt)-1; break; } } } elsif (me._is_str_char(line[i])) { me.current.last_operator = nil; append(me.current.level, chr(line[i])); logprint(_REPL_dbg_level, "> into string with "~me.current.level[-1]); } else { var ret = me._handle_level(me.current.level, chr(line[i]), size(me.current.line)+1); me.current.last_operator = nil; if (ret == nil) # error return 0; elsif (ret == 0) { foreach (var o; me.operators_binary_unary) if (line[i] == o[0]) { me.current.last_operator = o; logprint(_REPL_dbg_level, "found operator "~o); break } } } } append(me.current.line, line); if (me.keep_history) append(me.history, { type: "input", line: line, }); } var execute = (me.current.statement == nil and me.current.last_operator == nil and !size(me.current.level)); if (execute) { me.df_status = 0; return me.execute(); } else return (me.df_status = -1); }, }; foreach (var b; keys(REPL.brackets)) { var v = REPL.brackets[b]; append(REPL.brackets_start, b); append(REPL.brackets_end, v); REPL.brackets_rev[v] = b; } var CanvasPlacement = { instances: [], current_instance: nil, keys: [ "ESC", "Exit/close this dialog", "Ctrl-d", "Same as ESC", "Ctrl-v", "Insert text (at the end of the current line)", "Ctrl-c", "Copy the current line of text", "Ctrl-x", "Copy and delete the current line of text", "Up", "Previous line in history", "Down", "Next line in history", "Left", nil, "Right", nil, "Shift+Left", nil, "Shift+Right", nil, ], translations: { "bad-result": "[Error: cannot display output]", "key-not-mapped": "[Not Implemented]", "help": "Welcome to the Nasal REPL Interpreter. Press any key to " "exit this message, ESC to exit the dialog (at any time " "afterwards), and type away to test code :).\n\nNote: " "this dialog will capture nearly all key-presses, so don't " "try to fly with the keyboard while this is open!" "\n\nImportant keys:", }, styles: { "default": { size: [600, 300], separate_lines: 1, window_style: "default", padding: 5, max_output_chars: 200, colors: { # TODO: integrate colors from debug.nas? text: [1,1,1], text_fill: nil, background: [0.1,0.06,0.4,0.3], error: [1,0.2,0.1], }, font_size: 17, font_file: "LiberationFonts/LiberationMono-Bold.ttf", font_aspect_ratio: 1.5, font_max_width: nil, }, "transparent-blue": { size: [600, 300], separate_lines: 1, window_style: nil, padding: 5, max_output_chars: 200, colors: { text: [1,1,1], text_fill: nil, background: [0.1,0.06,0.4,0.3], error: [1,0.2,0.1], }, font_size: 17, font_file: "LiberationFonts/LiberationMono-Bold.ttf", font_aspect_ratio: 1.5, font_max_width: nil, }, "transparent-red": { size: [600, 300], separate_lines: 1, window_style: nil, padding: 5, max_output_chars: 200, colors: { text: [1,1,1], text_fill: nil, background: [0.8,0.06,0.07,0.4], error: [1,0.2,0.1], }, font_size: 17, font_file: "LiberationFonts/LiberationMono-Bold.ttf", font_aspect_ratio: 1.5, font_max_width: nil, }, "canvas-default": { size: [600, 300], separate_lines: 1, window_style: "default", padding: 5, max_output_chars: 87, colors: { text: [0.8,0.86,0.8], text_fill: nil, background: [0.05,0.03,0.2], error: [1,0.2,0.1], }, font_size: 17, font_file: "LiberationFonts/LiberationMono-Bold.ttf", font_aspect_ratio: 1.5, font_max_width: nil, #font_max_width: 588, }, }, new: func(name="", style="canvas-default") { if (typeof(style) == 'scalar') { style = CanvasPlacement.styles[style]; } if (typeof(style) != 'hash') die("bad style"); var m = { parents: [CanvasPlacement, style], state: "startup", listeners: [], window: canvas.Window.new(style.size, style.window_style, "REPL-interpreter-"~name), lines_of_text: [], history: [], curr: 0, completion_pos: 0, #tabs: [], # TODO: support multiple tabs }; m.window.set("title", "Nasal REPL Interpreter"); #debug.dump(m.window._node); m.window.del = func() { delete(me, "del"); me.del(); # inherited canvas.Window.del(); m.window = nil; m.del(); }; if (m.window_style != nil) m.window.setBool("resize", 1); m.canvas = m.window.createCanvas() .setColorBackground(m.colors.background); m.group = m.canvas.createGroup("content"); m.vbox = canvas.VBoxLayout.new(); m.window.setLayout(m.vbox); m.scroll = canvas.gui.widgets .ScrollArea.new(m.group, canvas.style, {}); m.scroll.setColorBackground(m.colors.background); m.vbox.addItem(m.scroll); m.group = m.scroll.getContent(); m.create_msg(); m.text_group = m.group.createChild("group", "text-display"); m.text = nil; m.cursor = m.group.createChild("path") .moveTo(0, -m.padding) .lineTo(0, -11-m.padding) .setStrokeLineWidth(2) .setColor(m.colors.text) .hide(); m.repl = REPL.new(placement:m, name:name); m.window.addEventListener("keydown", func(event) { var modifiers = { "shift":event.shiftKey, "ctrl":event.ctrlKey, "alt":event.altKey, "meta":event.metaKey }; if (m.handle_key(event.key, modifiers, event.keyCode)) #keyN.setValue(-1); # drop key event }); m.update(); append(CanvasPlacement.instances, m); return m; }, del: func() { if (me.window != nil) { me.window.del(); me.window = nil } foreach (var l; me.listeners) removelistener(l); setsize(me.listeners, 0); forindex (var i; CanvasPlacement.instances) if (CanvasPlacement.instances[i] == me) { CanvasPlacement.instances[i] = CanvasPlacement.instances[-1]; pop(CanvasPlacement.instances); break; } }, add_char: func(char, reset_view=0) { me.reset_input_from_history(); me.input ~= chr(char); me.text.appendText(chr(char)); if (reset_view) me.reset_view(); return nil; }, add_text: func(text, reset_view=0) { me.reset_input_from_history(); me.input ~= text; me.text.appendText(text); if (reset_view) me.reset_view(); return nil; }, remove_char: func(reset_view=0) { me.reset_input_from_history(); me.input = substr(me.input, 0, size(me.input) - 1); var t = me.text.get("text"); if (size(t) <= me.text.stop) return nil; me.text.setText(substr(t, 0, size(t)-1)); if (reset_view) me.reset_view(); return t[-1]; }, clear_input: func(reset_view=0) { me.reset_input_from_history(); var ret = me.input; me.input = ""; var t = me.text.get("text"); me.text.setText(substr(t, 0, me.text.stop)); if (reset_view) me.reset_view(); return ret; }, replace_line: func(replacement, replace_input=1, reset_view=0) { if (replace_input) me.input = replacement; var t = me.text.get("text"); me.text.setText(substr(t, 0, me.text.stop)~replacement); if (reset_view) me.reset_view(); return nil; }, add_line: func(text, reset_text=1, reset_view=0) { me.create_line(reset_text); me.text.appendText(text); if (reset_view) me.reset_view(); }, new_prompt: func() { me.add_line(">>> "); me.text.stop = size(me.text.get("text")); }, continue_line: func(reset_text=1) { me.add_line("... ", reset_text); me.text.stop = size(me.text.get("text")); }, reset_input_from_history: func(reset_view=0) { if (me.curr < size(me.history)) { me.input = me.history[me.curr]; me.curr = size(me.history); } if (reset_view) me.reset_view(); }, reset_view: func() { me.group.update(); me.scroll.scrollToLeft().scrollToBottom(); }, set_line_color: func(color) { if (me.separate_lines) # Only change colors if this is its own line me.text.setColor(color); }, set_line_font: func(font) { if (me.separate_lines) # Only change font if this is its own line me.text.setFont(font); }, clear: func() { me.text.del(); foreach (var t; me.lines_of_text) t.del(); setsize(me.history, 0); me.curr = 0; me.input = ""; me.text = nil; setsize(me.lines_of_text, 0); me.reset_view(); }, create_msg: func() { # Text drawing mode: text and maybe a bounding box var draw_mode = canvas.Text.TEXT + (me.colors.text_fill != nil ? canvas.Text.FILLEDBOUNDINGBOX : 0); me.msg = me.group.createChild("group", "startup-message"); me.msg.text = me.msg.createChild("text", "help") .setTranslation(me.padding, me.padding+10) .setAlignment("left-baseline") .setFontSize(me.font_size, me.font_aspect_ratio) .setFont(me.font_file) .setColor(me.colors.text) .setDrawMode(draw_mode) .setMaxWidth(me.window.get("content-size[0]") - me.padding) .setText(me.gettranslation("help")); if (me.colors.text_fill != nil) me.msg.text.setColorFill(me.colors.text_fill); me.msg.text.update(); #debug.dump(me.msg.text.getTransformedBounds()); me.msg.left_col = me.msg.createChild("text", "keys") .setTranslation(me.padding, me.msg.text.getTransformedBounds()[3] + 30) .setAlignment("left-baseline") .setFontSize(me.font_size, me.font_aspect_ratio) .setFont(me.font_file) .setColor(me.colors.text) .setDrawMode(draw_mode); if (me.colors.text_fill != nil) me.msg.left_col.setColorFill(me.colors.text_fill); me.msg.left_col.update(); for (var i=0; i" : ""); # FIXME: hack, canvas::Text needs a printing character # on the first line in order to recognize the newlines ? if (me.colors.text_fill != nil) me.text.setColorFill(me.colors.text_fill); if (me.font_max_width != nil) if (me.font_max_width < 0) me.text.setMaxWidth(me.window.get("content-size[0]") - me.font_max_width); else me.text.setMaxWidth(me.font_max_width); foreach (var t; me.lines_of_text) me.text.appendText("\n"); }, update: func() { #debug.dump(me.text.getTransformedBounds()); if (me.state != "startup") me.cursor.setTranslation( me.text.getTransformedBounds()[2] + 6, me.text.getTransformedBounds()[3] + 5 ).show(); me.scroll.update(); }, handle_key: func(key, modifiers, keyCode) { var modifier_str = ""; foreach (var m; keys(modifiers)) { if (modifiers[m]) modifier_str ~= substr(m,0,1); } if (me.state == "startup") { me.msg.del(); me.msg = nil; me.new_prompt(); # initialize a new line me.text.stop = size(me.text.get("text")); me.state = "accepting input"; } elsif (!contains({"s":,"c":,"":}, modifier_str)) { return 0; # had extra modifiers, reject this event } elsif (modifiers.ctrl) { if (keyCode == `c`) { logprint(_REPL_dbg_level, "ctrl+c: "~debug.string(me.input)); me.reset_input_from_history(); if( size(me.input) and !clipboard.setText(me.input) ) print("Failed to write to clipboard"); } elsif (keyCode == `x`) { logprint(_REPL_dbg_level, "ctrl+x"); me.reset_input_from_history(); if( size(me.input) and !clipboard.setText(me.clear_input()) ) print("Failed to write to clipboard"); } elsif (keyCode == `v`) { var input = clipboard.getText(); logprint(_REPL_dbg_level, "ctrl+v: "~debug.string(input)); me.reset_input_from_history(); var abnormal = func string.iscntrl(input[j]) or (string.isxspace(input[j]) and input[j] != ` `) or !string.isascii(input[j]); var i=0; while (i cancel logprint(_REPL_dbg_level, "esc"); me.del(); return 1; } elsif (key == "Tab") { # tab logprint(_REPL_dbg_level, "tab"); return 0; me.reset_input_from_history(); if (size(text) and text[0] == `/`) { me.input = me.complete(me.input, modifiers.shift ? -1 : 1); } } elsif (size(key) > 1 or !string.isprint(key[0])) { logprint(_REPL_dbg_level, "other key: "~key); return 0; # pass other funny events } else { logprint(_REPL_dbg_level, "key: "~key[0]~" (`"~key~"`)"); me.add_char(key[0]); me.completion_pos = -1; } #logprint(_REPL_dbg_level, " -> "~me.input); me.update(); me.reset_view(); return 1; # discard key event }, get_line: func() { return me.input; }, display_result: func(res=nil) { if (res == nil) return 1; # don't display NULL results var res = call(debug.string, [res, 0], var err=[]); if (size(err)) { me.add_line(me.gettranslation("bad-result")); me.set_line_color(me.colors.error); if (me.font_file == "LiberationFonts/LiberationMono-Bold.ttf") me.set_line_font("LiberationFonts/LiberationMono-BoldItalic.ttf"); return 1; } if (size(res) > me.max_output_chars) res = substr(res, 0, me.max_output_chars-5)~". . ."; me.add_line(res); return 1; }, handle_runtime_error: func(err) { debug.printerror(err); me.add_line("Runtime error: "~err[0]); me.set_line_color(me.colors.error); for (var i=1; i", "canvas-default");