From 628910a8505f7a1b0c18e0996f88da48c1b64e0a Mon Sep 17 00:00:00 2001 From: Austin Cory Bart Date: Fri, 26 Jul 2019 09:12:51 -0400 Subject: [PATCH] Alpha version! --- .idea/workspace.xml | 76 +- dist/block_mirror.css | 391 +++ dist/block_mirror.js | 6194 ++++++++++++++++++++++++++++++++- lib/codemirror/foldcode.js | 152 + lib/codemirror/foldgutter.js | 146 + lib/codemirror/fullscreen.css | 6 + lib/codemirror/fullscreen.js | 41 + lib/codemirror/python-hint.js | 93 + lib/codemirror/show-hint.css | 36 + lib/codemirror/show-hint.js | 460 +++ src/ast/ast_Call.js | 1 + src/ast/ast_Comp.js | 16 +- src/ast/ast_FunctionDef.js | 2 +- src/block_mirror.js | 5 +- src/text_editor.js | 71 +- test/simple.html | 28 + test/simple_dev.html | 7 +- webpack.config.js | 87 +- 18 files changed, 7583 insertions(+), 229 deletions(-) create mode 100644 lib/codemirror/foldcode.js create mode 100644 lib/codemirror/foldgutter.js create mode 100644 lib/codemirror/fullscreen.css create mode 100644 lib/codemirror/fullscreen.js create mode 100644 lib/codemirror/python-hint.js create mode 100644 lib/codemirror/show-hint.css create mode 100644 lib/codemirror/show-hint.js diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 01572cb..799bc58 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,66 +2,14 @@ - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -106,6 +54,7 @@ + @@ -174,8 +123,17 @@ - + + + + 1564063805490 + + @@ -186,12 +144,18 @@ - + + + + + diff --git a/dist/block_mirror.css b/dist/block_mirror.css index a4cce5d..b2ec407 100644 --- a/dist/block_mirror.css +++ b/dist/block_mirror.css @@ -1,3 +1,394 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; + direction: ltr; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0 !important; + background: #7e7; +} +.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} +.cm-fat-cursor-mark { + background-color: rgba(20, 255, 20, 0.5); + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + background-color: #7e7; +} +@-moz-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@-webkit-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} + +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror-overwrite .CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-rulers { + position: absolute; + left: 0; right: 0; top: -50px; bottom: -20px; + overflow: hidden; +} +.CodeMirror-ruler { + border-left: 1px solid #ccc; + top: 0; bottom: 0; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actual scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + min-height: 100%; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -30px; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; +} +.CodeMirror-gutter-background { + position: absolute; + top: 0; bottom: 0; + z-index: 4; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} +.CodeMirror-gutter-wrapper ::selection { background-color: transparent } +.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: contextual; + font-variant-ligatures: contextual; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + padding: 0.1px; /* Force widget margins to stay inside of the container */ +} + +.CodeMirror-widget {} + +.CodeMirror-rtl pre { direction: rtl; } + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.CodeMirror-cursor { + position: absolute; + pointer-events: none; +} +.CodeMirror-measure pre { position: static; } + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +div.CodeMirror-dragcursors { + visibility: visible; +} + +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } +.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background-color: #ffa; + background-color: rgba(255, 255, 0, .4); +} + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } + +.CodeMirror-fullscreen { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + height: auto; + z-index: 9; +} + +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + + margin: 0; + padding: 2px; + + -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + box-shadow: 2px 3px 5px rgba(0,0,0,.2); + border-radius: 3px; + border: 1px solid silver; + + background: white; + font-size: 90%; + font-family: monospace; + + max-height: 20em; + overflow-y: auto; +} + +.CodeMirror-hint { + margin: 0; + padding: 0 4px; + border-radius: 2px; + white-space: pre; + color: black; + cursor: pointer; +} + +li.CodeMirror-hint-active { + background: #08f; + color: white; +} + .CodeMirror-gutters { background-color: #ddd; /*border-left: 1px solid #bbb;*/ diff --git a/dist/block_mirror.js b/dist/block_mirror.js index c5da63d..a2f73b2 100644 --- a/dist/block_mirror.js +++ b/dist/block_mirror.js @@ -1,141 +1,153 @@ -function BlockMirrorTextEditor(blockMirror) { - this.blockMirror = blockMirror; - this.textContainer = blockMirror.tags.textContainer; - this.textArea = blockMirror.tags.textArea; - this.textSidebar = blockMirror.tags.textSidebar; - - // notification - this.silentEvents_ = false; - - let codeMirrorOptions = { - mode: { - name: 'python', - version: 3, - singleLineStringErrors: false - }, - readOnly: blockMirror.configuration.readOnly, - showCursorWhenSelecting: true, - lineNumbers: true, - firstLineNumber: 1, - indentUnit: 4, - tabSize: 4, - indentWithTabs: false, - matchBrackets: true, - extraKeys: { - 'Tab': 'indentMore', - 'Shift-Tab': 'indentLess', - 'Ctrl-Enter': blockMirror.run, - 'Esc': this.defocus.bind(this) - } - }; - this.codeMirror = CodeMirror.fromTextArea(this.textArea, codeMirrorOptions); - this.codeMirror.on('change', this.changed.bind(this)); - this.codeMirror.setSize(null, '100%'); - this.textContainer.style.border= '1px solid lightgray'; - this.textContainer.style.float = 'left'; - this.updateWidth(); - this.textContainer.style.height = blockMirror.configuration.height; - // Style sidebar - this.textSidebar.style.height = '100%'; - this.textSidebar.style.float = 'left'; - this.textSidebar.style.backgroundColor = '#ddd'; - -} - -BlockMirrorTextEditor.prototype.defocus = function() { - this.codeMirror.display.input.blur(); -} +/** + * Initialise the database of variable names. + * @param {!Blockly.Workspace} workspace Workspace to generate code from. + */ +Blockly.Python.init = function (workspace) { + /** + * Empty loops or conditionals are not allowed in Python. + */ + Blockly.Python.PASS = this.INDENT + 'pass\n'; + // Create a dictionary of definitions to be printed before the code. + Blockly.Python.definitions_ = Object.create(null); + // Create a dictionary mapping desired function names in definitions_ + // to actual function names (to avoid collisions with user functions). + Blockly.Python.functionNames_ = Object.create(null); -BlockMirrorTextEditor.prototype.updateWidth = function() { - var newWidth = '0%'; - /*if (this.blockMirror.views.includes('text')) { - newWidth = (100 / this.blockMirror.views.length)+'%'; + if (!Blockly.Python.variableDB_) { + Blockly.Python.variableDB_ = + new Blockly.Names(Blockly.Python.RESERVED_WORDS_); + } else { + Blockly.Python.variableDB_.reset(); } - this.textContainer.style.width = newWidth;*/ -} -BlockMirrorTextEditor.prototype.setReadOnly = function(isReadOnly) { - this.codeMirror.setOption('readOnly', isReadOnly); -} + Blockly.Python.variableDB_.setVariableMap(workspace.getVariableMap()); -BlockMirrorTextEditor.prototype.VIEW_CONFIGURATIONS = { - 'split': { - 'width': '40%', - 'visible': true, - 'indentSidebar': false - }, - 'text': { - 'width': '100%', - 'visible': true, - 'indentSidebar': true - }, - 'block': { - 'width': '0%', - 'visible': false, - 'indentSidebar': false + var defvars = []; + // Add developer variables (not created or named by the user). + /* + var devVarList = Blockly.Variables.allDeveloperVariables(workspace); + for (var i = 0; i < devVarList.length; i++) { + defvars.push(Blockly.Python.variableDB_.getName(devVarList[i], + Blockly.Names.DEVELOPER_VARIABLE_TYPE) + ' = None'); } -} -BlockMirrorTextEditor.prototype.setMode = function(mode) { - mode = mode.toLowerCase(); - let configuration = this.VIEW_CONFIGURATIONS[mode]; - this.textContainer.style.width = configuration.width; - if (configuration.visible) { - this.textContainer.style.height = this.blockMirror.configuration.height; - this.textContainer.style.display = 'block'; - this.codeMirror.getWrapperElement().style.display = 'block'; - this.codeMirror.refresh(); - } else { - this.textContainer.style.height = '0%'; - this.textContainer.style.display = 'none'; - this.codeMirror.getWrapperElement().style.display = 'none'; - } - // Should we indent the toolbox - if (configuration.indentSidebar) { - let gutters = this.textContainer.querySelector('.CodeMirror-gutters'); - let gutterWidth = gutters.offsetWidth; - let toolbarWidth = this.blockMirror.blockEditor.getToolbarWidth(); - let newGutterWidth = toolbarWidth - gutterWidth - 2; - this.textSidebar.style.width = newGutterWidth + 'px'; - this.textSidebar.style.display = 'block'; - } else { - this.textSidebar.style.display = 'none'; - this.textSidebar.style.width = '0px'; + // Add user variables, but only ones that are being used. + var variables = Blockly.Variables.allUsedVarModels(workspace); + for (var i = 0; i < variables.length; i++) { + defvars.push(Blockly.Python.variableDB_.getName(variables[i].getId(), + Blockly.Variables.NAME_TYPE) + ' = None'); } -} -BlockMirrorTextEditor.prototype.setCode = function(code, quietly) { - this.silentEvents_ = quietly; - if (code === undefined || code.trim() === "") { - this.codeMirror.setValue("\n"); - } else { - this.codeMirror.setValue(code); - } + Blockly.Python.definitions_['variables'] = defvars.join('\n'); + */ }; -BlockMirrorTextEditor.prototype.getCode = function() { - return this.codeMirror.getValue(); +/** + * Prepend the generated code with the variable definitions. + * @param {string} code Generated code. + * @return {string} Completed code. + */ +Blockly.Python.finish = function (code) { + // Convert the definitions dictionary into a list. + var imports = []; + var definitions = []; + for (var name in Blockly.Python.definitions_) { + var def = Blockly.Python.definitions_[name]; + if (def.match(/^(from\s+\S+\s+)?import\s+\S+/)) { + imports.push(def); + } else { + definitions.push(def); + } + } + // Clean up temporary data. + delete Blockly.Python.definitions_; + delete Blockly.Python.functionNames_; + Blockly.Python.variableDB_.reset(); + // acbart: Don't actually inject initializations - we don't need 'em. + return code; }; -BlockMirrorTextEditor.prototype.changed = function(codeMirror, event) { - if (!this.silentEvents_) { - let newCode = this.getCode(); - this.blockMirror.blockEditor.setCode(newCode, true); - this.blockMirror.model_ = newCode; - } - this.silentEvents_ = false; - //console.log("Changed text"); +Blockly.Python.INDENT = ' '; + +Blockly.Python.RESERVED_WORDS_ = ( + "False,None,True,and,as,assert,break,class," + + "continue,def,del,elif,else,except,finally,for," + + "from,global,if,import,in,is,lambda,nonlocal," + + "not,or,pass,raise,return,try,while,with,yield" +); + +/** + * Naked values are top-level blocks with outputs that aren't plugged into + * anything. + * @param {string} line Line of generated code. + * @return {string} Legal line of code. + */ +Blockly.Python.scrubNakedValue = function (line) { + // acbart: Remove extra new line + return line; }; +/** + * Construct the blocks required by the flyout for the variable category. + * @param {!Blockly.Workspace} workspace The workspace containing variables. + * @return {!Array.} Array of XML block elements. + */ +Blockly.Variables.flyoutCategoryBlocks = function (workspace) { + var variableModelList = workspace.getVariablesOfType(''); + + var xmlList = []; + if (variableModelList.length > 0) { + // New variables are added to the end of the variableModelList. + var mostRecentVariableFieldXmlString = + variableModelList[variableModelList.length - 1]; + if (Blockly.Blocks['ast_Assign']) { + var gap = Blockly.Blocks['ast_AugAssign'] ? 8 : 24; + var blockText = '' + + '' + + mostRecentVariableFieldXmlString + + '' + + ''; + var block = Blockly.Xml.textToDom(blockText).firstChild; + xmlList.push(block); + } + if (Blockly.Blocks['ast_AugAssign']) { + var gap = Blockly.Blocks['ast_Name'] ? 20 : 8; + var blockText = '' + + '' + + mostRecentVariableFieldXmlString + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + ''; + var block = Blockly.Xml.textToDom(blockText).firstChild; + xmlList.push(block); + } + + if (Blockly.Blocks['ast_Name']) { + variableModelList.sort(Blockly.VariableModel.compareByName); + for (var i = 0, variable; variable = variableModelList[i]; i++) { + var block = Blockly.utils.xml.createElement('block'); + block.setAttribute('type', 'ast_Name'); + block.setAttribute('gap', 8); + block.appendChild(Blockly.Variables.generateVariableFieldDom(variable)); + xmlList.push(block); + } + } + } + return xmlList; +}; /** External visible stuff - Changing mode/code/filename can fail on the block side + Changing mode/code can fail on the block side setMode(mode) -> bool - setFilename(filename) -> bool setCode(filename, code) -> bool setHighlight(line) -> bool setReadOnly(isReadOnly) @@ -146,12 +158,9 @@ BlockMirrorTextEditor.prototype.changed = function(codeMirror, event) { list[list[string]] onChange(event) -> onModeChange - onFilenameChange onCodeChange - getCode(filename) -> string getCode() -> string - getFilename() -> string getMode() -> string getImage(callback) @@ -170,19 +179,12 @@ function BlockMirror(configuration) { this.blockEditor = new BlockMirrorBlockEditor(this); this.textToBlocks = new BlockMirrorTextToBlocks(this); - Sk.configure({ - __future__: Sk.python3, - read: function (filename) { - if (Sk.builtinFiles === undefined || - Sk.builtinFiles["files"][filename] === undefined) { - throw "File not found: '" + filename + "'"; - } - return Sk.builtinFiles["files"][filename]; - } - }); + if (!this.configuration.skipSkulpt) { + this.loadSkulpt(); + } this.setMode(this.configuration.viewMode); -} +}; BlockMirror.prototype.validateConfiguration = function (configuration) { this.configuration = {}; @@ -218,6 +220,12 @@ BlockMirror.prototype.validateConfiguration = function (configuration) { // viewMode this.configuration.viewMode = configuration.viewMode || 'split'; + + // Need to load skulpt? + this.configuration.skipSkulpt = configuration.skipSkulpt || false; + + // Delay? + this.configuration.blockDelay = configuration.blockDelay || false; } BlockMirror.prototype.initializeVariables = function () { @@ -246,8 +254,8 @@ BlockMirror.prototype.initializeVariables = function () { } // Files - this.files = []; - this.model_ = ""; + this.code_ = ""; + this.mode_ = null; // Update Flags this.silenceBlock = false; @@ -266,18 +274,31 @@ BlockMirror.prototype.initializeVariables = function () { this.listeners_ = []; } +BlockMirror.prototype.loadSkulpt = function () { + Sk.configure({ + __future__: Sk.python3, + read: function (filename) { + if (Sk.builtinFiles === undefined || + Sk.builtinFiles["files"][filename] === undefined) { + throw "File not found: '" + filename + "'"; + } + return Sk.builtinFiles["files"][filename]; + } + }); +}; + BlockMirror.prototype.addChangeListener = function (callback) { this.listeners_.push(callback); -} +}; BlockMirror.prototype.fireChangeListener = function (event) { - for (var i = 0, func; func = this.listeners_[i]; i++) { + for (let i = 0, func; func = this.listeners_[i]; i++) { func(event); } }; BlockMirror.prototype.setCode = function (code, quietly) { - this.model_ = code; + this.code_ = code; if (!quietly) { this.blockEditor.setCode(code, true); this.textEditor.setCode(code, true); @@ -286,22 +307,5913 @@ BlockMirror.prototype.setCode = function (code, quietly) { }; BlockMirror.prototype.getCode = function () { - return this.model_; + return this.code_; +}; + +BlockMirror.prototype.getMode = function () { + return this.mode_; }; BlockMirror.prototype.setMode = function (mode) { + this.mode_ = mode; this.blockEditor.setMode(mode); this.textEditor.setMode(mode); }; -BlockMirror.prototype.setFilename = function (filename) { -}; - BlockMirror.prototype.setReadOnly = function (isReadOnly) { this.textEditor.setReadOnly(isReadOnly); this.blockEditor.setReadOnly(isReadOnly); this.configuration.container.toggleClass("block-mirror-read-only", isReadOnly); }; +BlockMirror.prototype.VISIBLE_MODES = { + 'block': ['block', 'split'], + 'text': ['text', 'split'] +}; + +exports = BlockMirror; +function BlockMirrorTextEditor(blockMirror) { + this.blockMirror = blockMirror; + this.textContainer = blockMirror.tags.textContainer; + this.textArea = blockMirror.tags.textArea; + this.textSidebar = blockMirror.tags.textSidebar; + + // notification + this.silentEvents_ = false; + + // Do we need to force an update? + this.outOfDate_ = null; + + // Use a timer to swallow updates + this.updateTimer_ = null; + + let codeMirrorOptions = { + mode: { + name: 'python', + version: 3, + singleLineStringErrors: false + }, + readOnly: blockMirror.configuration.readOnly, + showCursorWhenSelecting: true, + lineNumbers: true, + firstLineNumber: 1, + indentUnit: 4, + tabSize: 4, + indentWithTabs: false, + matchBrackets: true, + extraKeys: { + 'Tab': 'indentMore', + 'Shift-Tab': 'indentLess', + 'Ctrl-Enter': blockMirror.run, + 'Esc': function(cm) { + if (cm.getOption("fullScreen")) { + cm.setOption("fullScreen", false); + } else { + cm.display.input.blur(); + } + }, + "F11": function(cm) { + cm.setOption("fullScreen", !cm.getOption("fullScreen")); + }, + "Esc": function(cm) { + + } + }, + foldGutter: true, + gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"] + }; + this.codeMirror = CodeMirror.fromTextArea(this.textArea, codeMirrorOptions); + this.codeMirror.on('change', this.changed.bind(this)); + this.codeMirror.setSize(null, '100%'); + this.textContainer.style.border = '1px solid lightgray'; + this.textContainer.style.float = 'left'; + this.updateWidth(); + this.textContainer.style.height = blockMirror.configuration.height; + // Style sidebar + this.textSidebar.style.height = '100%'; + this.textSidebar.style.float = 'left'; + this.textSidebar.style.backgroundColor = '#ddd'; + + // TODO: Finish implementing code completion + /*this.codeMirror.on('inputRead', function onChange(editor, input) { + if (input.text[0] === ';' || input.text[0] === ' ' || input.text[0] === ":") { + return; + } + editor.showHint({ + hint: CodeMirror.pythonHint + }); + });*/ +} + +BlockMirrorTextEditor.prototype.defocus = function () { + this.codeMirror.display.input.blur(); +} + +BlockMirrorTextEditor.prototype.updateWidth = function () { + var newWidth = '0%'; + /*if (this.blockMirror.views.includes('text')) { + newWidth = (100 / this.blockMirror.views.length)+'%'; + } + this.textContainer.style.width = newWidth;*/ +} + +BlockMirrorTextEditor.prototype.setReadOnly = function (isReadOnly) { + this.codeMirror.setOption('readOnly', isReadOnly); +}; + +BlockMirrorTextEditor.prototype.VIEW_CONFIGURATIONS = { + 'split': { + 'width': '40%', + 'visible': true, + 'indentSidebar': false + }, + 'text': { + 'width': '100%', + 'visible': true, + 'indentSidebar': true + }, + 'block': { + 'width': '0%', + 'visible': false, + 'indentSidebar': false + } +}; -var exports = BlockMirror; \ No newline at end of file +BlockMirrorTextEditor.prototype.setMode = function (mode) { + mode = mode.toLowerCase(); + let configuration = this.VIEW_CONFIGURATIONS[mode]; + // If there is an update waiting and we're visible, then update + if (this.outOfDate_ !== null && this.isVisible()) { + this.setCode(this.outOfDate_, true); + } + // Show/hide editor + this.textContainer.style.width = configuration.width; + if (configuration.visible) { + this.textContainer.style.height = this.blockMirror.configuration.height; + this.textContainer.style.display = 'block'; + this.codeMirror.getWrapperElement().style.display = 'block'; + this.codeMirror.refresh(); + } else { + this.textContainer.style.height = '0%'; + this.textContainer.style.display = 'none'; + this.codeMirror.getWrapperElement().style.display = 'none'; + } + // Should we indent the toolbox + if (configuration.indentSidebar) { + let gutters = this.textContainer.querySelector('.CodeMirror-gutters'); + let gutterWidth = gutters.offsetWidth; + let toolbarWidth = this.blockMirror.blockEditor.getToolbarWidth(); + let newGutterWidth = toolbarWidth - gutterWidth - 2; + this.textSidebar.style.width = newGutterWidth + 'px'; + this.textSidebar.style.display = 'block'; + } else { + this.textSidebar.style.display = 'none'; + this.textSidebar.style.width = '0px'; + } +} + +BlockMirrorTextEditor.prototype.setCode = function (code, quietly) { + this.silentEvents_ = quietly; + + // Defaults to a single blank line + code = (code === undefined || code.trim() === "") ? "\n" : code; + + if (this.isVisible()) { + this.codeMirror.setValue(code); + this.outOfDate_ = null; + } else { + this.outOfDate_ = code; + } +}; + +BlockMirrorTextEditor.prototype.getCode = function () { + return this.codeMirror.getValue(); +}; + +BlockMirrorTextEditor.prototype.changed = function (codeMirror, event) { + if (!this.silentEvents_) { + let handleChange = () => { + let newCode = this.getCode(); + this.blockMirror.blockEditor.setCode(newCode, true); + this.blockMirror.code_ = newCode; + }; + if (this.blockMirror.configuration.blockDelay === false) { + handleChange(); + } else { + if (this.updateTimer_ !== null) { + clearTimeout(this.updateTimer_); + } + this.updateTimer_ = setTimeout(handleChange, this.blockMirror.configuration.blockDelay); + } + } + this.silentEvents_ = false; +}; + +BlockMirrorTextEditor.prototype.isVisible = function () { + return this.blockMirror.VISIBLE_MODES.text.indexOf(this.blockMirror.mode_) !== -1; +}; +/** + * Worth noting - Blockly uses a setTimeOut of 0 steps to make events + * wait. That has some confusing interaction with trying to make things percolate. + * @param blockMirror + * @constructor + */ + +function BlockMirrorBlockEditor(blockMirror) { + this.blockMirror = blockMirror; + this.blockContainer = blockMirror.tags.blockContainer; + this.blockEditor = blockMirror.tags.blockEditor; + this.blockArea = blockMirror.tags.blockArea; + + // Null, or the source of the last update + this.outOfDate_ = null; + + // Inject Blockly + let blocklyOptions = { + media: blockMirror.configuration.blocklyMediaPath, + // We use special comment blocks + zoom: {controls: true}, + comments: false, + disable: false, + oneBasedIndex: false, + readOnly: blockMirror.configuration.readOnly, + scrollbars: true, + toolbox: this.makeToolbox() + } + this.workspace = Blockly.inject(blockMirror.tags.blockEditor, + blocklyOptions); + // Configure Blockly + this.workspace.addChangeListener(this.changed.bind(this)); + + // Configure Blockly DIV + //blockMirror.tags.blockEditor.style.resize = 'horizontal'; + this.blockContainer.style.float = 'left'; + this.blockEditor.style.position = 'absolute'; + this.blockEditor.style.width = '100%'; + this.blockArea.style.height = blockMirror.configuration.height; + + window.addEventListener('resize', this.resized.bind(this), false); + this.resized(); +} + +BlockMirrorBlockEditor.prototype.updateWidth = function () { + var newWidth = '0%'; + this.resized(); +} + +BlockMirrorBlockEditor.prototype.resized = function (e) { + // Compute the absolute coordinates and dimensions of blocklyArea. + var blockArea = this.blockMirror.tags.blockArea; + var current = blockArea; + var x = 0; + var y = 0; + do { + x += current.offsetLeft; + y += current.offsetTop; + current = current.offsetParent; + } while (current); + // Position blocklyDiv over blockArea. + var blockEditor = this.blockMirror.tags.blockEditor; + blockEditor.style.left = x + 'px'; + blockEditor.style.top = y + 'px'; + blockEditor.style.width = blockArea.offsetWidth + 'px'; + blockEditor.style.height = blockArea.offsetHeight + 'px'; + Blockly.svgResize(this.workspace); +} + +BlockMirrorBlockEditor.prototype.makeToolbox = function () { + var xml = ''; + return xml; +}; + +BlockMirrorBlockEditor.prototype.remakeToolbox = function () { + this.workspace.updateToolbox(this.makeToolbox()); + this.resized(); +}; + +/** + * Retrieves the current width of the Blockly Toolbox, unless + * we're in read-only mode (when there is no toolbox). + * @returns {Number} The current width of the toolbox. + */ +BlockMirrorBlockEditor.prototype.getToolbarWidth = function () { + if (this.blockMirror.configuration.readOnly) { + return 0; + } else { + return this.workspace.toolbox_.width; + } +} + +BlockMirrorBlockEditor.prototype.VIEW_CONFIGURATIONS = { + 'split': { + 'width': '60%', + 'visible': true + }, + 'block': { + 'width': '100%', + 'visible': true + }, + 'text': { + 'width': '0%', + 'visible': false + } +} + +BlockMirrorBlockEditor.prototype.setMode = function (mode) { + mode = mode.toLowerCase(); + let configuration = this.VIEW_CONFIGURATIONS[mode]; + + // Show/hide editor + this.blockContainer.style.width = configuration.width; + this.workspace.setVisible(configuration.visible); + if (configuration.visible) { + this.blockContainer.style.height = this.blockMirror.configuration.height; + this.blockEditor.style.width = '100%'; + this.resized(); + } else { + this.blockContainer.style.height = '0%'; + } + + // If there is an update waiting and we're visible, then update + if (this.outOfDate_ !== null && this.isVisible()) { + this.setCode(this.outOfDate_, true); + } +}; + +/** + * Attempts to update the model for the current code file from the + * block workspace. Might be prevented if an update event was already + * percolating. + */ +BlockMirrorBlockEditor.prototype.getCode = function () { + return Blockly.Python.workspaceToCode(this.workspace); +}; + +/** + * Attempts to update the model for the current code file from the + * block workspace. Might be prevented if an update event was already + * percolating. + */ +BlockMirrorBlockEditor.prototype.setCode = function (code, quietly) { + if (this.isVisible()) { + let result = this.blockMirror.textToBlocks.convertSource('__main__.py', code); + if (quietly) { + Blockly.Events.disable(); + } + try { + let xml_code = Blockly.Xml.textToDom(result.xml); + this.workspace.clear(); + Blockly.Xml.domToWorkspace(xml_code, this.workspace); + this.workspace.cleanUp(); + } catch (error) { + console.error(error); + } + if (quietly) { + Blockly.Events.enable(); + } + this.outOfDate_ = null; + } else { + this.outOfDate_ = code; + } +} + +BlockMirrorBlockEditor.prototype.BLOCKLY_CHANGE_EVENTS = [ + Blockly.Events.CREATE, Blockly.Events.DELETE, Blockly.Events.CHANGE, Blockly.Events.MOVE +]; + +BlockMirrorBlockEditor.prototype.changed = function (event) { + if ((event === undefined || this.BLOCKLY_CHANGE_EVENTS.indexOf(event.type) !== -1) && + !this.workspace.isDragging()) { + let newCode = this.getCode(); + this.blockMirror.textEditor.setCode(newCode, true); + this.blockMirror.code_ = newCode; + } +}; + +BlockMirrorBlockEditor.prototype.isVisible = function () { + return this.blockMirror.VISIBLE_MODES.block.indexOf(this.blockMirror.mode_) !== -1; +}; +function BlockMirrorTextToBlocks(blockMirror) { + this.blockMirror = blockMirror; + this.strictAnnotations = ['int', 'float', 'str', 'bool']; + Blockly.defineBlocksWithJsonArray(BlockMirrorTextToBlocks.BLOCKS); +} + +BlockMirrorTextToBlocks.xmlToString = function (xml) { + return new XMLSerializer().serializeToString(xml); +} + +BlockMirrorTextToBlocks.prototype.convertSourceToCodeBlock = function (python_source) { + var xml = document.createElement("xml"); + xml.appendChild(BlockMirrorTextToBlocks.raw_block(python_source)); + return BlockMirrorTextToBlocks.xmlToString(xml); +} + +/** + * The main function for converting a string representation of Python + * code to the Blockly XML representation. + * + * @param {string} filename - The filename being parsed. + * @param {string} python_source - The string representation of Python + * code (e.g., "a = 0"). + * @returns {Object} An object which will either have the converted + * source code or an error message and the code as a code-block. + */ +BlockMirrorTextToBlocks.prototype.convertSource = function (filename, python_source) { + var xml = document.createElement("xml"); + // Attempt parsing - might fail! + var parse, ast = null, symbol_table, error; + let badChunks = []; + let originalSource = python_source; + this.source = python_source.split("\n"); + let previousLine = 1+this.source.length; + while (ast === null) { + if (python_source.trim() === "") { + return {"xml": BlockMirrorTextToBlocks.xmlToString(xml), "error": null}; + } + try { + parse = Sk.parse(filename, python_source); + ast = Sk.astFromParse(parse.cst, filename, parse.flags); + } catch (e) { + error = e; + if (e.traceback && e.traceback.length && e.traceback[0].lineno && + e.traceback[0].lineno < previousLine) { + previousLine = e.traceback[0].lineno - 1; + badChunks = badChunks.concat(this.source.slice(previousLine)); + this.source = this.source.slice(0, previousLine); + python_source = this.source.join("\n"); + } else { + xml.appendChild(BlockMirrorTextToBlocks.raw_block(originalSource)); + return {"xml": BlockMirrorTextToBlocks.xmlToString(xml), "error": error}; + } + } + } + this.comments = {}; + for (var commentLocation in parse.comments) { + var lineColumn = commentLocation.split(","); + var yLocation = parseInt(lineColumn[0], 10); + this.comments[yLocation] = parse.comments[commentLocation]; + } + this.highestLineSeen = 0; + this.levelIndex = 0; + this.nextExpectedLine = 0; + this.measureNode(ast); + var converted = this.convert(ast); + if (converted !== null) { + for (var block = 0; block < converted.length; block += 1) { + xml.appendChild(converted[block]); + } + } + if (badChunks.length) { + xml.appendChild(BlockMirrorTextToBlocks.raw_block(badChunks.join("\n"))); + } + return { + "xml": BlockMirrorTextToBlocks.xmlToString(xml), "error": null, + "lineMap": this.lineMap, 'comments': this.comments + }; +} + +BlockMirrorTextToBlocks.prototype.recursiveMeasure = function (node, nextBlockLine) { + if (node === undefined) { + return; + } + var myNext = nextBlockLine; + if ("orelse" in node && node.orelse.length > 0) { + if (node.orelse.length == 1 && node.orelse[0]._astname == "If") { + myNext = node.orelse[0].lineno - 1; + } else { + myNext = node.orelse[0].lineno - 1 - 1; + } + } + this.heights.push(nextBlockLine); + if ("body" in node) { + for (var i = 0; i < node.body.length; i++) { + var next; + if (i + 1 == node.body.length) { + next = myNext; + } else { + next = node.body[i + 1].lineno - 1; + } + this.recursiveMeasure(node.body[i], next); + } + } + if ("orelse" in node) { + for (var i = 0; i < node.orelse.length; i++) { + var next; + if (i == node.orelse.length) { + next = nextBlockLine; + } else { + next = 1 + (node.orelse[i].lineno - 1); + } + this.recursiveMeasure(node.orelse[i], next); + } + } +} + +BlockMirrorTextToBlocks.prototype.measureNode = function (node) { + this.heights = []; + this.recursiveMeasure(node, this.source.length - 1); + this.heights.shift(); +} + +BlockMirrorTextToBlocks.prototype.getSourceCode = function (frm, to) { + var lines = this.source.slice(frm - 1, to); + // Strip out any starting indentation. + if (lines.length > 0) { + var indentation = lines[0].search(/\S/); + for (var i = 0; i < lines.length; i++) { + lines[i] = lines[i].substring(indentation); + } + } + return lines.join("\n"); +} + +BlockMirrorTextToBlocks.prototype.convertBody = function (node, parent) { + this.levelIndex += 1; + let is_top_level = this.isTopLevel(parent); + // Empty body, return nothing + /*if (node.length === 0) { + return null; + }*/ + + // Final result list + var children = [], // The complete set of peers + root = null, // The top of the current peer + current = null, // The bottom of the current peer + levelIndex = this.levelIndex; + + function addPeer(peer) { + if (root == null) { + children.push(peer); + } else { + children.push(root); + } + root = peer; + current = peer; + } + + function finalizePeers() { + if (root != null) { + children.push(root); + } + } + + function nestChild(child) { + if (root == null) { + root = child; + current = child; + } else if (current == null) { + root = current; + } else { + var nextElement = document.createElement("next"); + nextElement.appendChild(child); + current.appendChild(nextElement); + current = child; + } + } + + var lineNumberInBody = 0, + lineNumberInProgram, + previousLineInProgram = null, + distance, + skipped_line, + commentCount, + previousHeight = null, + previousWasStatement = false; + visitedFirstLine = false; + + // Iterate through each node + for (var i = 0; i < node.length; i++) { + lineNumberInBody += 1; + + lineNumberInProgram = node[i].lineno; + distance = 0, wasFirstLine = true; + if (previousLineInProgram != null) { + distance = lineNumberInProgram - previousLineInProgram - 1; + wasFirstLine = false; + } + lineNumberInBody += distance; + + // Handle earlier comments + commentCount = 0; + for (var commentLineInProgram in this.comments) { + if (commentLineInProgram < lineNumberInProgram) { + commentChild = this.ast_Comment(this.comments[commentLineInProgram], commentLineInProgram); + if (previousLineInProgram == null) { + nestChild(commentChild); + } else { + skipped_previous_line = Math.abs(previousLineInProgram - commentLineInProgram) > 1; + if (is_top_level && skipped_previous_line) { + addPeer(commentChild); + } else { + nestChild(commentChild); + } + } + previousLineInProgram = commentLineInProgram; + this.highestLineSeen = Math.max(this.highestLineSeen, parseInt(commentLineInProgram, 10)); + distance = lineNumberInProgram - previousLineInProgram; + delete this.comments[commentLineInProgram]; + commentCount += 1; + } + } + + distance = lineNumberInProgram - this.highestLineSeen; + this.highestLineSeen = Math.max(lineNumberInProgram, this.highestLineSeen); + + // Now convert the actual node + var height = this.heights.shift(); + var originalSourceCode = this.getSourceCode(lineNumberInProgram, height); + var newChild = this.convertStatement(node[i], originalSourceCode, parent); + + // Skip null blocks (e.g., imports) + if (newChild == null) { + continue; + } + + skipped_line = distance > 1; + previousLineInProgram = lineNumberInProgram; + previousHeight = height; + + // Handle top-level expression blocks + if (is_top_level && newChild.constructor == Array) { + addPeer(newChild[0]); + // Handle skipped line + } else if (is_top_level && skipped_line && visitedFirstLine) { + addPeer(newChild); + // The previous line was not a Peer + } else if (is_top_level && !previousWasStatement) { + addPeer(newChild); + // Otherwise, always embed it in there. + } else { + nestChild(newChild); + } + previousWasStatement = newChild.constructor !== Array; + + visitedFirstLine = true; + } + + + // Handle comments that are on the very last line + var lastLineNumber = lineNumberInProgram + 1; + if (lastLineNumber in this.comments) { + commentChild = this.ast_Comment(this.comments[lastLineNumber], lastLineNumber); + nestChild(commentChild); + delete this.comments[lastLineNumber]; + } + + // Handle any extra comments that stuck around + if (is_top_level) { + for (var commentLineInProgram in this.comments) { + commentChild = this.ast_Comment(this.comments[commentLineInProgram], commentLineInProgram); + distance = commentLineInProgram - previousLineInProgram; + if (previousLineInProgram == null) { + addPeer(commentChild); + } else if (distance > 1) { + addPeer(commentChild); + } else { + nestChild(commentChild); + } + previousLineInProgram = commentLineInProgram; + delete this.comments[lastLineNumber]; + } + } + + + finalizePeers(); + + this.levelIndex -= 1; + + return children; +}; + +BlockMirrorTextToBlocks.prototype.TOP_LEVEL_NODES = ['Module', 'Expression', 'Interactive', 'Suite']; + +BlockMirrorTextToBlocks.prototype.isTopLevel = function (parent) { + + return !parent || this.TOP_LEVEL_NODES.indexOf(parent._astname) !== -1; +}; + +BlockMirrorTextToBlocks.prototype.convert = function (node, parent) { + let functionName = 'ast_' + node._astname; + if (this[functionName] === undefined) { + throw new Error("Could not find function: " + functionName); + } + node._parent = parent; + return this[functionName](node, parent); +}; + +function arrayMax(array) { + return array.reduce(function (a, b) { + return Math.max(a, b); + }); +} + +function arrayMin(array) { + return array.reduce(function (a, b) { + return Math.min(a, b); + }); +} + +BlockMirrorTextToBlocks.prototype.convertStatement = function (node, full_source, parent) { + try { + return this.convert(node, parent); + } catch (e) { + heights = this.getChunkHeights(node); + extractedSource = this.getSourceCode(arrayMin(heights), arrayMax(heights)); + console.error(e); + return BlockMirrorTextToBlocks.raw_block(extractedSource); + } +} + +BlockMirrorTextToBlocks.prototype.getChunkHeights = function (node) { + var lineNumbers = []; + if (node.hasOwnProperty("lineno")) { + lineNumbers.push(node.lineno); + } + if (node.hasOwnProperty("body")) { + for (var i = 0; i < node.body.length; i += 1) { + var subnode = node.body[i]; + lineNumbers = lineNumbers.concat(this.getChunkHeights(subnode)); + } + } + if (node.hasOwnProperty("orelse")) { + for (var i = 0; i < node.orelse.length; i += 1) { + var subnode = node.orelse[i]; + lineNumbers = lineNumbers.concat(this.getChunkHeights(subnode)); + } + } + return lineNumbers; +} + +BlockMirrorTextToBlocks.create_block = function (type, lineNumber, fields, values, settings, mutations, statements) { + var newBlock = document.createElement("block"); + // Settings + newBlock.setAttribute("type", type); + newBlock.setAttribute("line_number", lineNumber); + for (var setting in settings) { + var settingValue = settings[setting]; + newBlock.setAttribute(setting, settingValue); + } + // Mutations + if (mutations !== undefined && Object.keys(mutations).length > 0) { + var newMutation = document.createElement("mutation"); + for (let mutation in mutations) { + var mutationValue = mutations[mutation]; + if (mutation.charAt(0) === '@') { + newMutation.setAttribute(mutation.substr(1), mutationValue); + } else if (mutationValue != null && mutationValue.constructor === Array) { + for (var i = 0; i < mutationValue.length; i++) { + let mutationNode = document.createElement(mutation); + mutationNode.setAttribute("name", mutationValue[i]); + newMutation.appendChild(mutationNode); + } + } else { + let mutationNode = document.createElement("arg"); + if (mutation.charAt(0) === '!') { + mutationNode.setAttribute("name", ""); + } else { + mutationNode.setAttribute("name", mutation); + } + if (mutationValue !== null) { + mutationNode.appendChild(mutationValue); + } + newMutation.appendChild(mutationNode); + } + } + newBlock.appendChild(newMutation); + } + // Fields + for (var field in fields) { + var fieldValue = fields[field]; + var newField = document.createElement("field"); + newField.setAttribute("name", field); + newField.appendChild(document.createTextNode(fieldValue)); + newBlock.appendChild(newField); + } + // Values + for (var value in values) { + var valueValue = values[value]; + var newValue = document.createElement("value"); + if (valueValue !== null) { + newValue.setAttribute("name", value); + newValue.appendChild(valueValue); + newBlock.appendChild(newValue); + } + } + // Statements + if (statements !== undefined && Object.keys(statements).length > 0) { + for (var statement in statements) { + var statementValue = statements[statement]; + if (statementValue == null) { + continue; + } else { + for (var i = 0; i < statementValue.length; i += 1) { + // In most cases, you really shouldn't ever have more than + // one statement in this list. I'm not sure Blockly likes + // that. + var newStatement = document.createElement("statement"); + newStatement.setAttribute("name", statement); + newStatement.appendChild(statementValue[i]); + newBlock.appendChild(newStatement); + } + } + } + } + return newBlock; +} + +BlockMirrorTextToBlocks.raw_block = function (txt) { + // TODO: lineno as second parameter! + return BlockMirrorTextToBlocks.create_block("ast_Raw", 0, {"TEXT": txt}); +}; + +BlockMirrorTextToBlocks.BLOCKS = []; + +BlockMirrorTextToBlocks.prototype['ast_Module'] = function (node) { + return this.convertBody(node.body, node); +}; + +BlockMirrorTextToBlocks.prototype['ast_Interactive'] = function (node) { + return this.convertBody(node.body, node); +}; + +BlockMirrorTextToBlocks.prototype['ast_Expression'] = BlockMirrorTextToBlocks.prototype['ast_Interactive']; +BlockMirrorTextToBlocks.prototype['ast_Suite'] = BlockMirrorTextToBlocks.prototype['ast_Module']; + +BlockMirrorTextToBlocks.prototype['ast_Pass'] = function () { + return null; //block("controls_pass"); +}; + + +BlockMirrorTextToBlocks.prototype.convertElements = function (key, values, parent) { + var output = {}; + for (var i = 0; i < values.length; i++) { + output[key + i] = this.convert(values[i], parent); + } + return output; +}; + +Blockly.Python['blank'] = '___'; + +BlockMirrorTextToBlocks.prototype.LOCKED_BLOCK = { + "inline": "true", + 'deletable': "false", + "movable": "false" +}; + +BlockMirrorTextToBlocks.COLOR = { + VARIABLES: 225, + FUNCTIONS: 210, + OO: 240, + CONTROL: 270, + MATH: 150, + TEXT: 120, + FILE: 125, + PLOTTING: 135, + LOGIC: 345, + PYTHON: 60, + EXCEPTIONS: 300, + SEQUENCES: 15, + LIST: 30, + DICTIONARY: 0, + SET: 10, + TUPLE: 20 +} +BlockMirrorTextToBlocks.prototype.FUNCTION_SIGNATURES = { + 'abs': { + 'returns': true, + 'full': ['x'], colour: BlockMirrorTextToBlocks.COLOR.MATH + }, + 'all': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LOGIC}, + 'any': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LOGIC}, + 'ascii': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'bin': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'bool': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LOGIC}, + 'breakpoint': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'bytearray': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'bytes': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'callable': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LOGIC}, + 'chr': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'classmethod': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, + 'compile': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'complex': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'delattr': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.VARIABLES}, + 'dict': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.DICTIONARY}, + 'dir': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'divmod': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'enumerate': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'eval': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'exec': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'filter': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'float': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'format': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'frozenset': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'getattr': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, + 'globals': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.VARIABLES}, + 'hasattr': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, + 'hash': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'help': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'hex': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'id': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'input': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'int': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'isinstance': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LOGIC}, + 'issubclass': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LOGIC}, + 'iter': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'len': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'list': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LIST}, + 'locals': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.VARIABLES}, + 'map': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'max': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'memoryview': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'min': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'next': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'object': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, + 'oct': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'open': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.FILE}, + 'ord': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'pow': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'print': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'property': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, + 'range': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'repr': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'reversed': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'round': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'set': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'setattr': { + 'returns': false, + 'full': ['object', 'name', 'value'], colour: BlockMirrorTextToBlocks.COLOR.OO + }, + 'slice': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'sorted': { + 'full': ['iterable', '*', '**key', '**reverse'], + 'simple': ['iterable'], + 'returns': true, + colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES + }, + 'staticmethod': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, + 'str': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'sum': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'super': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, + 'tuple': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TUPLE}, + 'type': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, + 'vars': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.VARIABLES}, + 'zip': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + '__import__': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.PYTHON} +}; + +BlockMirrorTextToBlocks.prototype.METHOD_SIGNATURES = { + 'conjugate': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'trunc': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'floor': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'ceil': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'bit_length': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'to_bytes': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'from_bytes': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'as_integer_ratio': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'is_integer': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'hex': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + 'fromhex': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.MATH}, + '__iter__': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + '__next__': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'index': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LIST}, + 'count': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LIST}, + 'append': { + 'returns': false, + 'full': ['x'], + 'message': 'append', + 'premessage': 'to list', colour: BlockMirrorTextToBlocks.COLOR.LIST + }, + 'clear': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'copy': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LIST}, + 'extend': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.LIST}, + 'insert': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.LIST}, + 'pop': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'remove': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'reverse': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.LIST}, + 'sort': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.LIST}, + 'capitalize': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'casefold': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'center': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'encode': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'endswith': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'expandtabs': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'find': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'format': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'format_map': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isalnum': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isalpha': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isascii': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isdecimal': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isdigit': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isidentifier': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'islower': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isnumeric': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isprintable': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isspace': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'istitle': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'isupper': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'join': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'ljust': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'lower': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'lstrip': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'maketrans': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'partition': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'replace': { + 'returns': true, + 'full': ['old', 'new', 'count'], + 'simple': ['old', 'new'], colour: BlockMirrorTextToBlocks.COLOR.TEXT + }, + 'rfind': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'rindex': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'rjust': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'rpartition': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'rsplit': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'rstrip': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'split': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'splitlines': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'startswith': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'strip': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'swapcase': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'title': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'translate': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'upper': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'zfill': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + 'decode': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.TEXT}, + '__eq__': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.LOGIC}, + 'tobytes': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'tolist': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'release': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'cast': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.PYTHON}, + 'isdisjoint': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'issubset': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'issuperset': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'union': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'intersection': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'difference': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'symmetric_difference': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'update': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'intersection_update': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'difference_update': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'symmetric_difference_update': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'add': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'discard': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.SET}, + 'fromkeys': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.DICTIONARY}, + 'get': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.DICTIONARY}, + 'items': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.DICTIONARY}, + 'keys': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.DICTIONARY}, + 'popitem': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.DICTIONARY}, + 'setdefault': {returns: false, colour: BlockMirrorTextToBlocks.COLOR.DICTIONARY}, + 'values': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.DICTIONARY}, + '__enter__': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.CONTROL}, + '__exit__': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.CONTROL}, + 'mro': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, + '__subclasses__': {returns: true, colour: BlockMirrorTextToBlocks.COLOR.OO}, +}; + +BlockMirrorTextToBlocks.prototype.MODULE_FUNCTION_SIGNATURES = { + 'plt': { + 'show': { + returns: false, + message: 'show plot canvas', + colour: BlockMirrorTextToBlocks.COLOR.PLOTTING + }, + 'hist': { + returns: false, + message: 'plot histogram', + colour: BlockMirrorTextToBlocks.COLOR.PLOTTING + }, + 'plot': { + returns: false, + message: 'plot line', + colour: BlockMirrorTextToBlocks.COLOR.PLOTTING + }, + 'scatter': { + returns: false, + message: 'plot scatter', + colour: BlockMirrorTextToBlocks.COLOR.PLOTTING + }, + 'title': { + returns: false, + message: "make plot's title", + colour: BlockMirrorTextToBlocks.COLOR.PLOTTING + }, + 'xlabel': { + returns: false, + message: "make plot's x-axis label", + colour: BlockMirrorTextToBlocks.COLOR.PLOTTING + }, + 'ylabel': { + returns: false, + message: "make plot's y-axis label", + colour: BlockMirrorTextToBlocks.COLOR.PLOTTING + } + } +}; + +BlockMirrorTextToBlocks.prototype.MODULE_FUNCTION_SIGNATURES['matplotlib.pyplot'] = + BlockMirrorTextToBlocks.prototype.MODULE_FUNCTION_SIGNATURES['plt']; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_For", + "message0": "for each item %1 in list %2 : %3 %4", + "args0": [ + { "type": "input_value", "name": "TARGET" }, + { "type": "input_value", "name": "ITER" }, + { "type": "input_dummy" }, + { "type": "input_statement", "name": "BODY" } + ], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.CONTROL, +}) + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_ForElse", + "message0": "for each item %1 in list %2 : %3 %4 else: %5 %6", + "args0": [ + { "type": "input_value", "name": "TARGET" }, + { "type": "input_value", "name": "ITER" }, + { "type": "input_dummy" }, + { "type": "input_statement", "name": "BODY" }, + { "type": "input_dummy" }, + { "type": "input_statement", "name": "ELSE" } + ], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.CONTROL, +}) + +Blockly.Python['ast_For'] = function(block) { + // For each loop. + var argument0 = Blockly.Python.valueToCode(block, 'TARGET', + Blockly.Python.ORDER_RELATIONAL) || Blockly.Python.blank; + var argument1 = Blockly.Python.valueToCode(block, 'ITER', + Blockly.Python.ORDER_RELATIONAL) || Blockly.Python.blank; + var branchBody = Blockly.Python.statementToCode(block, 'BODY') || Blockly.Python.PASS; + var branchElse = Blockly.Python.statementToCode(block, 'ELSE'); + var code = 'for ' + argument0 + ' in ' + argument1 + ':\n' + branchBody; + if (branchElse) { + code += 'else:\n' + branchElse; + } + return code; +}; + +BlockMirrorTextToBlocks.prototype['ast_For'] = function (node, parent) { + var target = node.target; + var iter = node.iter; + var body = node.body; + var orelse = node.orelse; + + var blockName = 'ast_For'; + var bodies = {'BODY': this.convertBody(body, node)}; + + if (orelse.length > 0) { + blockName = "ast_ForElse"; + bodies['ELSE'] = this.convertBody(orelse, node); + } + + return BlockMirrorTextToBlocks.create_block(blockName, node.lineno, { + }, { + "ITER": this.convert(iter, node), + "TARGET": this.convert(target, node) + }, {}, {}, bodies); +} + +Blockly.Python['ast_ForElse'] = Blockly.Python['ast_For']; +BlockMirrorTextToBlocks.prototype['ast_ForElse'] = BlockMirrorTextToBlocks.prototype['ast_For']; + +Blockly.Blocks['ast_If'] = { + init: function () { + this.orelse_ = 0; + this.elifs_ = 0; + this.appendValueInput('TEST') + .appendField("if"); + this.appendStatementInput("BODY") + .setCheck(null) + .setAlign(Blockly.ALIGN_RIGHT); + this.setInputsInline(false); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.LOGIC); + this.updateShape_(); + }, + // TODO: Not mutable currently + updateShape_: function () { + let latestInput = "BODY"; + for (var i = 0; i < this.elifs_; i++) { + if (!this.getInput('ELIF' + i)) { + this.appendValueInput('ELIFTEST' + i) + .appendField('elif'); + this.appendStatementInput("ELIFBODY" + i) + .setCheck(null); + } + } + // Remove deleted inputs. + while (this.getInput('ELIFTEST' + i)) { + this.removeInput('ELIFTEST' + i); + this.removeInput('ELIFBODY' + i); + i++; + } + + if (this.orelse_ && !this.getInput('ELSE')) { + this.appendDummyInput('ORELSETEST') + .appendField("else:"); + this.appendStatementInput("ORELSEBODY") + .setCheck(null); + } else if (!this.orelse_ && this.getInput('ELSE')) { + block.removeInput('ORELSETEST'); + block.removeInput('ORELSEBODY'); + } + + for (i = 0; i < this.elifs_; i++) { + if (this.orelse_) { + this.moveInputBefore('ELIFTEST' + i, 'ORELSETEST'); + this.moveInputBefore('ELIFBODY' + i, 'ORELSETEST'); + } else if (i+1 < this.elifs_) { + this.moveInputBefore('ELIFTEST' + i, 'ELIFTEST' + (i+1)); + this.moveInputBefore('ELIFBODY' + i, 'ELIFBODY' + (i+1)); + } + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + let container = document.createElement('mutation'); + container.setAttribute('orelse', this.orelse_); + container.setAttribute('elifs', this.elifs_); + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.orelse_ = "true" === xmlElement.getAttribute('orelse'); + this.elifs_ = parseInt(xmlElement.getAttribute('elifs'), 10); + this.updateShape_(); + }, +}; + +Blockly.Python['ast_If'] = function (block) { + // Test + let test = "if " + (Blockly.Python.valueToCode(block, 'TEST', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank) + ":\n"; + // Body: + let body = Blockly.Python.statementToCode(block, 'BODY') || Blockly.Python.PASS; + // Elifs + + let elifs = new Array(block.elifs_); + for (let i = 0; i < block.elifs_; i++) { + let elif = block.elifs_[i]; + let clause = "elif " + (Blockly.Python.valueToCode(block, 'ELIFTEST' + i, + Blockly.Python.ORDER_NONE) || Blockly.Python.blank); + clause += ":\n" + (Blockly.Python.statementToCode(block, 'ELIFBODY' + i) || Blockly.Python.PASS); + elifs[i] = clause; + } + // Orelse: + let orelse = ""; + if (this.orelse_) { + orelse = "else:\n" + (Blockly.Python.statementToCode(block, 'ORELSEBODY') || Blockly.Python.PASS); + } + return test + body + elifs.join("") + orelse; +}; + +BlockMirrorTextToBlocks.prototype['ast_If'] = function (node, parent) { + let test = node.test; + let body = node.body; + let orelse = node.orelse; + + let hasOrelse = false; + let elifCount = 0; + + let values = {"TEST": this.convert(test, node)}; + let statements = {"BODY": this.convertBody(body, node)}; + + while (orelse !== undefined && orelse.length > 0) { + if (orelse.length === 1) { + if (orelse[0]._astname === "If") { + // This is an ELIF + this.heights.shift(); + values['ELIFTEST' + elifCount] = this.convert(orelse[0].test, node); + statements['ELIFBODY' + elifCount] = this.convertBody(orelse[0].body, node); + elifCount++; + } else { + hasOrelse = true; + statements['ORELSEBODY'] = this.convertBody(orelse, node); + } + } else { + hasOrelse = true; + statements['ORELSEBODY'] = this.convertBody(orelse, node); + } + orelse = orelse[0].orelse; + } + + return BlockMirrorTextToBlocks.create_block("ast_If", node.lineno, {}, + values, {}, { + "@orelse": hasOrelse, + "@elifs": elifCount + }, statements); +}; +Blockly.Blocks['ast_While'] = { + init: function () { + this.orelse_ = 0; + this.appendValueInput('TEST') + .appendField("while"); + this.appendStatementInput("BODY") + .setCheck(null) + .setAlign(Blockly.ALIGN_RIGHT); + this.setInputsInline(false); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.CONTROL); + this.updateShape_(); + }, + // TODO: Not mutable currently + updateShape_: function () { + let latestInput = "BODY"; + + if (this.orelse_ && !this.getInput('ELSE')) { + this.appendDummyInput('ORELSETEST') + .appendField("else:"); + this.appendStatementInput("ORELSEBODY") + .setCheck(null); + } else if (!this.orelse_ && this.getInput('ELSE')) { + block.removeInput('ORELSETEST'); + block.removeInput('ORELSEBODY'); + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + let container = document.createElement('mutation'); + container.setAttribute('orelse', this.orelse_); + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.orelse_ = "true" === xmlElement.getAttribute('orelse'); + this.updateShape_(); + }, +}; + +Blockly.Python['ast_While'] = function (block) { + // Test + let test = "while " + (Blockly.Python.valueToCode(block, 'TEST', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank) + ":\n"; + // Body: + let body = Blockly.Python.statementToCode(block, 'BODY') || Blockly.Python.PASS; + // Orelse: + let orelse = ""; + if (this.orelse_) { + orelse = "else:\n" + (Blockly.Python.statementToCode(block, 'ORELSEBODY') || Blockly.Python.PASS); + } + return test + body + orelse; +}; + +BlockMirrorTextToBlocks.prototype['ast_While'] = function (node, parent) { + let test = node.test; + let body = node.body; + let orelse = node.orelse; + + let values = {"TEST": this.convert(test, node)}; + let statements = {"BODY": this.convertBody(body, node)}; + + let hasOrelse = false; + if (orelse !== null && orelse.length > 0) { + statements['ORELSEBODY'] = this.convertBody(orelse, node); + hasOrelse = true; + } + + return BlockMirrorTextToBlocks.create_block("ast_While", node.lineno, {}, + values, {}, { + "@orelse": hasOrelse + }, statements); +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Num", + "message0": "%1", + "args0": [ + { "type": "field_number", "name": "NUM", "value": 0} + ], + "output": "Number", + "colour": BlockMirrorTextToBlocks.COLOR.MATH +}) + +Blockly.Python['ast_Num'] = function(block) { + // Numeric value. + var code = parseFloat(block.getFieldValue('NUM')); + var order; + if (code == Infinity) { + code = 'float("inf")'; + order = Blockly.Python.ORDER_FUNCTION_CALL; + } else if (code == -Infinity) { + code = '-float("inf")'; + order = Blockly.Python.ORDER_UNARY_SIGN; + } else { + order = code < 0 ? Blockly.Python.ORDER_UNARY_SIGN : + Blockly.Python.ORDER_ATOMIC; + } + return [code, order]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Num'] = function (node, parent) { + var n = node.n; + return BlockMirrorTextToBlocks.create_block("ast_Num", node.lineno, { + "NUM": Sk.ffi.remapToJs(n) + }); +} + +BlockMirrorTextToBlocks.BINOPS = [ + ["+", "Add", Blockly.Python.ORDER_ADDITIVE, 'Return the sum of the two numbers.', 'increase', 'by'], + ["-", "Sub", Blockly.Python.ORDER_ADDITIVE, 'Return the difference of the two numbers.', 'decrease', 'by'], + ["*", "Mult", Blockly.Python.ORDER_MULTIPLICATIVE, 'Return the product of the two numbers.', 'multiply', 'by'], + ["/", "Div", Blockly.Python.ORDER_MULTIPLICATIVE, 'Return the quotient of the two numbers.', 'divide', 'by'], + ["%", "Mod", Blockly.Python.ORDER_MULTIPLICATIVE, 'Return the remainder of the first number divided by the second number.', + 'modulo', 'by'], + ["**", "Pow", Blockly.Python.ORDER_EXPONENTIATION, 'Return the first number raised to the power of the second number.', + 'raise', 'to'], + ["//", "FloorDiv", Blockly.Python.ORDER_MULTIPLICATIVE, 'Return the truncated quotient of the two numbers.', + 'floor divide', 'by'], + ["<<", "LShift", Blockly.Python.ORDER_BITWISE_SHIFT, 'Return the left number left shifted by the right number.', + 'left shift', 'by'], + [">>", "RShift", Blockly.Python.ORDER_BITWISE_SHIFT, 'Return the left number right shifted by the right number.', + 'right shift', 'by'], + ["|", "BitOr", Blockly.Python.ORDER_BITWISE_OR, 'Returns the bitwise OR of the two values.', + 'bitwise OR', 'using'], + ["^", "BitXor", Blockly.Python.ORDER_BITWISE_XOR, 'Returns the bitwise XOR of the two values.', + 'bitwise XOR', 'using'], + ["&", "BitAnd", Blockly.Python.ORDER_BITWISE_AND, 'Returns the bitwise AND of the two values.', + 'bitwise AND', 'using'], + ["@", "MatMult", Blockly.Python.ORDER_MULTIPLICATIVE, 'Return the matrix multiplication of the two numbers.', + 'matrix multiply', 'by'] +]; +var BINOPS_SIMPLE = ['Add', 'Sub', 'Mult', 'Div', 'Mod', 'Pow']; +var BINOPS_BLOCKLY_DISPLAY_FULL = BlockMirrorTextToBlocks.BINOPS.map( + binop => [binop[0], binop[1]] +); +var BINOPS_BLOCKLY_DISPLAY = BINOPS_BLOCKLY_DISPLAY_FULL.filter( + binop => BINOPS_SIMPLE.indexOf(binop[1]) >= 0 +); +BlockMirrorTextToBlocks.BINOPS_AUGASSIGN_DISPLAY_FULL =BlockMirrorTextToBlocks.BINOPS.map( + binop => [binop[4], binop[1]] +); +BlockMirrorTextToBlocks.BINOPS_AUGASSIGN_DISPLAY = BlockMirrorTextToBlocks.BINOPS_AUGASSIGN_DISPLAY_FULL.filter( + binop => BINOPS_SIMPLE.indexOf(binop[1]) >= 0 +); + +var BINOPS_BLOCKLY_GENERATE = {}; +BlockMirrorTextToBlocks.BINOPS_AUGASSIGN_PREPOSITION = {}; +BlockMirrorTextToBlocks.BINOPS.forEach(function (binop) { + BINOPS_BLOCKLY_GENERATE[binop[1]] = [" " + binop[0], binop[2]]; + BlockMirrorTextToBlocks.BINOPS_AUGASSIGN_PREPOSITION[binop[1]] = binop[5]; + //Blockly.Constants.Math.TOOLTIPS_BY_OP[binop[1]] = binop[3]; +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_BinOpFull", + "message0": "%1 %2 %3", + "args0": [ + {"type": "input_value", "name": "A"}, + {"type": "field_dropdown", "name": "OP", "options": BINOPS_BLOCKLY_DISPLAY_FULL}, + {"type": "input_value", "name": "B"} + ], + "inputsInline": true, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.MATH + //"extensions": ["math_op_tooltip"] +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_BinOp", + "message0": "%1 %2 %3", + "args0": [ + {"type": "input_value", "name": "A"}, + {"type": "field_dropdown", "name": "OP", "options": BINOPS_BLOCKLY_DISPLAY}, + {"type": "input_value", "name": "B"} + ], + "inputsInline": true, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.MATH + //"extensions": ["math_op_tooltip"] +}); + +Blockly.Python['ast_BinOp'] = function (block) { + // Basic arithmetic operators, and power. + var tuple = BINOPS_BLOCKLY_GENERATE[block.getFieldValue('OP')]; + var operator = tuple[0]+" "; + var order = tuple[1]; + var argument0 = Blockly.Python.valueToCode(block, 'A', order) || Blockly.Python.blank; + var argument1 = Blockly.Python.valueToCode(block, 'B', order) || Blockly.Python.blank; + var code = argument0 + operator + argument1; + return [code, order]; +}; + +BlockMirrorTextToBlocks.prototype['ast_BinOp'] = function (node, parent) { + let left = node.left; + let op = node.op.name; + let right = node.right; + + let blockName = (BINOPS_SIMPLE.indexOf(op) >= 0) ? "ast_BinOp" : 'ast_BinOpFull'; + + return BlockMirrorTextToBlocks.create_block(blockName, node.lineno, { + "OP": op + }, { + "A": this.convert(left, node), + "B": this.convert(right, node) + }, { + "inline": true + }); +} + +Blockly.Python['ast_BinOpFull'] = Blockly.Python['ast_BinOp']; +BlockMirrorTextToBlocks.prototype['ast_BinOpFull'] = BlockMirrorTextToBlocks.prototype['ast_BinOp']; + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Name", + "message0": "%1", + "args0": [ + {"type": "field_variable", "name": "VAR", "variable": "%{BKY_VARIABLES_DEFAULT_NAME}"} + ], + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.VARIABLES, + "extensions": ["contextMenu_variableSetterGetter_forBlockMirror"] +}) + +/** + * Mixin to add context menu items to create getter/setter blocks for this + * setter/getter. + * Used by blocks 'ast_Name' and 'ast_Assign'. + * @mixin + * @augments Blockly.Block + * @package + * @readonly + */ +Blockly.Constants.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN_FOR_BLOCK_MIRROR = { + /** + * Add menu option to create getter/setter block for this setter/getter. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu: function(options) { + let name; + if (!this.isInFlyout){ + // Getter blocks have the option to create a setter block, and vice versa. + let opposite_type, contextMenuMsg; + if (this.type === 'ast_Name') { + opposite_type = 'ast_Assign'; + contextMenuMsg = Blockly.Msg['VARIABLES_GET_CREATE_SET']; + } else { + opposite_type = 'ast_Name'; + contextMenuMsg = Blockly.Msg['VARIABLES_SET_CREATE_GET']; + } + + var option = {enabled: this.workspace.remainingCapacity() > 0}; + name = this.getField('VAR').getText(); + option.text = contextMenuMsg.replace('%1', name); + var xmlField = document.createElement('field'); + xmlField.setAttribute('name', 'VAR'); + xmlField.appendChild(document.createTextNode(name)); + var xmlBlock = document.createElement('block'); + xmlBlock.setAttribute('type', opposite_type); + xmlBlock.appendChild(xmlField); + option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock); + options.push(option); + // Getter blocks have the option to rename or delete that variable. + } else { + if (this.type === 'ast_Name' || this.type === 'variables_get_reporter'){ + var renameOption = { + text: Blockly.Msg.RENAME_VARIABLE, + enabled: true, + callback: Blockly.Constants.Variables.RENAME_OPTION_CALLBACK_FACTORY(this) + }; + name = this.getField('VAR').getText(); + var deleteOption = { + text: Blockly.Msg.DELETE_VARIABLE.replace('%1', name), + enabled: true, + callback: Blockly.Constants.Variables.DELETE_OPTION_CALLBACK_FACTORY(this) + }; + options.unshift(renameOption); + options.unshift(deleteOption); + } + } + } +}; + +Blockly.Extensions.registerMixin('contextMenu_variableSetterGetter_forBlockMirror', + Blockly.Constants.Variables.CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN_FOR_BLOCK_MIRROR); + +Blockly.Python['ast_Name'] = function (block) { + // Variable getter. + var code = Blockly.Python.variableDB_.getName(block.getFieldValue('VAR'), + Blockly.Variables.NAME_TYPE); + return [code, Blockly.Python.ORDER_ATOMIC]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Name'] = function (node, parent) { + var id = node.id; + var ctx = node.ctx; + if (id.v == Blockly.Python.blank) { + return null; + } else { + return BlockMirrorTextToBlocks.create_block('ast_Name', node.lineno, { + "VAR": id.v + }); + } +} + +Blockly.Blocks['ast_Assign'] = { + init: function () { + this.setInputsInline(true); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.VARIABLES); + this.targetCount_ = 1; + this.simpleTarget_ = true; + this.updateShape_(); + Blockly.Extensions.apply("contextMenu_variableSetterGetter", this, false); + }, + updateShape_: function () { + if (!this.getInput('VALUE')) { + this.appendDummyInput() + .appendField("set"); + this.appendValueInput('VALUE') + .appendField('='); + } + let i = 0; + if (this.targetCount_ === 1 && this.simpleTarget_) { + this.setInputsInline(true); + if (!this.getInput('VAR_ANCHOR')) { + this.appendDummyInput('VAR_ANCHOR') + .appendField(new Blockly.FieldVariable("variable"), "VAR"); + } + this.moveInputBefore('VAR_ANCHOR', 'VALUE'); + } else { + this.setInputsInline(true); + // Add new inputs. + for (; i < this.targetCount_; i++) { + if (!this.getInput('TARGET' + i)) { + var input = this.appendValueInput('TARGET' + i); + if (i !== 0) { + input.appendField('and').setAlign(Blockly.ALIGN_RIGHT); + } + } + this.moveInputBefore('TARGET' + i, 'VALUE'); + } + // Kill simple VAR + if (this.getInput('VAR_ANCHOR')) { + this.removeInput('VAR_ANCHOR'); + } + } + // Remove deleted inputs. + while (this.getInput('TARGET' + i)) { + this.removeInput('TARGET' + i); + i++; + } + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('targets', this.targetCount_); + container.setAttribute('simple', this.simpleTarget_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.targetCount_ = parseInt(xmlElement.getAttribute('targets'), 10); + this.simpleTarget_ = "true" === xmlElement.getAttribute('simple'); + this.updateShape_(); + }, +}; + +Blockly.Python['ast_Assign'] = function (block) { + // Create a list with any number of elements of any type. + let value = Blockly.Python.valueToCode(block, 'VALUE', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + let targets = new Array(block.targetCount_); + if (block.targetCount_ === 1 && block.simpleTarget_) { + targets[0] = Blockly.Python.variableDB_.getName(block.getFieldValue('VAR'), Blockly.Variables.NAME_TYPE); + } else { + for (var i = 0; i < block.targetCount_; i++) { + targets[i] = (Blockly.Python.valueToCode(block, 'TARGET' + i, + Blockly.Python.ORDER_NONE) || Blockly.Python.blank); + } + } + return targets.join(' = ') + " = " + value + "\n"; +}; + +BlockMirrorTextToBlocks.prototype['ast_Assign'] = function (node, parent) { + let targets = node.targets; + let value = node.value; + + let values; + let fields = {}; + let simpleTarget = (targets.length === 1 && targets[0]._astname === 'Name'); + if (simpleTarget) { + values = {}; + fields['VAR'] = Sk.ffi.remapToJs(targets[0].id); + } else { + values = this.convertElements("TARGET", targets, node); + } + values['VALUE'] = this.convert(value, node); + + return BlockMirrorTextToBlocks.create_block("ast_Assign", node.lineno, fields, + values, + { + "inline": "true", + }, { + "@targets": targets.length, + "@simple": simpleTarget + }); +}; +Blockly.Blocks['ast_AnnAssignFull'] = { + init: function () { + this.appendValueInput("TARGET") + .setCheck(null) + .appendField("set"); + this.appendValueInput("ANNOTATION") + .setCheck(null) + .appendField(":"); + this.setInputsInline(true); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.VARIABLES); + this.initialized_ = true; + this.updateShape_(); + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + let container = document.createElement('mutation'); + container.setAttribute('initialized', this.initialized_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.initialized_ = "true" === xmlElement.getAttribute('initialized'); + this.updateShape_(); + }, + updateShape_: function (block) { + // Add new inputs. + if (this.initialized_ && !this.getInput('VALUE')) { + this.appendValueInput('VALUE') + .appendField('=') + .setAlign(Blockly.ALIGN_RIGHT); + } + if (!this.initialized_ && this.getInput('VALUE')) { + this.removeInput('VALUE'); + } + } +}; + +BlockMirrorTextToBlocks.ANNOTATION_OPTIONS = [ + ["int", "int"], + ["float", "float"], + ["str", "str"], + ["bool", "bool"], + ["None", "None"] +]; + +BlockMirrorTextToBlocks.ANNOTATION_GENERATE = {}; +BlockMirrorTextToBlocks.ANNOTATION_OPTIONS.forEach(function (ann) { + BlockMirrorTextToBlocks.ANNOTATION_GENERATE[ann[1]] = ann[0]; +}); + +Blockly.Blocks['ast_AnnAssign'] = { + init: function () { + this.appendDummyInput() + .appendField("set") + .appendField(new Blockly.FieldVariable("item"), "TARGET") + .appendField(":") + .appendField(new Blockly.FieldDropdown(BlockMirrorTextToBlocks.ANNOTATION_OPTIONS), "ANNOTATION"); + this.appendValueInput("VALUE") + .setCheck(null) + .appendField("="); + this.setInputsInline(true); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.VARIABLES); + this.strAnnotations_ = false; + this.initialized_ = true; + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + let container = document.createElement('mutation'); + container.setAttribute('str', this.strAnnotations_); + container.setAttribute('initialized', this.initialized_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.strAnnotations_ = "true" === xmlElement.getAttribute('str'); + this.initialized_ = "true" === xmlElement.getAttribute('initialized'); + this.updateShape_(); + }, + updateShape_: function (block) { + // Add new inputs. + if (this.initialized_ && !this.getInput('VALUE')) { + this.appendValueInput('VALUE') + .appendField('=') + .setAlign(Blockly.ALIGN_RIGHT); + } + if (!this.initialized_ && this.getInput('VALUE')) { + this.removeInput('VALUE'); + } + } +}; + +Blockly.Python['ast_AnnAssignFull'] = function (block) { + // Create a list with any number of elements of any type. + let target = Blockly.Python.valueToCode(block, 'TARGET', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + let annotation = Blockly.Python.valueToCode(block, 'ANNOTATION', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + let value = ""; + if (this.initialized_) { + value = " = " + Blockly.Python.valueToCode(block, 'VALUE', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + } + return target + ": " + annotation + value + "\n"; +}; + +Blockly.Python['ast_AnnAssign'] = function (block) { + // Create a list with any number of elements of any type. + var target = Blockly.Python.variableDB_.getName(block.getFieldValue('TARGET'), + Blockly.Variables.NAME_TYPE); + let annotation = block.getFieldValue('ANNOTATION'); + if (block.strAnnotations_) { + annotation = Blockly.Python.quote_(annotation); + } + let value = ""; + if (this.initialized_) { + value = " = " + Blockly.Python.valueToCode(block, 'VALUE', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + } + return target + ": " + annotation + value + "\n"; +}; + +BlockMirrorTextToBlocks.prototype.getBuiltinAnnotation = function (annotation) { + let result = false; + // Can we turn it into a basic type? + if (annotation._astname === 'Name') { + result = Sk.ffi.remapToJs(annotation.id); + } else if (annotation._astname === 'Str') { + result = Sk.ffi.remapToJs(annotation.s); + } + + // Potentially filter out unknown annotations + if (result !== false && this.strictAnnotations) { + if (this.strictAnnotations.indexOf(result) !== -1) { + return result; + } else { + return false; + } + } else { + return result; + } +} + +BlockMirrorTextToBlocks.prototype['ast_AnnAssign'] = function (node, parent) { + let target = node.target; + let annotation = node.annotation; + let value = node.value; + + let values = {}; + let mutations = {'@initialized': false}; + if (value !== null) { + values['VALUE'] = this.convert(value, node); + mutations['@initialized'] = true; + } + + // TODO: This controls whether the annotation is stored in __annotations__ + let simple = node.simple; + + let builtinAnnotation = this.getBuiltinAnnotation(annotation); + + if (target._astname === 'Name' && target.id.v !== Blockly.Python.blank && builtinAnnotation !== false) { + mutations['@str'] = annotation._astname === 'Str' + return BlockMirrorTextToBlocks.create_block("ast_AnnAssign", node.lineno, { + 'TARGET': target.id.v, + 'ANNOTATION': builtinAnnotation, + }, + values, + { + "inline": "true", + }, mutations); + } else { + values['TARGET'] = this.convert(target, node); + values['ANNOTATION'] = this.convert(annotation, node); + return BlockMirrorTextToBlocks.create_block("ast_AnnAssignFull", node.lineno, {}, + values, + { + "inline": "true", + }, mutations); + } +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Str", + "message0": "%1", + "args0": [ + {"type": "field_input", "name": "TEXT", "value": ''} + ], + "output": "String", + "colour": BlockMirrorTextToBlocks.COLOR.TEXT, + "extensions": ["text_quotes"] +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_StrMultiline", + "message0": "%1", + "args0": [ + {"type": "field_multilinetext", "name": "TEXT", "value": ''} + ], + "output": "String", + "colour": BlockMirrorTextToBlocks.COLOR.TEXT, + "extensions": ["text_quotes"] +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_StrDocstring", + "message0": "Docstring: %1 %2", + "args0": [ + {"type": "input_dummy"}, + {"type": "field_multilinetext", "name": "TEXT", "value": ''} + ], + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.TEXT +}); + +Blockly.Python['ast_Str'] = function (block) { + // Text value. + let code = Blockly.Python.quote_(block.getFieldValue('TEXT')); + return [code, Blockly.Python.ORDER_ATOMIC]; +}; + +Blockly.Python['ast_StrMultiline'] = function (block) { + // Text value. + let code = Blockly.Python.multiline_quote_(block.getFieldValue('TEXT')); + return [code, Blockly.Python.ORDER_ATOMIC]; +}; + +Blockly.Python['ast_StrDocstring'] = function (block) { + // Text value. + let code = block.getFieldValue('TEXT'); + if (code.charAt(0) !== '\n') { + code = '\n' + code; + } + if (code.charAt(code.length-1) !== '\n') { + code = code + '\n'; + } + return Blockly.Python.multiline_quote_(code); +}; + +BlockMirrorTextToBlocks.prototype.isDocString = function (node, parent) { + return (parent._astname === 'Expr' && + parent._parent && + ['FunctionDef', 'ClassDef'].indexOf(parent._parent._astname) !== -1 && + parent._parent.body[0] === parent); +}; + +BlockMirrorTextToBlocks.prototype.dedent = function (text, levels) { + let split = text.split("\n"); + let indentation = " ".repeat(levels); + let recombined = []; + for (let i = 0; i < split.length; i++) { + // Are all lines indented? + if (split[i] === '') { + if (i !== 0) { + recombined.push(""); + } + } else if (split[i].startsWith(indentation)) { + let unindentedLine = split[i].substr(indentation.length); + if (unindentedLine !== '' || i !== split.length - 1) { + recombined.push(unindentedLine); + } + } else if (i === 0) { + recombined.push(split[i]); + } else { + return text; + } + } + return recombined.join("\n"); +}; + +// TODO: Handle indentation intelligently +BlockMirrorTextToBlocks.prototype['ast_Str'] = function (node, parent) { + let s = node.s; + let text = Sk.ffi.remapToJs(s); + if (this.isDocString(node, parent)) { + let dedented = this.dedent(text, this.levelIndex - 1); + return [BlockMirrorTextToBlocks.create_block("ast_StrDocstring", node.lineno, {"TEXT": dedented})]; + } else if (text.indexOf('\n') === -1) { + return BlockMirrorTextToBlocks.create_block("ast_Str", node.lineno, {"TEXT": text}); + } else { + let dedented = this.dedent(text, this.levelIndex - 1); + return BlockMirrorTextToBlocks.create_block("ast_StrMultiline", node.lineno, {"TEXT": dedented}); + } +}; + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Expr", + "message0": "do nothing with %1", + "args0": [ + {"type": "input_value", "name": "VALUE"} + ], + "inputsInline": false, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.PYTHON, +}); + +Blockly.Python['ast_Expr'] = function (block) { + // Numeric value. + var value = Blockly.Python.valueToCode(block, 'VALUE', Blockly.Python.ORDER_ATOMIC) || Blockly.Python.blank; + // TODO: Assemble JavaScript into code variable. + return value+"\n"; +}; + +BlockMirrorTextToBlocks.prototype['ast_Expr'] = function (node, parent) { + var value = node.value; + + var converted = this.convert(value, node); + + if (converted.constructor === Array) { + return converted[0]; + } else if (this.isTopLevel(parent)) { + return [this.convert(value, node)]; + } else { + return BlockMirrorTextToBlocks.create_block("ast_Expr", node.lineno, {}, { + "VALUE": this.convert(value, node) + }); + } +}; + +BlockMirrorTextToBlocks.UNARYOPS = [ + ["+", "UAdd", 'Do nothing to the number'], + ["-", "USub", 'Make the number negative'], + ["not", "Not", 'Return the logical opposite of the value.'], + ["~", "Invert", 'Take the bit inversion of the number'] +]; + +BlockMirrorTextToBlocks.UNARYOPS.forEach(function (unaryop) { + //Blockly.Constants.Math.TOOLTIPS_BY_OP[unaryop[1]] = unaryop[2]; + + let fullName = "ast_UnaryOp" + unaryop[1]; + + BlockMirrorTextToBlocks.BLOCKS.push({ + "type": fullName, + "message0": unaryop[0] + " %1", + "args0": [ + {"type": "input_value", "name": "VALUE"} + ], + "inputsInline": false, + "output": null, + "colour": (unaryop[1] == 'Not' ? + BlockMirrorTextToBlocks.COLOR.LOGIC : + BlockMirrorTextToBlocks.COLOR.MATH) + }); + + Blockly.Python[fullName] = function (block) { + // Basic arithmetic operators, and power. + var order = (unaryop[1] == 'Not' ? Blockly.Python.ORDER_LOGICAL_NOT : Blockly.Python.ORDER_UNARY_SIGN); + var argument1 = Blockly.Python.valueToCode(block, 'VALUE', order) || Blockly.Python.blank; + var code = unaryop[0] + (unaryop[1] == 'Not' ? ' ' : '') + argument1; + return [code, order]; + }; +}); + +BlockMirrorTextToBlocks.prototype['ast_UnaryOp'] = function (node, parent) { + let op = node.op.name; + let operand = node.operand; + + return BlockMirrorTextToBlocks.create_block('ast_UnaryOp' + op, node.lineno, {}, { + "VALUE": this.convert(operand, node) + }, { + "inline": false + }); +} +BlockMirrorTextToBlocks.BOOLOPS = [ + ["and", "And", Blockly.Python.ORDER_LOGICAL_AND, 'Return whether the left and right both evaluate to True.'], + ["or", "Or", Blockly.Python.ORDER_LOGICAL_OR, 'Return whether either the left or right evaluate to True.'] +]; +var BOOLOPS_BLOCKLY_DISPLAY = BlockMirrorTextToBlocks.BOOLOPS.map( + boolop => [boolop[0], boolop[1]] +); +var BOOLOPS_BLOCKLY_GENERATE = {}; +BlockMirrorTextToBlocks.BOOLOPS.forEach(function (boolop) { + BOOLOPS_BLOCKLY_GENERATE[boolop[1]] = [" " + boolop[0] + " ", boolop[2]]; +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_BoolOp", + "message0": "%1 %2 %3", + "args0": [ + {"type": "input_value", "name": "A"}, + {"type": "field_dropdown", "name": "OP", "options": BOOLOPS_BLOCKLY_DISPLAY}, + {"type": "input_value", "name": "B"} + ], + "inputsInline": true, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.LOGIC +}); + +Blockly.Python['ast_BoolOp'] = function (block) { + // Operations 'and', 'or'. + var operator = (block.getFieldValue('OP') === 'And') ? 'and' : 'or'; + var order = (operator === 'and') ? Blockly.Python.ORDER_LOGICAL_AND : + Blockly.Python.ORDER_LOGICAL_OR; + var argument0 = Blockly.Python.valueToCode(block, 'A', order) || Blockly.Python.blank; + var argument1 = Blockly.Python.valueToCode(block, 'B', order) || Blockly.Python.blank; + var code = argument0 + ' ' + operator + ' ' + argument1; + return [code, order]; +}; + +BlockMirrorTextToBlocks.prototype['ast_BoolOp'] = function (node, parent) { + var op = node.op; + var values = node.values; + var result_block = this.convert(values[0], node); + for (var i = 1; i < values.length; i += 1) { + result_block = BlockMirrorTextToBlocks.create_block("ast_BoolOp", node.lineno, { + "OP": op.name + }, { + "A": result_block, + "B": this.convert(values[i], node) + }, { + "inline": "true" + }); + } + return result_block; +}; + + + +BlockMirrorTextToBlocks.COMPARES = [ + ["==", "Eq", 'Return whether the two values are equal.'], + ["!=", "NotEq", 'Return whether the two values are not equal.'], + ["<", "Lt", 'Return whether the left value is less than the right value.'], + ["<=", "LtE", 'Return whether the left value is less than or equal to the right value.'], + [">", "Gt", 'Return whether the left value is greater than the right value.'], + [">=", "GtE", 'Return whether the left value is greater than or equal to the right value.'], + ["is", "Is", 'Return whether the left value is identical to the right value.'], + ["is not", "IsNot", 'Return whether the left value is not identical to the right value.'], + ["in", "In", 'Return whether the left value is in the right value.'], + ["not in", "NotIn", 'Return whether the left value is not in the right value.'], +]; + +var COMPARES_BLOCKLY_DISPLAY = BlockMirrorTextToBlocks.COMPARES.map( + boolop => [boolop[0], boolop[1]] +); +var COMPARES_BLOCKLY_GENERATE = {}; +BlockMirrorTextToBlocks.COMPARES.forEach(function (boolop) { + COMPARES_BLOCKLY_GENERATE[boolop[1]] = boolop[0]; +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Compare", + "message0": "%1 %2 %3", + "args0": [ + {"type": "input_value", "name": "A"}, + {"type": "field_dropdown", "name": "OP", "options": COMPARES_BLOCKLY_DISPLAY}, + {"type": "input_value", "name": "B"} + ], + "inputsInline": true, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.LOGIC +}); + +Blockly.Python['ast_Compare'] = function (block) { + // Basic arithmetic operators, and power. + var tuple = COMPARES_BLOCKLY_GENERATE[block.getFieldValue('OP')]; + var operator = ' ' + tuple + ' '; + var order = Blockly.Python.ORDER_RELATIONAL; + var argument0 = Blockly.Python.valueToCode(block, 'A', order) || Blockly.Python.blank; + var argument1 = Blockly.Python.valueToCode(block, 'B', order) || Blockly.Python.blank; + var code = argument0 + operator + argument1; + return [code, order]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Compare'] = function (node, parent) { + var ops = node.ops; + var left = node.left; + var values = node.comparators; + var result_block = this.convert(left, node); + for (var i = 0; i < values.length; i += 1) { + result_block = BlockMirrorTextToBlocks.create_block("ast_Compare", node.lineno, { + "OP": ops[i].name + }, { + "A": result_block, + "B": this.convert(values[i], node) + }, { + "inline": "true" + }); + } + return result_block; +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_AssertFull", + "message0": "assert %1 %2", + "args0": [ + {"type": "input_value", "name": "TEST"}, + {"type": "input_value", "name": "MSG"} + ], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.LOGIC, +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Assert", + "message0": "assert %1", + "args0": [ + {"type": "input_value", "name": "TEST"} + ], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.LOGIC, +}); + +Blockly.Python['ast_Assert'] = function (block) { + var test = Blockly.Python.valueToCode(block, 'TEST', Blockly.Python.ORDER_ATOMIC) || Blockly.Python.blank; + return "assert " + test + "\n"; +}; + +Blockly.Python['ast_AssertFull'] = function (block) { + var test = Blockly.Python.valueToCode(block, 'TEST', Blockly.Python.ORDER_ATOMIC) || Blockly.Python.blank; + var msg = Blockly.Python.valueToCode(block, 'MSG', Blockly.Python.ORDER_ATOMIC) || Blockly.Python.blank; + return "assert " + test + ", "+msg+"\n"; +}; + +BlockMirrorTextToBlocks.prototype['ast_Assert'] = function (node, parent) { + var test = node.test; + var msg = node.msg; + if (msg == null) { + return BlockMirrorTextToBlocks.create_block("ast_Assert", node.lineno, {}, { + "TEST": this.convert(test, node) + }); + } else { + return BlockMirrorTextToBlocks.create_block("ast_AssertFull", node.lineno, {}, { + "TEST": this.convert(test, node), + "MSG": this.convert(msg, node) + }); + } +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_NameConstantNone", + "message0": "None", + "args0": [], + "output": "None", + "colour": BlockMirrorTextToBlocks.COLOR.LOGIC +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_NameConstantBoolean", + "message0": "%1", + "args0": [ + { + "type": "field_dropdown", "name": "BOOL", "options": [ + ["True", "TRUE"], + ["False", "FALSE"] + ] + } + ], + "output": "Boolean", + "colour": BlockMirrorTextToBlocks.COLOR.LOGIC +}); + +Blockly.Python['ast_NameConstantBoolean'] = function (block) { + // Boolean values true and false. + var code = (block.getFieldValue('BOOL') == 'TRUE') ? 'True' : 'False'; + return [code, Blockly.Python.ORDER_ATOMIC]; +}; + +Blockly.Python['ast_NameConstantNone'] = function (block) { + // Boolean values true and false. + var code = 'None'; + return [code, Blockly.Python.ORDER_ATOMIC]; +}; + +BlockMirrorTextToBlocks.prototype['ast_NameConstant'] = function (node, parent) { + let value = node.value; + + if (value === Sk.builtin.none.none$) { + return BlockMirrorTextToBlocks.create_block('ast_NameConstantNone', node.lineno, {}); + } else if (value === Sk.builtin.bool.true$) { + return BlockMirrorTextToBlocks.create_block('ast_NameConstantBoolean', node.lineno, { + "BOOL": 'TRUE' + }); + } else if (value === Sk.builtin.bool.false$) { + return BlockMirrorTextToBlocks.create_block('ast_NameConstantBoolean', node.lineno, { + "BOOL": 'FALSE' + }); + } +}; +Blockly.Blocks['ast_List'] = { + /** + * Block for creating a list with any number of elements of any type. + * @this Blockly.Block + */ + init: function () { + this.setHelpUrl(Blockly.Msg['LISTS_CREATE_WITH_HELPURL']); + this.setColour(BlockMirrorTextToBlocks.COLOR.LIST); + this.itemCount_ = 3; + this.updateShape_(); + this.setOutput(true, 'List'); + this.setMutator(new Blockly.Mutator(['ast_List_create_with_item'])); + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('items', this.itemCount_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); + this.updateShape_(); + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this Blockly.Block + */ + decompose: function (workspace) { + var containerBlock = workspace.newBlock('ast_List_create_with_container'); + containerBlock.initSvg(); + var connection = containerBlock.getInput('STACK').connection; + for (var i = 0; i < this.itemCount_; i++) { + var itemBlock = workspace.newBlock('ast_List_create_with_item'); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + compose: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + // Count number of inputs. + var connections = []; + while (itemBlock) { + connections.push(itemBlock.valueConnection_); + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + // Disconnect any children that don't belong. + for (var i = 0; i < this.itemCount_; i++) { + var connection = this.getInput('ADD' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) == -1) { + connection.disconnect(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (var i = 0; i < this.itemCount_; i++) { + Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); + } + }, + /** + * Store pointers to any connected child blocks. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + saveConnections: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + var i = 0; + while (itemBlock) { + var input = this.getInput('ADD' + i); + itemBlock.valueConnection_ = input && input.connection.targetConnection; + i++; + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + }, + /** + * Modify this block to have the correct number of inputs. + * @private + * @this Blockly.Block + */ + updateShape_: function () { + if (this.itemCount_ && this.getInput('EMPTY')) { + this.removeInput('EMPTY'); + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { + this.appendDummyInput('EMPTY') + .appendField('create empty list []'); + } + // Add new inputs. + for (var i = 0; i < this.itemCount_; i++) { + if (!this.getInput('ADD' + i)) { + var input = this.appendValueInput('ADD' + i); + if (i == 0) { + input.appendField('create list with ['); + } else { + input.appendField(',').setAlign(Blockly.ALIGN_RIGHT); + } + } + } + // Remove deleted inputs. + while (this.getInput('ADD' + i)) { + this.removeInput('ADD' + i); + i++; + } + // Add the trailing "]" + if (this.getInput('TAIL')) { + this.removeInput('TAIL'); + } + if (this.itemCount_) { + this.appendDummyInput('TAIL') + .appendField(']') + .setAlign(Blockly.ALIGN_RIGHT); + } + } +}; + +Blockly.Blocks['ast_List_create_with_container'] = { + /** + * Mutator block for list container. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.LIST); + this.appendDummyInput() + .appendField('Add new list elements below'); + this.appendStatementInput('STACK'); + this.contextMenu = false; + } +}; + +Blockly.Blocks['ast_List_create_with_item'] = { + /** + * Mutator block for adding items. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.LIST); + this.appendDummyInput() + .appendField('Element'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.contextMenu = false; + } +}; + +Blockly.Python['ast_List'] = function (block) { + // Create a list with any number of elements of any type. + var elements = new Array(block.itemCount_); + for (var i = 0; i < block.itemCount_; i++) { + elements[i] = Blockly.Python.valueToCode(block, 'ADD' + i, + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + } + var code = '[' + elements.join(', ') + ']'; + return [code, Blockly.Python.ORDER_ATOMIC]; +}; + +BlockMirrorTextToBlocks.prototype['ast_List'] = function (node, parent) { + var elts = node.elts; + var ctx = node.ctx; + + return BlockMirrorTextToBlocks.create_block("ast_List", node.lineno, {}, + this.convertElements("ADD", elts, node), + { + "inline": elts.length > 3 ? "false" : "true", + }, { + "@items": elts.length + }); +} + +Blockly.Blocks['ast_Tuple'] = { + /** + * Block for creating a tuple with any number of elements of any type. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.TUPLE); + this.itemCount_ = 3; + this.updateShape_(); + this.setOutput(true, 'Tuple'); + this.setMutator(new Blockly.Mutator(['ast_Tuple_create_with_item'])); + }, + /** + * Create XML to represent tuple inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('items', this.itemCount_); + return container; + }, + /** + * Parse XML to restore the tuple inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); + this.updateShape_(); + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this Blockly.Block + */ + decompose: function (workspace) { + var containerBlock = workspace.newBlock('ast_Tuple_create_with_container'); + containerBlock.initSvg(); + var connection = containerBlock.getInput('STACK').connection; + for (var i = 0; i < this.itemCount_; i++) { + var itemBlock = workspace.newBlock('ast_Tuple_create_with_item'); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + compose: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + // Count number of inputs. + var connections = []; + while (itemBlock) { + connections.push(itemBlock.valueConnection_); + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + // Disconnect any children that don't belong. + for (var i = 0; i < this.itemCount_; i++) { + var connection = this.getInput('ADD' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) == -1) { + connection.disconnect(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (var i = 0; i < this.itemCount_; i++) { + Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); + } + }, + /** + * Store pointers to any connected child blocks. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + saveConnections: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + var i = 0; + while (itemBlock) { + var input = this.getInput('ADD' + i); + itemBlock.valueConnection_ = input && input.connection.targetConnection; + i++; + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + }, + /** + * Modify this block to have the correct number of inputs. + * @private + * @this Blockly.Block + */ + updateShape_: function () { + if (this.itemCount_ && this.getInput('EMPTY')) { + this.removeInput('EMPTY'); + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { + this.appendDummyInput('EMPTY') + .appendField('()'); + } + // Add new inputs. + for (var i = 0; i < this.itemCount_; i++) { + if (!this.getInput('ADD' + i)) { + var input = this.appendValueInput('ADD' + i); + if (i === 0) { + input.appendField('(', ).setAlign(Blockly.ALIGN_RIGHT); + } else { + input.appendField(',').setAlign(Blockly.ALIGN_RIGHT); + } + } + } + // Remove deleted inputs. + while (this.getInput('ADD' + i)) { + this.removeInput('ADD' + i); + i++; + } + // Add the trailing "]" + if (this.getInput('TAIL')) { + this.removeInput('TAIL'); + } + if (this.itemCount_) { + let tail = this.appendDummyInput('TAIL'); + if (this.itemCount_ === 1) { + tail.appendField(',)'); + } else { + tail.appendField(')'); + } + tail.setAlign(Blockly.ALIGN_RIGHT); + } + } +}; + +Blockly.Blocks['ast_Tuple_create_with_container'] = { + /** + * Mutator block for tuple container. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.TUPLE); + this.appendDummyInput() + .appendField('Add new tuple elements below'); + this.appendStatementInput('STACK'); + this.contextMenu = false; + } +}; + +Blockly.Blocks['ast_Tuple_create_with_item'] = { + /** + * Mutator block for adding items. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.TUPLE); + this.appendDummyInput() + .appendField('Element'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.contextMenu = false; + } +}; + +Blockly.Python['ast_Tuple'] = function (block) { + // Create a tuple with any number of elements of any type. + var elements = new Array(block.itemCount_); + for (var i = 0; i < block.itemCount_; i++) { + elements[i] = Blockly.Python.valueToCode(block, 'ADD' + i, + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + } + let requiredComma = ""; + if (block.itemCount_ == 1) { + requiredComma = ", "; + } + var code = '(' + elements.join(', ') + requiredComma+')'; + return [code, Blockly.Python.ORDER_ATOMIC]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Tuple'] = function (node, parent) { + var elts = node.elts; + var ctx = node.ctx; + + return BlockMirrorTextToBlocks.create_block("ast_Tuple", node.lineno, {}, + this.convertElements("ADD", elts, node), + { + "inline": elts.length > 4 ? "false" : "true", + }, { + "@items": elts.length + }); +} + +Blockly.Blocks['ast_Set'] = { + /** + * Block for creating a set with any number of elements of any type. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.SET); + this.itemCount_ = 3; + this.updateShape_(); + this.setOutput(true, 'Set'); + this.setMutator(new Blockly.Mutator(['ast_Set_create_with_item'])); + }, + /** + * Create XML to represent set inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('items', this.itemCount_); + return container; + }, + /** + * Parse XML to restore the set inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); + this.updateShape_(); + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this Blockly.Block + */ + decompose: function (workspace) { + var containerBlock = workspace.newBlock('ast_Set_create_with_container'); + containerBlock.initSvg(); + var connection = containerBlock.getInput('STACK').connection; + for (var i = 0; i < this.itemCount_; i++) { + var itemBlock = workspace.newBlock('ast_Set_create_with_item'); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + compose: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + // Count number of inputs. + var connections = []; + while (itemBlock) { + connections.push(itemBlock.valueConnection_); + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + // Disconnect any children that don't belong. + for (var i = 0; i < this.itemCount_; i++) { + var connection = this.getInput('ADD' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) == -1) { + connection.disconnect(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (var i = 0; i < this.itemCount_; i++) { + Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); + } + }, + /** + * Store pointers to any connected child blocks. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + saveConnections: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + var i = 0; + while (itemBlock) { + var input = this.getInput('ADD' + i); + itemBlock.valueConnection_ = input && input.connection.targetConnection; + i++; + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + }, + /** + * Modify this block to have the correct number of inputs. + * @private + * @this Blockly.Block + */ + updateShape_: function () { + if (this.itemCount_ && this.getInput('EMPTY')) { + this.removeInput('EMPTY'); + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { + this.appendDummyInput('EMPTY') + .appendField('create empty set'); + } + // Add new inputs. + for (var i = 0; i < this.itemCount_; i++) { + if (!this.getInput('ADD' + i)) { + var input = this.appendValueInput('ADD' + i); + if (i === 0) { + input.appendField('create set with {').setAlign(Blockly.ALIGN_RIGHT); + } else { + input.appendField(',').setAlign(Blockly.ALIGN_RIGHT); + } + } + } + // Remove deleted inputs. + while (this.getInput('ADD' + i)) { + this.removeInput('ADD' + i); + i++; + } + // Add the trailing "]" + if (this.getInput('TAIL')) { + this.removeInput('TAIL'); + } + if (this.itemCount_) { + this.appendDummyInput('TAIL').appendField('}').setAlign(Blockly.ALIGN_RIGHT); + + } + } +}; + +Blockly.Blocks['ast_Set_create_with_container'] = { + /** + * Mutator block for set container. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.SET); + this.appendDummyInput() + .appendField('Add new set elements below'); + this.appendStatementInput('STACK'); + this.contextMenu = false; + } +}; + +Blockly.Blocks['ast_Set_create_with_item'] = { + /** + * Mutator block for adding items. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.SET); + this.appendDummyInput() + .appendField('Element'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.contextMenu = false; + } +}; + +Blockly.Python['ast_Set'] = function (block) { + // Create a set with any number of elements of any type. + if (block.itemCount_ === 0) { + return ['set()', Blockly.Python.ORDER_FUNCTION_CALL]; + } + var elements = new Array(block.itemCount_); + for (var i = 0; i < block.itemCount_; i++) { + elements[i] = Blockly.Python.valueToCode(block, 'ADD' + i, + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + } + var code = '{' + elements.join(', ') + '}'; + return [code, Blockly.Python.ORDER_ATOMIC]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Set'] = function (node, parent) { + var elts = node.elts; + + return BlockMirrorTextToBlocks.create_block("ast_Set", node.lineno, {}, + this.convertElements("ADD", elts, node), + { + "inline": elts.length > 3 ? "false" : "true", + }, { + "@items": elts.length + }); +} + +Blockly.Blocks['ast_DictItem'] = { + init: function () { + this.appendValueInput("KEY") + .setCheck(null); + this.appendValueInput("VALUE") + .setCheck(null) + .appendField(":"); + this.setInputsInline(true); + this.setOutput(true, "DictPair"); + this.setColour(BlockMirrorTextToBlocks.COLOR.DICTIONARY); + } +}; + +Blockly.Blocks['ast_Dict'] = { + /** + * Block for creating a dict with any number of elements of any type. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.DICTIONARY); + this.itemCount_ = 3; + this.updateShape_(); + this.setOutput(true, 'Dict'); + this.setMutator(new Blockly.Mutator(['ast_Dict_create_with_item'])); + }, + /** + * Create XML to represent dict inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('items', this.itemCount_); + return container; + }, + /** + * Parse XML to restore the dict inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); + this.updateShape_(); + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this Blockly.Block + */ + decompose: function (workspace) { + var containerBlock = workspace.newBlock('ast_Dict_create_with_container'); + containerBlock.initSvg(); + var connection = containerBlock.getInput('STACK').connection; + for (var i = 0; i < this.itemCount_; i++) { + var itemBlock = workspace.newBlock('ast_Dict_create_with_item'); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + compose: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + // Count number of inputs. + var connections = []; + while (itemBlock) { + connections.push(itemBlock.valueConnection_); + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + // Disconnect any children that don't belong. + for (var i = 0; i < this.itemCount_; i++) { + var connection = this.getInput('ADD' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) == -1) { + let key = connection.getSourceBlock().getInput("KEY"); + if (key.connection.targetConnection) { + key.connection.targetConnection.getSourceBlock().unplug(true); + } + let value = connection.getSourceBlock().getInput("VALUE"); + if (value.connection.targetConnection) { + value.connection.targetConnection.getSourceBlock().unplug(true); + } + connection.disconnect(); + connection.getSourceBlock().dispose(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (var i = 0; i < this.itemCount_; i++) { + Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i); + if (!connections[i]) { + let itemBlock = this.workspace.newBlock('ast_DictItem'); + itemBlock.setDeletable(false); + itemBlock.setMovable(false); + itemBlock.initSvg(); + this.getInput('ADD'+i).connection.connect(itemBlock.outputConnection); + itemBlock.render(); + //this.get(itemBlock, 'ADD'+i) + } + } + }, + /** + * Store pointers to any connected child blocks. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + saveConnections: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + var i = 0; + while (itemBlock) { + var input = this.getInput('ADD' + i); + itemBlock.valueConnection_ = input && input.connection.targetConnection; + i++; + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + }, + /** + * Modify this block to have the correct number of inputs. + * @private + * @this Blockly.Block + */ + updateShape_: function () { + if (this.itemCount_ && this.getInput('EMPTY')) { + this.removeInput('EMPTY'); + } else if (!this.itemCount_ && !this.getInput('EMPTY')) { + this.appendDummyInput('EMPTY') + .appendField('empty dictionary'); + } + // Add new inputs. + for (var i = 0; i < this.itemCount_; i++) { + if (!this.getInput('ADD' + i)) { + var input = this.appendValueInput('ADD' + i) + .setCheck('DictPair'); + if (i === 0) { + input.appendField('create dict with').setAlign(Blockly.ALIGN_RIGHT); + } + } + } + // Remove deleted inputs. + while (this.getInput('ADD' + i)) { + this.removeInput('ADD' + i); + i++; + } + // Add the trailing "}" + /* + if (this.getInput('TAIL')) { + this.removeInput('TAIL'); + } + if (this.itemCount_) { + let tail = this.appendDummyInput('TAIL') + .appendField('}'); + tail.setAlign(Blockly.ALIGN_RIGHT); + }*/ + } +}; + +Blockly.Blocks['ast_Dict_create_with_container'] = { + /** + * Mutator block for dict container. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.DICTIONARY); + this.appendDummyInput() + .appendField('Add new dict elements below'); + this.appendStatementInput('STACK'); + this.contextMenu = false; + } +}; + +Blockly.Blocks['ast_Dict_create_with_item'] = { + /** + * Mutator block for adding items. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.DICTIONARY); + this.appendDummyInput() + .appendField('Element'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.contextMenu = false; + } +}; + +Blockly.Python['ast_Dict'] = function (block) { + // Create a dict with any number of elements of any type. + var elements = new Array(block.itemCount_); + for (var i = 0; i < block.itemCount_; i++) { + let child = block.getInputTargetBlock('ADD' + i); + if (child === null || child.type != 'ast_DictItem') { + elements[i] = (Blockly.Python.blank + ": "+ Blockly.Python.blank); + continue; + } + let key = Blockly.Python.valueToCode(child, 'KEY', Blockly.Python.ORDER_NONE) || + Blockly.Python.blank; + let value = Blockly.Python.valueToCode(child, 'VALUE', Blockly.Python.ORDER_NONE) || + Blockly.Python.blank; + elements[i] = (key+ ": "+value); + } + var code = '{' + elements.join(', ') + '}'; + return [code, Blockly.Python.ORDER_ATOMIC]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Dict'] = function (node, parent) { + let keys = node.keys; + let values = node.values; + + if (keys === null) { + return BlockMirrorTextToBlocks.create_block("ast_Dict", node.lineno, {}, + {}, {"inline": "false"}, {"@items": 0}); + } + + let elements = {}; + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + let value = values[i]; + elements["ADD" + i] = BlockMirrorTextToBlocks.create_block("ast_DictItem", node.lineno, {}, + { + "KEY": this.convert(key, node), + "VALUE": this.convert(value, node) + }, + this.LOCKED_BLOCK); + } + + return BlockMirrorTextToBlocks.create_block("ast_Dict", node.lineno, {}, + elements, + { + "inline": "false" + }, { + "@items": keys.length + }); +} + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": 'ast_Starred', + "message0": "*%1", + "args0": [ + {"type": "input_value", "name": "VALUE"} + ], + "inputsInline": false, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.VARIABLES +}); + +Blockly.Python['ast_Starred'] = function (block) { + // Basic arithmetic operators, and power. + var order = Blockly.Python.ORDER_NONE; + var argument1 = Blockly.Python.valueToCode(block, 'VALUE', order) || Blockly.Python.blank; + var code = "*" + argument1; + return [code, order]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Starred'] = function (node, parent) { + let value = node.value; + let ctx = node.ctx; + + return BlockMirrorTextToBlocks.create_block('ast_Starred', node.lineno, {}, { + "VALUE": this.convert(value, node) + }, { + "inline": true + }); +} +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_IfExp", + "message0": "%1 if %2 else %3", + "args0": [ + {"type": "input_value", "name": "BODY"}, + {"type": "input_value", "name": "TEST"}, + {"type": "input_value", "name": "ORELSE"} + ], + "inputsInline": true, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.LOGIC +}); + +Blockly.Python['ast_IfExp'] = function (block) { + var test = Blockly.Python.valueToCode(block, 'TEST', Blockly.Python.ORDER_CONDITIONAL) || Blockly.Python.blank; + var body = Blockly.Python.valueToCode(block, 'BODY', Blockly.Python.ORDER_CONDITIONAL) || Blockly.Python.blank; + var orelse = Blockly.Python.valueToCode(block, 'ORELSE', Blockly.Python.ORDER_CONDITIONAL) || Blockly.Python.blank; + return [body + " if " + test + " else " + orelse + "\n", Blockly.Python.ORDER_CONDITIONAL]; +}; + +BlockMirrorTextToBlocks.prototype['ast_IfExp'] = function (node, parent) { + let test = node.test; + let body = node.body; + let orelse = node.orelse; + + return BlockMirrorTextToBlocks.create_block("ast_IfExp", node.lineno, {}, { + "TEST": this.convert(test, node), + "BODY": this.convert(body, node), + "ORELSE": this.convert(orelse, node) + }); +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_AttributeFull", + "lastDummyAlign0": "RIGHT", + "message0": "%1 . %2", + "args0": [ + {"type": "input_value", "name": "VALUE"}, + {"type": "field_input", "name": "ATTR", "text": "default"} + ], + "inputsInline": true, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.OO, +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Attribute", + "message0": "%1 . %2", + "args0": [ + {"type": "field_variable", "name": "VALUE", "variable": "variable"}, + {"type": "field_input", "name": "ATTR", "text": "attribute"} + ], + "inputsInline": true, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.OO, +}); + +Blockly.Python['ast_Attribute'] = function (block) { + // Text value. + var value = Blockly.Python.variableDB_.getName(block.getFieldValue('VALUE'), + Blockly.Variables.NAME_TYPE); + var attr = block.getFieldValue('ATTR'); + let code = value + "." + attr; + return [code, Blockly.Python.ORDER_MEMBER]; +}; + +Blockly.Python['ast_AttributeFull'] = function (block) { + // Text value. + var value = Blockly.Python.valueToCode(block, 'VALUE', Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + var attr = block.getFieldValue('ATTR'); + let code = value + "." + attr; + return [code, Blockly.Python.ORDER_MEMBER]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Attribute'] = function (node, parent) { + let value = node.value; + let attr = node.attr; + + //if (value.constructor) + if (value._astname == "Name") { + return BlockMirrorTextToBlocks.create_block("ast_Attribute", node.lineno, { + "VALUE": Sk.ffi.remapToJs(value.id), + "ATTR": Sk.ffi.remapToJs(attr) + },); + } else { + return BlockMirrorTextToBlocks.create_block("ast_AttributeFull", node.lineno, { + "ATTR": Sk.ffi.remapToJs(attr) + }, { + "VALUE": this.convert(value, node) + }); + } +} + +// TODO: Support stuff like "append" where the message is after the value input +// TODO: Handle updating function/method definition -> update call +// TODO: Do a pretraversal to determine if a given function returns + +Blockly.Blocks['ast_Call'] = { + /** + * Block for calling a procedure with no return value. + * @this Blockly.Block + */ + init: function () { + this.givenColour_ = BlockMirrorTextToBlocks.COLOR.FUNCTIONS + this.setInputsInline(true); + // Regular ('NAME') or Keyword (either '**' or '*NAME') + this.arguments_ = []; + this.argumentVarModels_ = []; + // acbart: Added count to keep track of unused parameters + this.argumentCount_ = 0; + this.quarkConnections_ = {}; + this.quarkIds_ = null; + // acbart: Show parameter names, if they exist + this.showParameterNames_ = false; + // acbart: Whether this block returns + this.returns_ = true; + // acbart: added simpleName to handle complex function calls (e.g., chained) + this.isMethod_ = false; + this.name_ = null; + this.message_ = "function"; + this.premessage_ = ""; + this.updateShape_(); + }, + + /** + * Returns the name of the procedure this block calls. + * @return {string} Procedure name. + * @this Blockly.Block + */ + getProcedureCall: function () { + return this.name_; + }, + /** + * Notification that a procedure is renaming. + * If the name matches this block's procedure, rename it. + * Also rename if it was previously null. + * @param {string} oldName Previous name of procedure. + * @param {string} newName Renamed procedure. + * @this Blockly.Block + */ + renameProcedure: function (oldName, newName) { + if (this.name_ === null || + Blockly.Names.equals(oldName, this.name_)) { + this.name_ = newName; + this.updateShape_(); + } + }, + /** + * Notification that the procedure's parameters have changed. + * @param {!Array.} paramNames New param names, e.g. ['x', 'y', 'z']. + * @param {!Array.} paramIds IDs of params (consistent for each + * parameter through the life of a mutator, regardless of param renaming), + * e.g. ['piua', 'f8b_', 'oi.o']. + * @private + * @this Blockly.Block + */ + setProcedureParameters_: function (paramNames, paramIds) { + // Data structures: + // this.arguments = ['x', 'y'] + // Existing param names. + // this.quarkConnections_ {piua: null, f8b_: Blockly.Connection} + // Look-up of paramIds to connections plugged into the call block. + // this.quarkIds_ = ['piua', 'f8b_'] + // Existing param IDs. + // Note that quarkConnections_ may include IDs that no longer exist, but + // which might reappear if a param is reattached in the mutator. + var defBlock = Blockly.Procedures.getDefinition(this.getProcedureCall(), + this.workspace); + var mutatorOpen = defBlock && defBlock.mutator && + defBlock.mutator.isVisible(); + if (!mutatorOpen) { + this.quarkConnections_ = {}; + this.quarkIds_ = null; + } + if (!paramIds) { + // Reset the quarks (a mutator is about to open). + return false; + } + // Test arguments (arrays of strings) for changes. '\n' is not a valid + // argument name character, so it is a valid delimiter here. + if (paramNames.join('\n') == this.arguments_.join('\n')) { + // No change. + this.quarkIds_ = paramIds; + return false; + } + if (paramIds.length !== paramNames.length) { + throw RangeError('paramNames and paramIds must be the same length.'); + } + this.setCollapsed(false); + if (!this.quarkIds_) { + // Initialize tracking for this block. + this.quarkConnections_ = {}; + this.quarkIds_ = []; + } + // Switch off rendering while the block is rebuilt. + var savedRendered = this.rendered; + this.rendered = false; + // Update the quarkConnections_ with existing connections. + for (let i = 0; i < this.arguments_.length; i++) { + var input = this.getInput('ARG' + i); + if (input) { + let connection = input.connection.targetConnection; + this.quarkConnections_[this.quarkIds_[i]] = connection; + if (mutatorOpen && connection && + paramIds.indexOf(this.quarkIds_[i]) === -1) { + // This connection should no longer be attached to this block. + connection.disconnect(); + connection.getSourceBlock().bumpNeighbours_(); + } + } + } + // Rebuild the block's arguments. + this.arguments_ = [].concat(paramNames); + this.argumentCount_ = this.arguments_.length; + // And rebuild the argument model list. + this.argumentVarModels_ = []; + /* + // acbart: Function calls don't create variables, what do they know? + for (let i = 0; i < this.arguments_.length; i++) { + let argumentName = this.arguments_[i]; + var variable = Blockly.Variables.getVariable( + this.workspace, null, this.arguments_[i], ''); + if (variable) { + this.argumentVarModels_.push(variable); + } + }*/ + + this.updateShape_(); + this.quarkIds_ = paramIds; + // Reconnect any child blocks. + if (this.quarkIds_) { + for (let i = 0; i < this.arguments_.length; i++) { + var quarkId = this.quarkIds_[i]; + if (quarkId in this.quarkConnections_) { + let connection = this.quarkConnections_[quarkId]; + if (!Blockly.Mutator.reconnect(connection, this, 'ARG' + i)) { + // Block no longer exists or has been attached elsewhere. + delete this.quarkConnections_[quarkId]; + } + } + } + } + // Restore rendering and show the changes. + this.rendered = savedRendered; + if (this.rendered) { + this.render(); + } + return true; + }, + /** + * Modify this block to have the correct number of arguments. + * @private + * @this Blockly.Block + */ + updateShape_: function () { + // If it's a method, add in the caller + if (this.isMethod_ && !this.getInput('FUNC')) { + let func = this.appendValueInput('FUNC'); + // If there's a premessage, add it in + if (this.premessage_ !== "") { + func.appendField(this.premessage_); + } + } else if (!this.isMethod_ && this.getInput('FUNC')) { + this.removeInput('FUNC'); + } + + let drawnArgumentCount = this.getDrawnArgumentCount_(); + let message = this.getInput('MESSAGE_AREA') + // Zero arguments, just do {message()} + if (drawnArgumentCount === 0) { + if (message) { + message.removeField('MESSAGE'); + } else { + message = this.appendDummyInput('MESSAGE_AREA') + .setAlign(Blockly.ALIGN_RIGHT); + } + message.appendField(new Blockly.FieldLabel(this.message_ + "\ ("), 'MESSAGE'); + // One argument, no MESSAGE_AREA + } else if (message) { + this.removeInput('MESSAGE_AREA'); + } + // Process arguments + let i; + for (i = 0; i < drawnArgumentCount; i++) { + let argument = this.arguments_[i]; + let argumentName = this.parseArgument_(argument); + if (i === 0) { + argumentName = this.message_ + "\ (" + argumentName; + } + let field = this.getField('ARGNAME' + i); + if (field) { + // Ensure argument name is up to date. + // The argument name field is deterministic based on the mutation, + // no need to fire a change event. + Blockly.Events.disable(); + try { + field.setValue(argumentName); + } finally { + Blockly.Events.enable(); + } + } else { + // Add new input. + field = new Blockly.FieldLabel(argumentName); + this.appendValueInput('ARG' + i) + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(field, 'ARGNAME' + i) + .init(); + } + if (argumentName) { + field.setVisible(true); + } else { + field.setVisible(false); + } + } + + // Closing parentheses + if (!this.getInput('CLOSE_PAREN')) { + this.appendDummyInput('CLOSE_PAREN') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(new Blockly.FieldLabel(")")); + } + + // Move everything into place + if (drawnArgumentCount === 0) { + if (this.isMethod_) { + this.moveInputBefore('FUNC', 'MESSAGE_AREA'); + } + this.moveInputBefore('MESSAGE_AREA', 'CLOSE_PAREN'); + } else { + if (this.isMethod_) { + this.moveInputBefore('FUNC', 'CLOSE_PAREN'); + } + } + for (let j = 0; j < i; j++) { + this.moveInputBefore('ARG' + j, 'CLOSE_PAREN') + } + + // Set return state + this.setReturn_(this.returns_, false); + // Remove deleted inputs. + while (this.getInput('ARG' + i)) { + this.removeInput('ARG' + i); + i++; + } + + this.setColour(this.givenColour_); + } + , + /** + * Create XML to represent the (non-editable) name and arguments. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + let name = this.getProcedureCall(); + container.setAttribute('name', name === null ? '*' : name); + container.setAttribute('arguments', this.argumentCount_); + container.setAttribute('returns', this.returns_); + container.setAttribute('parameters', this.showParameterNames_); + container.setAttribute('method', this.isMethod_); + container.setAttribute('message', this.message_); + container.setAttribute('premessage', this.premessage_); + container.setAttribute('colour', this.givenColour_); + for (var i = 0; i < this.arguments_.length; i++) { + var parameter = document.createElement('arg'); + parameter.setAttribute('name', this.arguments_[i]); + container.appendChild(parameter); + } + return container; + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.name_ = xmlElement.getAttribute('name'); + this.name_ = this.name_ === '*' ? null : this.name_; + this.argumentCount_ = parseInt(xmlElement.getAttribute('arguments'), 10); + this.showParameterNames_ = "true" === xmlElement.getAttribute('parameters'); + this.returns_ = "true" === xmlElement.getAttribute('returns'); + this.isMethod_ = "true" === xmlElement.getAttribute('method'); + this.message_ = xmlElement.getAttribute('message'); + this.premessage_ = xmlElement.getAttribute('premessage'); + this.givenColour_ = parseInt(xmlElement.getAttribute('colour'), 10); + + var args = []; + var paramIds = []; + for (var i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() === 'arg') { + args.push(childNode.getAttribute('name')); + paramIds.push(childNode.getAttribute('paramId')); + } + } + let result = this.setProcedureParameters_(args, paramIds); + if (!result) { + this.updateShape_(); + } + if (this.name_ !== null) { + this.renameProcedure(this.getProcedureCall(), this.name_); + } + }, + /** + * Return all variables referenced by this block. + * @return {!Array.} List of variable models. + * @this Blockly.Block + */ + getVarModels: function () { + return this.argumentVarModels_; + } + , + /** + * Add menu option to find the definition block for this call. + * @param {!Array} options List of menu options to add to. + * @this Blockly.Block + */ + customContextMenu: function (options) { + if (!this.workspace.isMovable()) { + // If we center on the block and the workspace isn't movable we could + // loose blocks at the edges of the workspace. + return; + } + + let workspace = this.workspace; + let block = this; + + // Highlight Definition + let option = {enabled: true}; + option.text = Blockly.Msg['PROCEDURES_HIGHLIGHT_DEF']; + let name = this.getProcedureCall(); + option.callback = function () { + let def = Blockly.Procedures.getDefinition(name, workspace); + if (def) { + workspace.centerOnBlock(def.id); + def.select(); + } + }; + options.push(option); + + // Show Parameter Names + options.push({ + enabled: true, + text: "Show/Hide parameters", + callback: function () { + block.showParameterNames_ = !block.showParameterNames_; + block.updateShape_(); + block.render(); + } + }); + + // Change Return Type + options.push({ + enabled: true, + text: this.returns_ ? "Make statement" : "Make expression", + callback: function () { + block.returns_ = !block.returns_; + block.setReturn_(block.returns_, true); + } + }) + }, + /** + * Notification that the procedure's return state has changed. + * @param {boolean} returnState New return state + * @param forceRerender Whether to render + * @this Blockly.Block + */ + setReturn_: function (returnState, forceRerender) { + this.unplug(true); + if (returnState) { + this.setPreviousStatement(false); + this.setNextStatement(false); + this.setOutput(true); + } else { + this.setOutput(false); + this.setPreviousStatement(true); + this.setNextStatement(true); + } + if (forceRerender) { + if (this.rendered) { + this.render(); + } + } + }, + //defType_: 'procedures_defnoreturn', + parseArgument_: function (argument) { + if (argument.startsWith('KWARGS:')) { + // KWARG + return "unpack"; + } else if (argument.startsWith('KEYWORD:')) { + return argument.substring(8) + "="; + } else { + if (this.showParameterNames_) { + if (argument.startsWith("KNOWN_ARG:")) { + return argument.substring(10) + "="; + } + } + } + return ""; + }, + getDrawnArgumentCount_: function () { + return Math.min(this.argumentCount_, this.arguments_.length); + } +}; + +Blockly.Python['ast_Call'] = function (block) { + // Get the caller + let funcName = ""; + if (block.isMethod_) { + funcName = Blockly.Python.valueToCode(block, 'FUNC', Blockly.Python.ORDER_FUNCTION_CALL) || + Blockly.Python.blank; + } + funcName += this.name_; + // Build the arguments + var args = []; + for (var i = 0; i < block.arguments_.length; i++) { + let value = Blockly.Python.valueToCode(block, 'ARG' + i, + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + let argument = block.arguments_[i]; + if (argument.startsWith('KWARGS:')) { + args[i] = "**" + value; + } else if (argument.startsWith('KEYWORD:')) { + args[i] = argument.substring(8) + "=" + value; + } else { + args[i] = value; + } + } + // Return the result + let code = funcName + '(' + args.join(', ') + ')'; + if (block.returns_) { + return [code, Blockly.Python.ORDER_FUNCTION_CALL]; + } else { + return code + "\n"; + } +}; + +BlockMirrorTextToBlocks.prototype.getAsModule = function (node) { + if (node._astname === 'Name') { + return Sk.ffi.remapToJs(node.id); + } else if (node._astname === 'Attribute') { + let origin = this.getAsModule(node.value); + if (origin !== null) { + return origin + '.' + Sk.ffi.remapToJs(node.attr); + } + } else { + return null; + } +} + +// messageBefore, message, name +// function call: print() -> "print" ([message]) ; print +// Module function: plt.show() -> "show plot" ([plot]) ; plt.show +// Method call: "test".title() -> "make" [str] "title case" () ; .title ; isMethod = true + +BlockMirrorTextToBlocks.prototype['ast_Call'] = function (node, parent) { + let func = node.func; + let args = node.args; + let keywords = node.keywords; + + // Can we make any guesses about this based on its name? + let signature = null; + let isMethod = false; + let premessage = ""; + let message = ""; + let name = ""; + let caller = null; + let colour = BlockMirrorTextToBlocks.COLOR.FUNCTIONS; + + if (func._astname === 'Name') { + message = name = Sk.ffi.remapToJs(func.id); + if (name in this.FUNCTION_SIGNATURES) { + signature = this.FUNCTION_SIGNATURES[Sk.ffi.remapToJs(func.id)]; + } + } else if (func._astname === 'Attribute') { + isMethod = true; + caller = func.value; + let potentialModule = this.getAsModule(caller); + let attributeName = Sk.ffi.remapToJs(func.attr); + message = "." + attributeName; + if (potentialModule in this.MODULE_FUNCTION_SIGNATURES) { + signature = this.MODULE_FUNCTION_SIGNATURES[potentialModule][attributeName]; + message = name = potentialModule + message; + isMethod = false; + } else if (attributeName in this.METHOD_SIGNATURES) { + signature = this.METHOD_SIGNATURES[attributeName]; + name = message; + } else { + name = message; + } + } else { + isMethod = true; + message = ""; + name = ""; + caller = func; + // (lambda x: x)() + } + let returns = true; + + if (signature !== null) { + if ('returns' in signature) { + returns = signature.returns; + } + if ('message' in signature) { + message = signature.message; + } + if ('premessage' in signature) { + premessage = signature.premessage; + } + if ('colour' in signature) { + colour = signature.colour; + } + } + + returns = returns || (parent._astname !== 'Expr'); + + let argumentsNormal = {}; + // TODO: do I need to be limiting only the *args* length, not keywords? + let argumentsMutation = { + "@arguments": (args !== null ? args.length : 0) + + (keywords !== null ? keywords.length : 0), + "@returns": returns, + "@parameters": true, + "@method": isMethod, + "@name": name, + "@message": message, + "@premessage": premessage, + "@colour": colour + }; + // Handle arguments + let overallI = 0; + if (args !== null) { + for (let i = 0; i < args.length; i += 1, overallI += 1) { + argumentsNormal["ARG" + overallI] = this.convert(args[i], node); + argumentsMutation["UNKNOWN_ARG:" + overallI] = null; + } + } + if (keywords !== null) { + for (let i = 0; i < keywords.length; i += 1, overallI += 1) { + let keyword = keywords[i]; + let arg = keyword.arg; + let value = keyword.value; + if (arg === null) { + argumentsNormal["ARG" + overallI] = this.convert(value, node); + argumentsMutation["KWARGS:" + overallI] = null; + } else { + argumentsNormal["ARG" + overallI] = this.convert(value, node); + argumentsMutation["KEYWORD:" + Sk.ffi.remapToJs(arg)] = null; + } + } + } + // Build actual block + let newBlock; + if (isMethod) { + argumentsNormal['FUNC'] = this.convert(caller, node); + newBlock = BlockMirrorTextToBlocks.create_block("ast_Call", node.lineno, + {}, argumentsNormal, {inline: true}, argumentsMutation); + } else { + newBlock = BlockMirrorTextToBlocks.create_block("ast_Call", node.lineno, {}, + argumentsNormal, {inline: true}, argumentsMutation); + } + // Return as either statement or expression + if (returns) { + return newBlock; + } else { + return [newBlock]; + } +}; +Blockly.Blocks['ast_Raise'] = { + init: function () { + this.setInputsInline(true); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.EXCEPTIONS); + this.exc_ = true; + this.cause_ = false; + + this.appendDummyInput() + .appendField("raise"); + this.updateShape_(); + }, + updateShape_: function () { + if (this.exc_ && !this.getInput('EXC')) { + this.appendValueInput("EXC") + .setCheck(null); + } else if (!this.exc_ && this.getInput('EXC')) { + this.removeInput('EXC'); + } + if (this.cause_ && !this.getInput('CAUSE')) { + this.appendValueInput("CAUSE") + .setCheck(null) + .appendField("from"); + } else if (!this.cause_ && this.getInput('CAUSE')) { + this.removeInput('CAUSE'); + } + if (this.cause_ && this.exc_) { + this.moveInputBefore('EXC', 'CAUSE'); + } + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('exc', this.exc_); + container.setAttribute('cause', this.cause_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.exc_ = "true" === xmlElement.getAttribute('exc'); + this.cause_ = "true" === xmlElement.getAttribute('cause'); + this.updateShape_(); + }, +}; + +Blockly.Python['ast_Raise'] = function (block) { + if (this.exc_) { + let exc = Blockly.Python.valueToCode(block, 'EXC', Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + if (this.cause_) { + let cause = Blockly.Python.valueToCode(block, 'CAUSE', Blockly.Python.ORDER_NONE) + || Blockly.Python.blank; + return "raise " + exc + " from " + cause + "\n"; + } else { + return "raise " + exc + "\n"; + } + } else { + return "raise"+"\n"; + } +}; + +BlockMirrorTextToBlocks.prototype['ast_Raise'] = function (node, parent) { + var exc = node.exc; + var cause = node.cause; + let values = {}; + let hasExc = false, hasCause = false; + if (exc !== null) { + values['EXC'] = this.convert(exc, node); + hasExc = true; + } + if (cause !== null) { + values['CAUSE'] = this.convert(cause, node); + hasCause = true; + } + return BlockMirrorTextToBlocks.create_block("ast_Raise", node.lineno, {}, values, {}, { + '@exc': hasExc, + '@cause': hasCause + }); +}; +Blockly.Blocks['ast_Delete'] = { + init: function () { + this.setInputsInline(true); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.VARIABLES); + this.targetCount_ = 1; + + this.appendDummyInput() + .appendField("delete"); + this.updateShape_(); + }, + updateShape_: function () { + // Add new inputs. + for (var i = 0; i < this.targetCount_; i++) { + if (!this.getInput('TARGET' + i)) { + var input = this.appendValueInput('TARGET' + i); + if (i !== 0) { + input.appendField(',').setAlign(Blockly.ALIGN_RIGHT); + } + } + } + // Remove deleted inputs. + while (this.getInput('TARGET' + i)) { + this.removeInput('TARGET' + i); + i++; + } + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('targets', this.targetCount_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.targetCount_ = parseInt(xmlElement.getAttribute('targets'), 10); + this.updateShape_(); + }, +}; + +Blockly.Python['ast_Delete'] = function (block) { + // Create a list with any number of elements of any type. + var elements = new Array(block.targetCount_); + for (var i = 0; i < block.targetCount_; i++) { + elements[i] = Blockly.Python.valueToCode(block, 'TARGET' + i, + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + } + var code = 'del ' + elements.join(', ') + "\n"; + return code; +}; + +BlockMirrorTextToBlocks.prototype['ast_Delete'] = function (node, parent) { + let targets = node.targets; + + return BlockMirrorTextToBlocks.create_block("ast_Delete", node.lineno, {}, + this.convertElements("TARGET", targets, node), + { + "inline": "true", + }, { + "@targets": targets.length + }); +}; +Blockly.Blocks['ast_Subscript'] = { + init: function () { + this.setInputsInline(true); + this.setOutput(true); + this.setColour(BlockMirrorTextToBlocks.COLOR.SEQUENCES); + this.sliceKinds_ = ["I"]; + + this.appendValueInput("VALUE") + .setCheck(null); + this.appendDummyInput('OPEN_BRACKET') + .appendField("[",); + this.appendDummyInput('CLOSE_BRACKET') + .appendField("]",); + this.updateShape_(); + }, + setExistence: function (label, exist, isDummy) { + if (exist && !this.getInput(label)) { + if (isDummy) { + return this.appendDummyInput(label); + } else { + return this.appendValueInput(label); + } + } else if (!exist && this.getInput(label)) { + this.removeInput(label); + } + return null; + }, + createSlice_: function (i, kind) { + // , + let input = this.setExistence('COMMA' + i, i !== 0, true); + if (input) { + input.appendField(",") + } + // Single index + let isIndex = (kind.charAt(0) === 'I'); + input = this.setExistence('INDEX' + i, isIndex, false); + // First index + input = this.setExistence('SLICELOWER' + i, !isIndex && "1" === kind.charAt(1), false); + // First colon + input = this.setExistence('SLICECOLON' + i, !isIndex , true); + if (input) { + input.appendField(':').setAlign(Blockly.ALIGN_RIGHT); + } + // Second index + input = this.setExistence('SLICEUPPER' + i, !isIndex && "1" === kind.charAt(2), false); + // Second colon and third index + input = this.setExistence('SLICESTEP' + i, !isIndex && "1" === kind.charAt(3), false); + if (input) { + input.appendField(':').setAlign(Blockly.ALIGN_RIGHT); + } + }, + updateShape_: function () { + // Add new inputs. + for (var i = 0; i < this.sliceKinds_.length; i++) { + this.createSlice_(i, this.sliceKinds_[i]); + } + + for (let j = 0; j < i; j++) { + if (j !== 0) { + this.moveInputBefore('COMMA' + j, 'CLOSE_BRACKET'); + } + let kind = this.sliceKinds_[j]; + if (kind.charAt(0) === "I") { + this.moveInputBefore('INDEX' + j, 'CLOSE_BRACKET'); + } else { + if (kind.charAt(1) === "1") { + this.moveInputBefore("SLICELOWER" + j, 'CLOSE_BRACKET'); + } + this.moveInputBefore("SLICECOLON" + j, 'CLOSE_BRACKET'); + if (kind.charAt(2) === "1") { + this.moveInputBefore("SLICEUPPER" + j, 'CLOSE_BRACKET'); + } + if (kind.charAt(3) === "1") { + this.moveInputBefore("SLICESTEP" + j, 'CLOSE_BRACKET'); + } + } + } + + // Remove deleted inputs. + while (this.getInput('TARGET' + i) || this.getInput('SLICECOLON')) { + this.removeInput('COMMA'+i, true); + if (this.getInput('INDEX' + i)) { + this.removeInput('INDEX' + i); + } else { + this.removeInput('SLICELOWER' + i, true); + this.removeInput('SLICECOLON' + i, true); + this.removeInput('SLICEUPPER' + i, true); + this.removeInput('SLICESTEP' + i, true); + } + i++; + } + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + let container = document.createElement('mutation'); + for (let i = 0; i < this.sliceKinds_.length; i++) { + let parameter = document.createElement('arg'); + parameter.setAttribute('name', this.sliceKinds_[i]); + container.appendChild(parameter); + } + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.sliceKinds_ = []; + for (let i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() === 'arg') { + this.sliceKinds_.push(childNode.getAttribute('name')); + } + } + this.updateShape_(); + }, +}; + +Blockly.Python['ast_Subscript'] = function (block) { + // Create a list with any number of elements of any type. + let value = Blockly.Python.valueToCode(block, 'VALUE', + Blockly.Python.ORDER_MEMBER) || Blockly.Python.blank; + var slices = new Array(block.sliceKinds_.length); + for (let i = 0; i < block.sliceKinds_.length; i++) { + let kind = block.sliceKinds_[i]; + if (kind.charAt(0) === 'I') { + slices[i] = Blockly.Python.valueToCode(block, 'INDEX' + i, + Blockly.Python.ORDER_MEMBER) || Blockly.Python.blank; + } else { + slices[i] = ""; + if (kind.charAt(1) === '1') { + slices[i] += Blockly.Python.valueToCode(block, 'SLICELOWER' + i, + Blockly.Python.ORDER_MEMBER) || Blockly.Python.blank; + } + slices[i] += ":"; + if (kind.charAt(2) === '1') { + slices[i] += Blockly.Python.valueToCode(block, 'SLICEUPPER' + i, + Blockly.Python.ORDER_MEMBER) || Blockly.Python.blank; + } + if (kind.charAt(3) === '1') { + slices[i] += ":" + Blockly.Python.valueToCode(block, 'SLICESTEP' + i, + Blockly.Python.ORDER_MEMBER) || Blockly.Python.blank; + } + } + } + var code = value + '[' + slices.join(', ') + "]"; + return [code, Blockly.Python.ORDER_MEMBER]; +}; + +var isWeirdSliceCase = function(slice) { + return (slice.lower == null && slice.upper == null && + slice.step !== null && slice.step._astname === 'NameConstant' && + slice.step.value === Sk.builtin.none.none$); +} + +BlockMirrorTextToBlocks.prototype.addSliceDim = function (slice, i, values, mutations, node) { + let sliceKind = slice._astname; + if (sliceKind === "Index") { + values['INDEX' + i] = this.convert(slice.value, node); + mutations.push("I"); + } else if (sliceKind === "Slice") { + let L = "0", U = "0", S = "0"; + if (slice.lower !== null) { + values['SLICELOWER' + i] = this.convert(slice.lower, node); + L = "1"; + } + if (slice.upper !== null) { + values['SLICEUPPER' + i] = this.convert(slice.upper, node); + U = "1"; + } + if (slice.step !== null && !isWeirdSliceCase(slice)) { + values['SLICESTEP' + i] = this.convert(slice.step, node); + S = "1"; + } + mutations.push("S" + L + U + S); + } +} + +BlockMirrorTextToBlocks.prototype['ast_Subscript'] = function (node, parent) { + let value = node.value; + let slice = node.slice; + let ctx = node.ctx; + + let values = {'VALUE': this.convert(value, node)}; + let mutations = []; + + let sliceKind = slice._astname; + if (sliceKind === "ExtSlice") { + for (let i = 0; i < slice.dims.length; i += 1) { + let dim = slice.dims[i]; + this.addSliceDim(dim, i, values, mutations, node); + } + } else { + this.addSliceDim(slice, 0, values, mutations, node); + } + return BlockMirrorTextToBlocks.create_block("ast_Subscript", node.lineno, {}, + values, {"inline": "true"}, {"arg": mutations}); +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_comprehensionFor", + "message0": "for %1 in %2", + "args0": [ + {"type": "input_value", "name": "TARGET"}, + {"type": "input_value", "name": "ITER"} + ], + "inputsInline": true, + "output": "ComprehensionFor", + "colour": BlockMirrorTextToBlocks.COLOR.SEQUENCES +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_comprehensionIf", + "message0": "if %1", + "args0": [ + {"type": "input_value", "name": "TEST"} + ], + "inputsInline": true, + "output": "ComprehensionIf", + "colour": BlockMirrorTextToBlocks.COLOR.SEQUENCES +}); + +Blockly.Blocks['ast_Comp_create_with_container'] = { + /** + * Mutator block for dict container. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.SEQUENCES); + this.appendDummyInput() + .appendField('Add new comprehensions below'); + this.appendDummyInput() + .appendField(' For clause'); + this.appendStatementInput('STACK'); + this.contextMenu = false; + } +}; + +Blockly.Blocks['ast_Comp_create_with_for'] = { + /** + * Mutator block for adding items. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.SEQUENCES); + this.appendDummyInput() + .appendField('For clause'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.contextMenu = false; + } +}; + +Blockly.Blocks['ast_Comp_create_with_if'] = { + /** + * Mutator block for adding items. + * @this Blockly.Block + */ + init: function () { + this.setColour(BlockMirrorTextToBlocks.COLOR.SEQUENCES); + this.appendDummyInput() + .appendField('If clause'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.contextMenu = false; + } +}; + +BlockMirrorTextToBlocks.COMP_SETTINGS = { + 'ListComp': {start: '[', end: ']', color: BlockMirrorTextToBlocks.COLOR.LIST}, + 'SetComp': {start: '{', end: '}', color: BlockMirrorTextToBlocks.COLOR.SET}, + 'GeneratorExp': {start: '(', end: ')', color: BlockMirrorTextToBlocks.COLOR.SEQUENCES}, + 'DictComp': {start: '{', end: '}', color: BlockMirrorTextToBlocks.COLOR.DICTIONARY}, +}; + +['ListComp', 'SetComp', 'GeneratorExp', 'DictComp'].forEach(function (kind) { + Blockly.Blocks['ast_' + kind] = { + /** + * Block for creating a dict with any number of elements of any type. + * @this Blockly.Block + */ + init: function () { + this.setStyle('loop_blocks'); + this.setColour(BlockMirrorTextToBlocks.COMP_SETTINGS[kind].color); + this.itemCount_ = 3; + let input = this.appendValueInput("ELT") + .appendField(BlockMirrorTextToBlocks.COMP_SETTINGS[kind].start); + if (kind === 'DictComp') { + input.setCheck('DictPair'); + } + this.appendDummyInput("END_BRACKET") + .appendField(BlockMirrorTextToBlocks.COMP_SETTINGS[kind].end); + this.updateShape_(); + this.setOutput(true); + this.setMutator(new Blockly.Mutator(['ast_Comp_create_with_for', 'ast_Comp_create_with_if'])); + }, + /** + * Create XML to represent dict inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('items', this.itemCount_); + return container; + }, + /** + * Parse XML to restore the dict inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); + this.updateShape_(); + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this Blockly.Block + */ + decompose: function (workspace) { + var containerBlock = workspace.newBlock('ast_Comp_create_with_container'); + containerBlock.initSvg(); + var connection = containerBlock.getInput('STACK').connection; + let generators = []; + for (var i = 1; i < this.itemCount_; i++) { + let generator = this.getInput('GENERATOR' + i).connection; + let createName; + if (generator.targetConnection.getSourceBlock().type === 'ast_comprehensionIf') { + createName = 'ast_Comp_create_with_if'; + } else if (generator.targetConnection.getSourceBlock().type === 'ast_comprehensionFor') { + createName = 'ast_Comp_create_with_for'; + } else { + throw Error("Unknown block type: " + generator.targetConnection.getSourceBlock().type); + } + var itemBlock = workspace.newBlock(createName); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + generators.push(itemBlock); + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + compose: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + // Count number of inputs. + var connections = [containerBlock.valueConnection_]; + let blockTypes = ['ast_Comp_create_with_for']; + while (itemBlock) { + connections.push(itemBlock.valueConnection_); + blockTypes.push(itemBlock.type); + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + // Disconnect any children that don't belong. + for (var i = 1; i < this.itemCount_; i++) { + var connection = this.getInput('GENERATOR' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) === -1) { + let connectedBlock = connection.getSourceBlock(); + if (connectedBlock.type === 'ast_comprehensionIf') { + let testField = connectedBlock.getInput('TEST'); + if (testField.connection.targetConnection) { + testField.connection.targetConnection.getSourceBlock().unplug(true); + } + } else if (connectedBlock.type === 'ast_comprehensionFor') { + let iterField = connectedBlock.getInput('ITER'); + if (iterField.connection.targetConnection) { + iterField.connection.targetConnection.getSourceBlock().unplug(true); + } + let targetField = connectedBlock.getInput('TARGET'); + if (targetField.connection.targetConnection) { + targetField.connection.targetConnection.getSourceBlock().unplug(true); + } + } else { + throw Error("Unknown block type: " + connectedBlock.type); + } + connection.disconnect(); + connection.getSourceBlock().dispose(); + } + } + this.itemCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (var i = 1; i < this.itemCount_; i++) { + Blockly.Mutator.reconnect(connections[i], this, 'GENERATOR' + i); + // TODO: glitch when inserting into middle, deletes children values + if (!connections[i]) { + let createName; + if (blockTypes[i] === 'ast_Comp_create_with_if') { + createName = 'ast_comprehensionIf'; + } else if (blockTypes[i] === 'ast_Comp_create_with_for') { + createName = 'ast_comprehensionFor'; + } else { + throw Error("Unknown block type: " + blockTypes[i]); + } + let itemBlock = this.workspace.newBlock(createName); + itemBlock.setDeletable(false); + itemBlock.setMovable(false); + itemBlock.initSvg(); + this.getInput('GENERATOR' + i).connection.connect(itemBlock.outputConnection); + itemBlock.render(); + //this.get(itemBlock, 'ADD'+i) + } + } + }, + /** + * Store pointers to any connected child blocks. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + saveConnections: function (containerBlock) { + containerBlock.valueConnection_ = this.getInput('GENERATOR0').connection.targetConnection; + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + var i = 1; + while (itemBlock) { + var input = this.getInput('GENERATOR' + i); + itemBlock.valueConnection_ = input && input.connection.targetConnection; + i++; + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + }, + /** + * Modify this block to have the correct number of inputs. + * @private + * @this Blockly.Block + */ + updateShape_: function () { + // Add new inputs. + for (var i = 0; i < this.itemCount_; i++) { + if (!this.getInput('GENERATOR' + i)) { + let input = this.appendValueInput('GENERATOR' + i); + if (i === 0) { + input.setCheck("ComprehensionFor"); + } else { + input.setCheck(["ComprehensionFor", "ComprehensionIf"]); + } + this.moveInputBefore('GENERATOR' + i, 'END_BRACKET'); + } + } + // Remove deleted inputs. + while (this.getInput('GENERATOR' + i)) { + this.removeInput('GENERATOR' + i); + i++; + } + } + }; + + Blockly.Python['ast_' + kind] = function (block) { + // elt + let elt; + if (kind === 'DictComp') { + let child = block.getInputTargetBlock('ELT'); + if (child === null || child.type !== 'ast_DictItem') { + elt = (Blockly.Python.blank + ": " + Blockly.Python.blank); + } else { + let key = Blockly.Python.valueToCode(child, 'KEY', Blockly.Python.ORDER_NONE) || + Blockly.Python.blank; + let value = Blockly.Python.valueToCode(child, 'VALUE', Blockly.Python.ORDER_NONE) || + Blockly.Python.blank; + elt = (key + ": " + value); + } + } else { + elt = Blockly.Python.valueToCode(block, 'ELT', Blockly.Python.ORDER_NONE) || + Blockly.Python.blank; + } + // generators + let elements = new Array(block.itemCount_); + const BAD_DEFAULT = (elt + " for " + Blockly.Python.blank + " in" + Blockly.Python.blank); + for (var i = 0; i < block.itemCount_; i++) { + let child = block.getInputTargetBlock('GENERATOR' + i); + if (child === null) { + elements[i] = BAD_DEFAULT; + } else if (child.type === 'ast_comprehensionIf') { + let test = Blockly.Python.valueToCode(child, 'TEST', Blockly.Python.ORDER_NONE) || + Blockly.Python.blank; + elements[i] = ("if " + test); + } else if (child.type === 'ast_comprehensionFor') { + let target = Blockly.Python.valueToCode(child, 'TARGET', Blockly.Python.ORDER_NONE) || + Blockly.Python.blank; + let iter = Blockly.Python.valueToCode(child, 'ITER', Blockly.Python.ORDER_NONE) || + Blockly.Python.blank; + elements[i] = ("for " + target + " in " + iter); + } else { + elements[i] = BAD_DEFAULT; + } + } + // Put it all together + let code = BlockMirrorTextToBlocks.COMP_SETTINGS[kind].start + + elt + " " + elements.join(' ') + + BlockMirrorTextToBlocks.COMP_SETTINGS[kind].end; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + + BlockMirrorTextToBlocks.prototype['ast_' + kind] = function (node, parent) { + let generators = node.generators; + + let elements = {}; + if (kind === 'DictComp') { + let key = node.key; + let value = node.value; + elements["ELT"] = BlockMirrorTextToBlocks.create_block("ast_DictItem", node.lineno, {}, + { + "KEY": this.convert(key, node), + "VALUE": this.convert(value, node) + }, + { + "inline": "true", + 'deletable': "false", + "movable": "false" + }); + } else { + let elt = node.elt; + elements["ELT"] = this.convert(elt, node); + } + let DEFAULT_SETTINGS = { + "inline": "true", + 'deletable': "false", + "movable": "false" + }; + let g = 0; + for (let i = 0; i < generators.length; i++) { + let target = generators[i].target; + let iter = generators[i].iter; + let ifs = generators[i].ifs; + let is_async = generators[i].is_async; + elements["GENERATOR" + g] = BlockMirrorTextToBlocks.create_block("ast_comprehensionFor", node.lineno, {}, + { + "ITER": this.convert(iter, node), + "TARGET": this.convert(target, node) + }, + DEFAULT_SETTINGS); + g += 1; + if (ifs) { + for (let j = 0; j < ifs.length; j++) { + elements["GENERATOR" + g] = BlockMirrorTextToBlocks.create_block("ast_comprehensionIf", node.lineno, {}, + { + "TEST": this.convert(ifs[j], node) + }, + DEFAULT_SETTINGS); + g += 1; + } + } + } + + return BlockMirrorTextToBlocks.create_block("ast_" + kind, node.lineno, {}, + elements, + { + "inline": "false" + }, { + "@items": g + }); + }; + +}); +// TODO: what if a user deletes a parameter through the context menu? + +// The mutator container +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_FunctionHeaderMutator", + "message0": "Setup parameters below: %1 %2 returns %3", + "args0": [ + {"type": "input_dummy"}, + {"type": "input_statement", "name": "STACK", "align": "RIGHT"}, + {"type": "field_checkbox", "name": "RETURNS", "checked": true, "align": "RIGHT"} + ], + "colour": BlockMirrorTextToBlocks.COLOR.FUNCTIONS, + "enableContextMenu": false +}); + +// The elements you can put into the mutator +[ + ['Parameter', 'Parameter', '', false, false], + ['ParameterType', 'Parameter with type', '', true, false], + ['ParameterDefault', 'Parameter with default value', '', false, true], + ['ParameterDefaultType', 'Parameter with type and default value', '', true, true], + ['ParameterVararg', 'Variable length parameter', '*', false, false], + ['ParameterVarargType', 'Variable length parameter with type', '*', true, false], + ['ParameterKwarg', 'Keyworded Variable length parameter', '**', false], + ['ParameterKwargType', 'Keyworded Variable length parameter with type', '**', true, false], +].forEach(function (parameterTypeTuple) { + let parameterType = parameterTypeTuple[0], + parameterDescription = parameterTypeTuple[1], + parameterPrefix = parameterTypeTuple[2], + parameterTyped = parameterTypeTuple[3], + parameterDefault = parameterTypeTuple[4]; + BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_FunctionMutant" + parameterType, + "message0": parameterDescription, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.FUNCTIONS, + "enableContextMenu": false + }); + let realParameterBlock = { + "type": "ast_Function" + parameterType, + "output": "Parameter", + "message0": parameterPrefix + (parameterPrefix ? ' ' : '') + "%1", + "args0": [{"type": "field_variable", "name": "NAME", "variable": "param"}], + "colour": BlockMirrorTextToBlocks.COLOR.FUNCTIONS, + "enableContextMenu": false, + "inputsInline": (parameterTyped && parameterDefault), + }; + if (parameterTyped) { + realParameterBlock['message0'] += " : %2"; + realParameterBlock['args0'].push({"type": "input_value", "name": "TYPE"}); + } + if (parameterDefault) { + realParameterBlock['message0'] += " = %" + (parameterTyped ? 3 : 2); + realParameterBlock['args0'].push({"type": "input_value", "name": "DEFAULT"}); + } + BlockMirrorTextToBlocks.BLOCKS.push(realParameterBlock); + + Blockly.Python["ast_Function" + parameterType] = function (block) { + let name = Blockly.Python.variableDB_.getName(block.getFieldValue('NAME'), + Blockly.Variables.NAME_TYPE); + let typed = ""; + if (parameterTyped) { + typed = ": " + Blockly.Python.valueToCode(block, 'TYPE', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + } + let defaulted = ""; + if (parameterDefault) { + defaulted = "=" + Blockly.Python.valueToCode(block, 'DEFAULT', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + } + return [parameterPrefix + name + typed + defaulted, Blockly.Python.ORDER_ATOMIC]; + } +}); + +// TODO: Figure out an elegant "complexity" flag feature to allow different levels of Mutators + +Blockly.Blocks['ast_FunctionDef'] = { + init: function () { + this.appendDummyInput() + .appendField("define") + .appendField(new Blockly.FieldTextInput("function"), "NAME"); + this.decoratorsCount_ = 0; + this.parametersCount_ = 0; + this.hasReturn_ = false; + this.mutatorComplexity_ = 0; + this.appendStatementInput("BODY") + .setCheck(null); + this.setInputsInline(false); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.FUNCTIONS); + this.updateShape_(); + this.setMutator(new Blockly.Mutator(['ast_FunctionMutantParameter', + 'ast_FunctionMutantParameterType'])); + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('decorators', this.decoratorsCount_); + container.setAttribute('parameters', this.parametersCount_); + container.setAttribute('returns', this.hasReturn_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.decoratorsCount_ = parseInt(xmlElement.getAttribute('decorators'), 10); + this.parametersCount_ = parseInt(xmlElement.getAttribute('parameters'), 10); + this.hasReturn_ = "true" === xmlElement.getAttribute('returns'); + this.updateShape_(); + }, + setReturnAnnotation_: function (status) { + let currentReturn = this.getInput('RETURNS'); + if (status) { + if (!currentReturn) { + this.appendValueInput("RETURNS") + .setCheck(null) + .setAlign(Blockly.ALIGN_RIGHT) + .appendField("returns"); + } + this.moveInputBefore('RETURNS', 'BODY'); + } else if (!status && currentReturn) { + this.removeInput('RETURNS'); + } + this.hasReturn_ = status; + }, + updateShape_: function () { + // Set up decorators and parameters + let block = this; + let position = 1; + [ + ['DECORATOR', 'decoratorsCount_', null, 'decorated by'], + ['PARAMETER', 'parametersCount_', 'Parameter', 'parameters:'] + ].forEach(function (childTypeTuple) { + let childTypeName = childTypeTuple[0], + countVariable = childTypeTuple[1], + inputCheck = childTypeTuple[2], + childTypeMessage = childTypeTuple[3]; + for (var i = 0; i < block[countVariable]; i++) { + if (!block.getInput(childTypeName + i)) { + let input = block.appendValueInput(childTypeName + i) + .setCheck(inputCheck) + .setAlign(Blockly.ALIGN_RIGHT); + if (i === 0) { + input.appendField(childTypeMessage); + } + } + block.moveInputBefore(childTypeName + i, 'BODY'); + } + // Remove deleted inputs. + while (block.getInput(childTypeName + i)) { + block.removeInput(childTypeName + i); + i++; + } + }); + // Set up optional Returns annotation + this.setReturnAnnotation_(this.hasReturn_); + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this Blockly.Block + */ + decompose: function (workspace) { + var containerBlock = workspace.newBlock('ast_FunctionHeaderMutator'); + containerBlock.initSvg(); + + // Check/uncheck the allow statement box. + if (this.getInput('RETURNS')) { + containerBlock.setFieldValue( + this.hasReturn_ ? 'TRUE' : 'FALSE', 'RETURNS'); + } else { + // TODO: set up "canReturns" for lambda mode + //containerBlock.getField('RETURNS').setVisible(false); + } + + // Set up parameters + var connection = containerBlock.getInput('STACK').connection; + let parameters = []; + for (var i = 0; i < this.parametersCount_; i++) { + let parameter = this.getInput('PARAMETER' + i).connection; + let sourceType = parameter.targetConnection.getSourceBlock().type; + let createName = 'ast_FunctionMutant' + sourceType.substring('ast_Function'.length); + var itemBlock = workspace.newBlock(createName); + itemBlock.initSvg(); + connection.connect(itemBlock.previousConnection); + connection = itemBlock.nextConnection; + parameters.push(itemBlock); + } + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + compose: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + // Count number of inputs. + var connections = []; + let blockTypes = []; + while (itemBlock) { + connections.push(itemBlock.valueConnection_); + blockTypes.push(itemBlock.type); + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + // Disconnect any children that don't belong. + for (let i = 0; i < this.parametersCount_; i++) { + var connection = this.getInput('PARAMETER' + i).connection.targetConnection; + if (connection && connections.indexOf(connection) === -1) { + // Disconnect all children of this block + let connectedBlock = connection.getSourceBlock(); + for (let j = 0; j < connectedBlock.inputList.length; j++) { + let field = connectedBlock.inputList[j].connection; + if (field && field.targetConnection) { + field.targetConnection.getSourceBlock().unplug(true); + } + } + connection.disconnect(); + connection.getSourceBlock().dispose(); + } + } + this.parametersCount_ = connections.length; + this.updateShape_(); + // Reconnect any child blocks. + for (let i = 0; i < this.parametersCount_; i++) { + Blockly.Mutator.reconnect(connections[i], this, 'PARAMETER' + i); + if (!connections[i]) { + let createName = 'ast_Function' + blockTypes[i].substring('ast_FunctionMutant'.length); + let itemBlock = this.workspace.newBlock(createName); + itemBlock.setDeletable(false); + itemBlock.setMovable(false); + itemBlock.initSvg(); + this.getInput('PARAMETER' + i).connection.connect(itemBlock.outputConnection); + itemBlock.render(); + //this.get(itemBlock, 'ADD'+i) + } + } + // Show/hide the returns annotation + let hasReturns = containerBlock.getFieldValue('RETURNS'); + if (hasReturns !== null) { + hasReturns = hasReturns === 'TRUE'; + if (this.hasReturn_ != hasReturns) { + if (hasReturns) { + this.setReturnAnnotation_(true); + Blockly.Mutator.reconnect(this.returnConnection_, this, 'RETURNS'); + this.returnConnection_ = null; + } else { + let returnConnection = this.getInput('RETURNS').connection + this.returnConnection_ = returnConnection.targetConnection; + if (this.returnConnection_) { + let returnBlock = returnConnection.targetBlock(); + returnBlock.unplug(); + returnBlock.bumpNeighbours_(); + } + this.setReturnAnnotation_(false); + } + } + } + }, + /** + * Store pointers to any connected child blocks. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this Blockly.Block + */ + saveConnections: function (containerBlock) { + var itemBlock = containerBlock.getInputTargetBlock('STACK'); + var i = 0; + while (itemBlock) { + var input = this.getInput('PARAMETER' + i); + itemBlock.valueConnection_ = input && input.connection.targetConnection; + i++; + itemBlock = itemBlock.nextConnection && + itemBlock.nextConnection.targetBlock(); + } + }, +}; + +Blockly.Python['ast_FunctionDef'] = function (block) { + // Name + let name = Blockly.Python.variableDB_.getName(block.getFieldValue('NAME'), Blockly.Variables.NAME_TYPE); + // Decorators + let decorators = new Array(block.decoratorsCount_); + for (let i = 0; i < block.decoratorsCount_; i++) { + let decorator = (Blockly.Python.valueToCode(block, 'DECORATOR' + i, Blockly.Python.ORDER_NONE) || + Blockly.Python.blank); + decorators[i] = "@" + decorator + "\n"; + } + // Parameters + let parameters = new Array(block.parametersCount_); + for (let i = 0; i < block.parametersCount_; i++) { + parameters[i] = (Blockly.Python.valueToCode(block, 'PARAMETER' + i, Blockly.Python.ORDER_NONE) || + Blockly.Python.blank); + } + // Return annotation + let returns = ""; + if (this.hasReturn_) { + returns = " -> " + Blockly.Python.valueToCode(block, 'RETURNS', Blockly.Python.ORDER_NONE) || + Blockly.Python.blank; + } + // Body + let body = Blockly.Python.statementToCode(block, 'BODY') || Blockly.Python.PASS; + return decorators.join('') + "def " + name + "(" + parameters.join(', ') + ")" + returns + ":\n" + body; +}; + +BlockMirrorTextToBlocks.prototype.parseArg = function (arg, type, lineno, values, node) { + let settings = { + "movable": false, + "deletable": false + }; + if (arg.annotation === null) { + return BlockMirrorTextToBlocks.create_block(type, + lineno, {'NAME': Sk.ffi.remapToJs(arg.arg)}, values, settings); + } else { + values['TYPE'] = this.convert(arg.annotation, node); + return BlockMirrorTextToBlocks.create_block(type + "Type", + lineno, {'NAME': Sk.ffi.remapToJs(arg.arg)}, values, settings); + } +}; + +BlockMirrorTextToBlocks.prototype.parseArgs = function (args, values, lineno, node) { + let positional = args.args, + vararg = args.vararg, + kwonlyargs = args.kwonlyargs, + kwarg = args.kwarg, + defaults = args.defaults, + kw_defaults = args.kw_defaults; + let totalArgs = 0; + // args (positional) + if (positional !== null) { + // "If there are fewer defaults, they correspond to the last n arguments." + let defaultOffset = defaults ? defaults.length - positional.length : 0; + for (let i = 0; i < positional.length; i++) { + let childValues = {}; + let type = 'ast_FunctionParameter'; + if (defaults[defaultOffset + i]) { + childValues['DEFAULT'] = this.convert(defaults[defaultOffset + i], node); + type += "Default"; + } + values['PARAMETER' + totalArgs] = this.parseArg(positional[i], type, lineno, childValues, node); + totalArgs += 1; + } + } + // *arg + if (vararg !== null) { + values['PARAMETER' + totalArgs] = this.parseArg(vararg, 'ast_FunctionParameterVararg', lineno, {}, node); + totalArgs += 1; + } + // keyword arguments that must be referenced by name + if (kwonlyargs !== null) { + for (let i = 0; i < kwonlyargs.length; i++) { + let childValues = {}; + let type = 'ast_FunctionParameter'; + if (kw_defaults[i]) { + childValues['DEFAULT'] = this.convert(kw_defaults[i], node); + type += "Default"; + } + values['PARAMETER' + totalArgs] = this.parseArg(kwonlyargs[i], type, lineno, childValues, node); + totalArgs += 1; + } + } + // **kwarg + if (kwarg) { + values['PARAMETER' + totalArgs] = this.parseArg(kwarg, 'ast_FunctionParameterKwarg', lineno, {}, node); + totalArgs += 1; + } + + return totalArgs; +}; + +BlockMirrorTextToBlocks.prototype['ast_FunctionDef'] = function (node, parent) { + let name = node.name; + let args = node.args; + let body = node.body; + let decorator_list = node.decorator_list; + let returns = node.returns; + + let values = {}; + + if (decorator_list !== null) { + for (let i = 0; i < decorator_list.length; i++) { + values['DECORATOR' + i] = this.convert(decorator_list[i], node); + } + } + + let parsedArgs = 0; + if (args !== null) { + parsedArgs = this.parseArgs(args, values, node.lineno, node); + } + + let hasReturn = (returns !== null && + (returns._astname !== 'NameConstant' || returns.value !== Sk.builtin.none.none$)); + if (hasReturn) { + values['RETURNS'] = this.convert(returns, node); + } + + return BlockMirrorTextToBlocks.create_block("ast_FunctionDef", node.lineno, { + 'NAME': Sk.ffi.remapToJs(name) + }, + values, + { + "inline": "false" + }, { + "@decorators": (decorator_list === null ? 0 : decorator_list.length), + "@parameters": parsedArgs, + "@returns": hasReturn, + }, { + 'BODY': this.convertBody(body, node) + }); +}; + +Blockly.Blocks['ast_Lambda'] = { + init: function () { + this.appendDummyInput() + .appendField("lambda") + .setAlign(Blockly.ALIGN_RIGHT); + this.decoratorsCount_ = 0; + this.parametersCount_ = 0; + this.hasReturn_ = false; + this.appendValueInput("BODY") + .appendField("body") + .setAlign(Blockly.ALIGN_RIGHT) + .setCheck(null); + this.setInputsInline(false); + this.setOutput(true); + this.setColour(BlockMirrorTextToBlocks.COLOR.FUNCTIONS); + this.updateShape_(); + }, + mutationToDom: Blockly.Blocks['ast_FunctionDef'].mutationToDom, + domToMutation: Blockly.Blocks['ast_FunctionDef'].domToMutation, + updateShape_: Blockly.Blocks['ast_FunctionDef'].updateShape_, + setReturnAnnotation_: Blockly.Blocks['ast_FunctionDef'].setReturnAnnotation_, +}; + +Blockly.Python['ast_Lambda'] = function (block) { + // Parameters + let parameters = new Array(block.parametersCount_); + for (let i = 0; i < block.parametersCount_; i++) { + parameters[i] = (Blockly.Python.valueToCode(block, 'PARAMETER' + i, Blockly.Python.ORDER_NONE) || + Blockly.Python.blank); + } + // Body + let body = Blockly.Python.valueToCode(block, 'BODY', Blockly.Python.ORDER_LAMBDA) || Blockly.Python.PASS; + return ["lambda " + parameters.join(', ') + ": " + body, Blockly.Python.ORDER_LAMBDA]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Lambda'] = function (node, parent) { + let args = node.args; + let body = node.body; + + let values = {'BODY': this.convert(body, node)}; + + let parsedArgs = 0; + if (args !== null) { + parsedArgs = this.parseArgs(args, values, node.lineno); + } + + return BlockMirrorTextToBlocks.create_block("ast_Lambda", node.lineno, {}, + values, + { + "inline": "false" + }, { + "@decorators": 0, + "@parameters": parsedArgs, + "@returns": false, + }); +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_ReturnFull", + "message0": "return %1", + "args0": [ + {"type": "input_value", "name": "VALUE"} + ], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.FUNCTIONS, +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Return", + "message0": "return", + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.FUNCTIONS, +}); + +Blockly.Python['ast_Return'] = function (block) { + return "return\n"; +}; + +Blockly.Python['ast_ReturnFull'] = function (block) { + var value = Blockly.Python.valueToCode(block, 'VALUE', Blockly.Python.ORDER_ATOMIC) || Blockly.Python.blank; + return "return " + value + "\n"; +}; + +BlockMirrorTextToBlocks.prototype['ast_Return'] = function (node, parent) { + let value = node.value; + + if (value == null) { + return BlockMirrorTextToBlocks.create_block("ast_Return", node.lineno); + } else { + return BlockMirrorTextToBlocks.create_block("ast_ReturnFull", node.lineno, {}, { + "VALUE": this.convert(value, node) + }); + } +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_YieldFull", + "message0": "yield %1", + "args0": [ + {"type": "input_value", "name": "VALUE"} + ], + "inputsInline": false, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.FUNCTIONS, +}); + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Yield", + "message0": "yield", + "inputsInline": false, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.FUNCTIONS, +}); + +Blockly.Python['ast_Yield'] = function (block) { + return ["yield", Blockly.Python.ORDER_LAMBDA]; +}; + +Blockly.Python['ast_YieldFull'] = function (block) { + var value = Blockly.Python.valueToCode(block, 'VALUE', Blockly.Python.ORDER_LAMBDA) || Blockly.Python.blank; + return ["yield " + value, Blockly.Python.ORDER_LAMBDA]; +}; + +BlockMirrorTextToBlocks.prototype['ast_Yield'] = function (node, parent) { + let value = node.value; + + if (value == null) { + return BlockMirrorTextToBlocks.create_block("ast_Yield", node.lineno); + } else { + return BlockMirrorTextToBlocks.create_block("ast_YieldFull", node.lineno, {}, { + "VALUE": this.convert(value, node) + }); + } +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_YieldFrom", + "message0": "yield from %1", + "args0": [ + {"type": "input_value", "name": "VALUE"} + ], + "inputsInline": false, + "output": null, + "colour": BlockMirrorTextToBlocks.COLOR.FUNCTIONS, +}); + +Blockly.Python['ast_YieldFrom'] = function (block) { + var value = Blockly.Python.valueToCode(block, 'VALUE', Blockly.Python.ORDER_LAMBDA) || Blockly.Python.blank; + return ["yield from " + value, Blockly.Python.ORDER_LAMBDA]; +}; + +BlockMirrorTextToBlocks.prototype['ast_YieldFrom'] = function (node, parent) { + let value = node.value; + + return BlockMirrorTextToBlocks.create_block("ast_YieldFrom", node.lineno, {}, { + "VALUE": this.convert(value, node) + }); +}; +Blockly.Blocks['ast_Global'] = { + init: function () { + this.setInputsInline(true); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.VARIABLES); + this.nameCount_ = 1; + this.appendDummyInput('GLOBAL') + .appendField("make global", "START_GLOBALS"); + this.updateShape_(); + }, + updateShape_: function () { + let input = this.getInput("GLOBAL"); + // Update pluralization + if (this.getField('START_GLOBALS')) { + this.setFieldValue(this.nameCount_ > 1 ? "make globals" : "make global", "START_GLOBALS"); + } + // Update fields + for (var i = 0; i < this.nameCount_; i++) { + if (!this.getField('NAME' + i)) { + if (i !== 0) { + input.appendField(',').setAlign(Blockly.ALIGN_RIGHT); + } + input.appendField(new Blockly.FieldVariable("variable"), 'NAME' + i); + } + } + // Remove deleted fields. + while (this.getField('NAME' + i)) { + input.removeField('NAME' + i); + i++; + } + // Delete and re-add ending field + if (this.getField("END_GLOBALS")) { + input.removeField("END_GLOBALS"); + } + input.appendField("available", "END_GLOBALS"); + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('names', this.nameCount_); + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.nameCount_ = parseInt(xmlElement.getAttribute('names'), 10); + this.updateShape_(); + }, +}; + +Blockly.Python['ast_Global'] = function (block) { + // Create a list with any number of elements of any type. + let elements = new Array(block.nameCount_); + for (let i = 0; i < block.nameCount_; i++) { + elements[i] = Blockly.Python.variableDB_.getName(block.getFieldValue('NAME' + i), Blockly.Variables.NAME_TYPE); + } + return 'global ' + elements.join(', ') + "\n"; +}; + +BlockMirrorTextToBlocks.prototype['ast_Global'] = function (node, parent) { + let names = node.names; + + let fields = {}; + for (var i = 0; i < names.length; i++) { + fields["NAME" + i] = Sk.ffi.remapToJs(names[i]); + } + + return BlockMirrorTextToBlocks.create_block("ast_Global", node.lineno, + fields, + {}, { + "inline": "true", + }, { + "@names": names.length + }); +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Break", + "message0": "break", + "inputsInline": false, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.CONTROL, +}); + +Blockly.Python['ast_Break'] = function (block) { + return "break\n"; +}; + +BlockMirrorTextToBlocks.prototype['ast_Break'] = function (node, parent) { + return BlockMirrorTextToBlocks.create_block("ast_Break", node.lineno); +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Continue", + "message0": "continue", + "inputsInline": false, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.CONTROL, +}); + +Blockly.Python['ast_Continue'] = function (block) { + return "continue\n"; +}; + +BlockMirrorTextToBlocks.prototype['ast_Continue'] = function (node, parent) { + return BlockMirrorTextToBlocks.create_block("ast_Continue", node.lineno); +}; +BlockMirrorTextToBlocks.HANDLERS_CATCH_ALL = 0; +BlockMirrorTextToBlocks.HANDLERS_NO_AS = 1; +BlockMirrorTextToBlocks.HANDLERS_COMPLETE = 3; + +Blockly.Blocks['ast_Try'] = { + init: function () { + this.handlersCount_ = 0; + this.handlers_ = []; + this.hasElse_ = false; + this.hasFinally_ = false; + this.appendDummyInput() + .appendField("try:"); + this.appendStatementInput("BODY") + .setCheck(null) + .setAlign(Blockly.ALIGN_RIGHT); + this.setInputsInline(true); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.EXCEPTIONS); + this.updateShape_(); + }, + // TODO: Not mutable currently + updateShape_: function () { + for (let i = 0; i < this.handlersCount_; i++) { + if (this.handlers_[i] === BlockMirrorTextToBlocks.HANDLERS_CATCH_ALL) { + this.appendDummyInput() + .appendField('except'); + } else { + this.appendValueInput("TYPE"+i) + .setCheck(null) + .appendField("except"); + if (this.handlers_[i] === BlockMirrorTextToBlocks.HANDLERS_COMPLETE) { + this.appendDummyInput() + .appendField("as") + .appendField(new Blockly.FieldVariable("item"), "NAME"+i); + } + } + this.appendStatementInput("HANDLER"+i) + .setCheck(null); + } + if (this.hasElse_) { + this.appendDummyInput() + .appendField("else:"); + this.appendStatementInput("ORELSE") + .setCheck(null); + } + if (this.hasFinally_) { + this.appendDummyInput() + .appendField("finally:"); + this.appendStatementInput("FINALBODY") + .setCheck(null); + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + let container = document.createElement('mutation'); + container.setAttribute('orelse', this.hasElse_); + container.setAttribute('finalbody', this.hasFinally_); + container.setAttribute('handlers', this.handlersCount_); + for (let i = 0; i < this.handlersCount_; i++) { + let parameter = document.createElement('arg'); + parameter.setAttribute('name', this.handlers_[i]); + container.appendChild(parameter); + } + return container; + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.hasElse_ = "true" === xmlElement.getAttribute('orelse'); + this.hasFinally_ = "true" === xmlElement.getAttribute('finalbody'); + this.handlersCount_ = parseInt(xmlElement.getAttribute('handlers'), 10); + this.handlers_ = []; + for (let i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() === 'arg') { + this.handlers_.push(parseInt(childNode.getAttribute('name'), 10)); + } + } + this.updateShape_(); + }, +}; + +Blockly.Python['ast_Try'] = function (block) { + // Try: + let body = Blockly.Python.statementToCode(block, 'BODY') || Blockly.Python.PASS; + // Except clauses + var handlers = new Array(block.handlersCount_); + for (let i = 0; i < block.handlersCount_; i++) { + let level = block.handlers_[i]; + let clause = "except"; + if (level !== BlockMirrorTextToBlocks.HANDLERS_CATCH_ALL) { + clause += " " + Blockly.Python.valueToCode(block, 'TYPE' + i, + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + if (level === BlockMirrorTextToBlocks.HANDLERS_COMPLETE) { + clause += " as " + Blockly.Python.variableDB_.getName(block.getFieldValue('NAME' + i), + Blockly.Variables.NAME_TYPE); + } + } + clause += ":\n" + (Blockly.Python.statementToCode(block, 'HANDLER' + i) || Blockly.Python.PASS); + handlers[i] = clause; + } + // Orelse: + let orelse = ""; + if (this.hasElse_) { + orelse = "else:\n" + (Blockly.Python.statementToCode(block, 'ORELSE') || Blockly.Python.PASS); + } + // Finally: + let finalbody = ""; + if (this.hasFinally_) { + finalbody = "finally:\n" + (Blockly.Python.statementToCode(block, 'FINALBODY') || Blockly.Python.PASS); + } + return "try:\n" + body + handlers.join("") + orelse + finalbody; +}; + +BlockMirrorTextToBlocks.prototype['ast_Try'] = function (node, parent) { + let body = node.body; + let handlers = node.handlers; + let orelse = node.orelse; + let finalbody = node.finalbody; + + let fields = {}; + let values = {}; + let mutations = { + "@ORELSE": orelse !== null && orelse.length > 0, + "@FINALBODY": finalbody !== null && finalbody.length > 0, + "@HANDLERS": handlers.length + }; + + let statements = {"BODY": this.convertBody(body, node)}; + if (orelse !== null) { + statements['ORELSE'] = this.convertBody(orelse, node); + } + if (finalbody !== null && finalbody.length) { + statements['FINALBODY'] = this.convertBody(finalbody, node); + } + + let handledLevels = []; + for (let i = 0; i < handlers.length; i++) { + let handler = handlers[i]; + statements["HANDLER" + i] = this.convertBody(handler.body, node); + if (handler.type === null) { + handledLevels.push(BlockMirrorTextToBlocks.HANDLERS_CATCH_ALL); + } else { + values["TYPE" + i] = this.convert(handler.type, node); + if (handler.name === null) { + handledLevels.push(BlockMirrorTextToBlocks.HANDLERS_NO_AS); + } else { + handledLevels.push(BlockMirrorTextToBlocks.HANDLERS_COMPLETE); + fields["NAME" + i] = Sk.ffi.remapToJs(handler.name.id); + } + } + } + + mutations["ARG"] = handledLevels; + + return BlockMirrorTextToBlocks.create_block("ast_Try", node.lineno, fields, + values, {}, mutations, statements); +}; +Blockly.Blocks['ast_ClassDef'] = { + init: function () { + this.decorators_ = 0; + this.bases_ = 0; + this.keywords_ = 0; + this.appendDummyInput('HEADER') + .appendField("Class") + .appendField(new Blockly.FieldVariable("item"), "NAME"); + this.appendStatementInput("BODY") + .setCheck(null); + this.setInputsInline(false); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.OO); + this.updateShape_(); + }, + // TODO: Not mutable currently + updateShape_: function () { + for (let i = 0; i < this.decorators_; i++) { + let input = this.appendValueInput("DECORATOR" + i) + .setCheck(null) + .setAlign(Blockly.ALIGN_RIGHT); + if (i === 0) { + input.appendField("decorated by"); + } + this.moveInputBefore('DECORATOR' + i, 'BODY'); + } + for (let i = 0; i < this.bases_; i++) { + let input = this.appendValueInput("BASE" + i) + .setCheck(null) + .setAlign(Blockly.ALIGN_RIGHT); + if (i === 0) { + input.appendField("inherits from"); + } + this.moveInputBefore('BASE' + i, 'BODY'); + } + + for (let i = 0; i < this.keywords_; i++) { + this.appendValueInput("KEYWORDVALUE" + i) + .setCheck(null) + .setAlign(Blockly.ALIGN_RIGHT) + .appendField(new Blockly.FieldTextInput("metaclass"), "KEYWORDNAME" + i) + .appendField("="); + this.moveInputBefore('KEYWORDVALUE' + i, 'BODY'); + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + let container = document.createElement('mutation'); + container.setAttribute('decorators', this.decorators_); + container.setAttribute('bases', this.bases_); + container.setAttribute('keywords', this.keywords_); + return container; + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.decorators_ = parseInt(xmlElement.getAttribute('decorators'), 10); + this.bases_ = parseInt(xmlElement.getAttribute('bases'), 10); + this.keywords_ = parseInt(xmlElement.getAttribute('keywords'), 10); + this.updateShape_(); + }, +}; + +Blockly.Python['ast_ClassDef'] = function (block) { + // Name + let name = Blockly.Python.variableDB_.getName(block.getFieldValue('NAME'), Blockly.Variables.NAME_TYPE); + // Decorators + let decorators = new Array(block.decorators_); + for (let i = 0; i < block.decorators_; i++) { + let decorator = (Blockly.Python.valueToCode(block, 'DECORATOR' + i, Blockly.Python.ORDER_NONE) || + Blockly.Python.blank); + decorators[i] = "@" + decorator + "\n"; + } + // Bases + let bases = new Array(block.bases_); + for (let i = 0; i < block.bases_; i++) { + bases[i] = (Blockly.Python.valueToCode(block, 'BASE' + i, Blockly.Python.ORDER_NONE) || + Blockly.Python.blank); + } + // Keywords + let keywords = new Array(block.keywords_); + for (let i = 0; i < block.keywords_; i++) { + let name = block.getFieldValue('KEYWORDNAME' + i); + let value = (Blockly.Python.valueToCode(block, 'KEYWORDVALUE' + i, Blockly.Python.ORDER_NONE) || + Blockly.Python.blank); + if (name == '**') { + keywords[i] = '**' + value; + } else { + keywords[i] = name + '=' + value; + } + } + // Body: + let body = Blockly.Python.statementToCode(block, 'BODY') || Blockly.Python.PASS; + // Put it together + let arguments = (bases.concat(keywords)); + arguments = (arguments.length === 0) ? "" : "(" + arguments.join(', ') + ")"; + return decorators.join('') + "class " + name + arguments + ":\n" + body; +} +; + +BlockMirrorTextToBlocks.prototype['ast_ClassDef'] = function (node, parent) { + let name = node.name; + let bases = node.bases; + let keywords = node.keywords; + let body = node.body; + let decorator_list = node.decorator_list; + + let values = {}; + let fields = {'NAME': Sk.ffi.remapToJs(name)}; + + if (decorator_list !== null) { + for (let i = 0; i < decorator_list.length; i++) { + values['DECORATOR' + i] = this.convert(decorator_list[i], node); + } + } + + if (bases !== null) { + for (let i = 0; i < bases.length; i++) { + values['BASE' + i] = this.convert(bases[i], node); + } + } + + if (keywords !== null) { + for (let i = 0; i < keywords.length; i++) { + values['KEYWORDVALUE' + i] = this.convert(keywords[i].value, node); + let arg = keywords[i].arg; + if (arg === null) { + fields['KEYWORDNAME' + i] = "**"; + } else { + fields['KEYWORDNAME' + i] = Sk.ffi.remapToJs(arg); + } + } + } + + return BlockMirrorTextToBlocks.create_block("ast_ClassDef", node.lineno, fields, + values, + { + "inline": "false" + }, { + "@decorators": (decorator_list === null ? 0 : decorator_list.length), + "@bases": (bases === null ? 0 : bases.length), + "@keywords": (keywords === null ? 0 : keywords.length), + }, { + 'BODY': this.convertBody(body, node) + }); +}; + +// TODO: direct imports are not variables, because you can do stuff like: +// import os.path +// What should the variable be? Blockly will mangle it, but we should really be +// doing something more complicated here with namespaces (probably make `os` the +// variable and have some kind of list of attributes. But that's in the fading zone. +Blockly.Blocks['ast_Import'] = { + init: function () { + this.nameCount_ = 1; + this.from_ = false; + this.regulars_ = [true]; + this.setInputsInline(false); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.PYTHON); + this.updateShape_(); + }, + // TODO: Not mutable currently + updateShape_: function () { + // Possible FROM part + if (this.from_ && !this.getInput('FROM')) { + this.appendDummyInput('FROM') + .setAlign(Blockly.ALIGN_RIGHT) + .appendField('from') + .appendField(new Blockly.FieldTextInput("module"), "MODULE"); + } else if (!this.from_ && this.getInput('FROM')) { + this.removeInput('FROM'); + } + // Import clauses + for (var i = 0; i < this.nameCount_; i++) { + let input = this.getInput('CLAUSE' + i); + if (!input) { + input = this.appendDummyInput('CLAUSE' + i) + .setAlign(Blockly.ALIGN_RIGHT); + if (i === 0) { + input.appendField("import"); + } + input.appendField(new Blockly.FieldTextInput("default"), "NAME" + i) + } + if (this.regulars_[i] && this.getField('AS' + i)) { + input.removeField('AS' + i); + input.removeField('ASNAME' + i); + } else if (!this.regulars_[i] && !this.getField('AS' + i)) { + input.appendField("as", 'AS' + i) + .appendField(new Blockly.FieldVariable("alias"), "ASNAME" + i); + } + } + // Remove deleted inputs. + while (this.getInput('CLAUSE' + i)) { + this.removeInput('CLAUSE' + i); + i++; + } + // Reposition everything + if (this.from_ && this.nameCount_ > 0) { + this.moveInputBefore('FROM', 'CLAUSE0'); + } + for (i = 0; i + 1 < this.nameCount_; i++) { + this.moveInputBefore('CLAUSE' + i, 'CLAUSE' + (i + 1)); + } + }, + /** + * Create XML to represent the (non-editable) name and arguments. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + let container = document.createElement('mutation'); + container.setAttribute('names', this.nameCount_); + container.setAttribute('from', this.from_); + for (let i = 0; i < this.nameCount_; i++) { + let parameter = document.createElement('regular'); + parameter.setAttribute('name', this.regulars_[i]); + container.appendChild(parameter); + } + return container; + }, + /** + * Parse XML to restore the (non-editable) name and parameters. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.nameCount_ = parseInt(xmlElement.getAttribute('names'), 10); + this.from_ = "true" === xmlElement.getAttribute('from'); + this.regulars_ = []; + for (let i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() === 'regular') { + this.regulars_.push("true" === childNode.getAttribute('name')); + } + } + this.updateShape_(); + }, +}; + +Blockly.Python['ast_Import'] = function (block) { + // Optional from part + let from = ""; + if (this.from_) { + from = "from "+block.getFieldValue('MODULE') + " "; + } + // Create a list with any number of elements of any type. + let elements = new Array(block.nameCount_); + for (let i = 0; i < block.nameCount_; i++) { + elements[i] = block.getFieldValue('NAME' + i); + if (!this.regulars_[i]) { + elements[i] += " as " + Blockly.Python.variableDB_.getName(block.getFieldValue('ASNAME' + i), Blockly.Variables.NAME_TYPE); + } + } + return from + 'import ' + elements.join(', ') + "\n"; +}; + +BlockMirrorTextToBlocks.prototype['ast_Import'] = function (node, parent) { + let names = node.names; + + let fields = {}; + let mutations = {'@names': names.length}; + + let regulars = []; + for (let i = 0; i < names.length; i++) { + fields["NAME" + i] = Sk.ffi.remapToJs(names[i].name); + let isRegular = (names[i].asname === null); + if (!isRegular) { + fields["ASNAME" + i] = Sk.ffi.remapToJs(names[i].asname); + } + regulars.push(isRegular); + } + mutations['regular'] = regulars; + + if (node._astname === 'ImportFrom') { + // acbart: GTS suggests module can be None for '.' but it's an empty string in Skulpt + mutations['@from'] = true; + fields['MODULE'] = ('.'.repeat(node.level)) + Sk.ffi.remapToJs(node.module); + } else { + mutations['@from'] = false; + } + + return BlockMirrorTextToBlocks.create_block("ast_Import", node.lineno, fields, + {}, {}, mutations); +}; + +// Alias ImportFrom because of big overlap +BlockMirrorTextToBlocks.prototype['ast_ImportFrom'] = BlockMirrorTextToBlocks.prototype['ast_Import']; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_WithItem", + "output": "WithItem", + "message0": "context %1", + "args0": [{"type": "input_value", "name": "CONTEXT"}], + "enableContextMenu": false, + "colour": BlockMirrorTextToBlocks.COLOR.CONTROL, + "inputsInline": false, +}); +Blockly.Python["ast_WithItem"] = function (block) { + let context = Blockly.Python.valueToCode(block, 'CONTEXT', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + return [context, Blockly.Python.ORDER_NONE]; +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_WithItemAs", + "output": "WithItem", + "message0": "context %1 as %2", + "args0": [{"type": "input_value", "name": "CONTEXT"}, + {"type": "input_value", "name": "AS"}], + "enableContextMenu": false, + "colour": BlockMirrorTextToBlocks.COLOR.CONTROL, + "inputsInline": true, +}); +Blockly.Python["ast_WithItemAs"] = function (block) { + let context = Blockly.Python.valueToCode(block, 'CONTEXT', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + let as = Blockly.Python.valueToCode(block, 'AS', + Blockly.Python.ORDER_NONE) || Blockly.Python.blank; + return [context + " as " + as, Blockly.Python.ORDER_NONE]; +}; + +Blockly.Blocks['ast_With'] = { + init: function () { + this.appendValueInput('ITEM0') + .appendField("with"); + this.appendStatementInput("BODY") + .setCheck(null); + this.itemCount_ = 1; + this.renames_ = [false]; + this.setInputsInline(false); + this.setPreviousStatement(true, null); + this.setNextStatement(true, null); + this.setColour(BlockMirrorTextToBlocks.COLOR.CONTROL); + this.updateShape_(); + }, + /** + * Create XML to represent list inputs. + * @return {!Element} XML storage element. + * @this Blockly.Block + */ + mutationToDom: function () { + var container = document.createElement('mutation'); + container.setAttribute('items', this.itemCount_); + for (let i = 0; i < this.itemCount_; i++) { + let parameter = document.createElement('as'); + parameter.setAttribute('name', this.renames_[i]); + container.appendChild(parameter); + } + return container; + }, + /** + * Parse XML to restore the list inputs. + * @param {!Element} xmlElement XML storage element. + * @this Blockly.Block + */ + domToMutation: function (xmlElement) { + this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10); + this.renames_ = []; + for (let i = 0, childNode; childNode = xmlElement.childNodes[i]; i++) { + if (childNode.nodeName.toLowerCase() === 'as') { + this.renames_.push("true" === childNode.getAttribute('name')); + } + } + this.updateShape_(); + }, + updateShape_: function () { + // With clauses + for (var i = 1; i < this.itemCount_; i++) { + let input = this.getInput('ITEM' + i); + if (!input) { + input = this.appendValueInput('ITEM' + i); + } + } + // Remove deleted inputs. + while (this.getInput('ITEM' + i)) { + this.removeInput('ITEM' + i); + i++; + } + // Reposition everything + for (i = 0; i < this.itemCount_; i++) { + this.moveInputBefore('ITEM' + i, 'BODY'); + } + }, +}; + +Blockly.Python['ast_With'] = function (block) { + // Contexts + let items = new Array(block.itemCount_); + for (let i = 0; i < block.itemCount_; i++) { + items[i] = (Blockly.Python.valueToCode(block, 'ITEM' + i, Blockly.Python.ORDER_NONE) || + Blockly.Python.blank); + } + // Body + let body = Blockly.Python.statementToCode(block, 'BODY') || Blockly.Python.PASS; + return "with " + items.join(', ') + ":\n" + body; +}; + +BlockMirrorTextToBlocks.prototype['ast_With'] = function (node, parent) { + let items = node.items; + let body = node.body; + + let values = {}; + let mutations = {"@items": items.length}; + + let renamedItems = []; + for (let i = 0; i < items.length; i++) { + let hasRename = items[i].optional_vars; + renamedItems.push(hasRename); + let innerValues = {'CONTEXT':this.convert(items[i].context_expr, node)}; + if (hasRename) { + innerValues['AS'] = this.convert(items[i].optional_vars, node); + values['ITEM'+i] = BlockMirrorTextToBlocks.create_block("ast_WithItemAs", node.lineno, + {}, innerValues, this.LOCKED_BLOCK); + } else { + values['ITEM'+i] = BlockMirrorTextToBlocks.create_block("ast_WithItem", node.lineno, + {}, innerValues, this.LOCKED_BLOCK); + } + } + mutations['as'] = renamedItems; + + return BlockMirrorTextToBlocks.create_block("ast_With", node.lineno, {}, + values, + { + "inline": "false" + }, mutations, { + 'BODY': this.convertBody(body, node) + }); +}; + +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Comment", + "message0": "# Comment: %1", + "args0": [{"type": "field_input", "name": "BODY", "text": "will be ignored"}], + "inputsInline": true, + "previousStatement": null, + "nextStatement": null, + "colour": BlockMirrorTextToBlocks.COLOR.PYTHON, +}); + +Blockly.Python['ast_Comment'] = function(block) { + var text_body = block.getFieldValue('BODY'); + return '#'+text_body+'\n'; +}; + +BlockMirrorTextToBlocks.prototype['ast_Comment'] = function (txt, lineno) { + var commentText = txt.slice(1); + /*if (commentText.length && commentText[0] === " ") { + commentText = commentText.substring(1); + }*/ + return BlockMirrorTextToBlocks.create_block("ast_Comment", lineno, { + "BODY": commentText + }) +}; +BlockMirrorTextToBlocks.BLOCKS.push({ + "type": "ast_Raw", + "message0": "Code Block: %1 %2", + "args0": [ + {"type": "input_dummy"}, + {"type": "field_multilinetext", "name": "TEXT", "value": ''} + ], + "colour": BlockMirrorTextToBlocks.COLOR.PYTHON, + "previousStatement": null, + "nextStatement": null, +}); + +Blockly.Python['ast_Raw'] = function (block) { + var code = block.getFieldValue('TEXT') + "\n"; + return code; +}; diff --git a/lib/codemirror/foldcode.js b/lib/codemirror/foldcode.js new file mode 100644 index 0000000..e146fb9 --- /dev/null +++ b/lib/codemirror/foldcode.js @@ -0,0 +1,152 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function doFold(cm, pos, options, force) { + if (options && options.call) { + var finder = options; + options = null; + } else { + var finder = getOption(cm, options, "rangeFinder"); + } + if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0); + var minSize = getOption(cm, options, "minFoldSize"); + + function getRange(allowFolded) { + var range = finder(cm, pos); + if (!range || range.to.line - range.from.line < minSize) return null; + var marks = cm.findMarksAt(range.from); + for (var i = 0; i < marks.length; ++i) { + if (marks[i].__isFold && force !== "fold") { + if (!allowFolded) return null; + range.cleared = true; + marks[i].clear(); + } + } + return range; + } + + var range = getRange(true); + if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) { + pos = CodeMirror.Pos(pos.line - 1, 0); + range = getRange(false); + } + if (!range || range.cleared || force === "unfold") return; + + var myWidget = makeWidget(cm, options); + CodeMirror.on(myWidget, "mousedown", function(e) { + myRange.clear(); + CodeMirror.e_preventDefault(e); + }); + var myRange = cm.markText(range.from, range.to, { + replacedWith: myWidget, + clearOnEnter: getOption(cm, options, "clearOnEnter"), + __isFold: true + }); + myRange.on("clear", function(from, to) { + CodeMirror.signal(cm, "unfold", cm, from, to); + }); + CodeMirror.signal(cm, "fold", cm, range.from, range.to); + } + + function makeWidget(cm, options) { + var widget = getOption(cm, options, "widget"); + if (typeof widget == "string") { + var text = document.createTextNode(widget); + widget = document.createElement("span"); + widget.appendChild(text); + widget.className = "CodeMirror-foldmarker"; + } else if (widget) { + widget = widget.cloneNode(true) + } + return widget; + } + + // Clumsy backwards-compatible interface + CodeMirror.newFoldFunction = function(rangeFinder, widget) { + return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; + }; + + // New-style interface + CodeMirror.defineExtension("foldCode", function(pos, options, force) { + doFold(this, pos, options, force); + }); + + CodeMirror.defineExtension("isFolded", function(pos) { + var marks = this.findMarksAt(pos); + for (var i = 0; i < marks.length; ++i) + if (marks[i].__isFold) return true; + }); + + CodeMirror.commands.toggleFold = function(cm) { + cm.foldCode(cm.getCursor()); + }; + CodeMirror.commands.fold = function(cm) { + cm.foldCode(cm.getCursor(), null, "fold"); + }; + CodeMirror.commands.unfold = function(cm) { + cm.foldCode(cm.getCursor(), null, "unfold"); + }; + CodeMirror.commands.foldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "fold"); + }); + }; + CodeMirror.commands.unfoldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold"); + }); + }; + + CodeMirror.registerHelper("fold", "combine", function() { + var funcs = Array.prototype.slice.call(arguments, 0); + return function(cm, start) { + for (var i = 0; i < funcs.length; ++i) { + var found = funcs[i](cm, start); + if (found) return found; + } + }; + }); + + CodeMirror.registerHelper("fold", "auto", function(cm, start) { + var helpers = cm.getHelpers(start, "fold"); + for (var i = 0; i < helpers.length; i++) { + var cur = helpers[i](cm, start); + if (cur) return cur; + } + }); + + var defaultOptions = { + rangeFinder: CodeMirror.fold.auto, + widget: "\u2194", + minFoldSize: 0, + scanUp: false, + clearOnEnter: true + }; + + CodeMirror.defineOption("foldOptions", null); + + function getOption(cm, options, name) { + if (options && options[name] !== undefined) + return options[name]; + var editorOptions = cm.options.foldOptions; + if (editorOptions && editorOptions[name] !== undefined) + return editorOptions[name]; + return defaultOptions[name]; + } + + CodeMirror.defineExtension("foldOption", function(options, name) { + return getOption(this, options, name); + }); +}); diff --git a/lib/codemirror/foldgutter.js b/lib/codemirror/foldgutter.js new file mode 100644 index 0000000..988c67c --- /dev/null +++ b/lib/codemirror/foldgutter.js @@ -0,0 +1,146 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./foldcode")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./foldcode"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("foldGutter", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.clearGutter(cm.state.foldGutter.options.gutter); + cm.state.foldGutter = null; + cm.off("gutterClick", onGutterClick); + cm.off("change", onChange); + cm.off("viewportChange", onViewportChange); + cm.off("fold", onFold); + cm.off("unfold", onFold); + cm.off("swapDoc", onChange); + } + if (val) { + cm.state.foldGutter = new State(parseOptions(val)); + updateInViewport(cm); + cm.on("gutterClick", onGutterClick); + cm.on("change", onChange); + cm.on("viewportChange", onViewportChange); + cm.on("fold", onFold); + cm.on("unfold", onFold); + cm.on("swapDoc", onChange); + } + }); + + var Pos = CodeMirror.Pos; + + function State(options) { + this.options = options; + this.from = this.to = 0; + } + + function parseOptions(opts) { + if (opts === true) opts = {}; + if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter"; + if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open"; + if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded"; + return opts; + } + + function isFolded(cm, line) { + var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0)); + for (var i = 0; i < marks.length; ++i) + if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i]; + } + + function marker(spec) { + if (typeof spec == "string") { + var elt = document.createElement("div"); + elt.className = spec + " CodeMirror-guttermarker-subtle"; + return elt; + } else { + return spec.cloneNode(true); + } + } + + function updateFoldInfo(cm, from, to) { + var opts = cm.state.foldGutter.options, cur = from; + var minSize = cm.foldOption(opts, "minFoldSize"); + var func = cm.foldOption(opts, "rangeFinder"); + cm.eachLine(from, to, function(line) { + var mark = null; + if (isFolded(cm, cur)) { + mark = marker(opts.indicatorFolded); + } else { + var pos = Pos(cur, 0); + var range = func && func(cm, pos); + if (range && range.to.line - range.from.line >= minSize) + mark = marker(opts.indicatorOpen); + } + cm.setGutterMarker(line, opts.gutter, mark); + ++cur; + }); + } + + function updateInViewport(cm) { + var vp = cm.getViewport(), state = cm.state.foldGutter; + if (!state) return; + cm.operation(function() { + updateFoldInfo(cm, vp.from, vp.to); + }); + state.from = vp.from; state.to = vp.to; + } + + function onGutterClick(cm, line, gutter) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + if (gutter != opts.gutter) return; + var folded = isFolded(cm, line); + if (folded) folded.clear(); + else cm.foldCode(Pos(line, 0), opts.rangeFinder); + } + + function onChange(cm) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + state.from = state.to = 0; + clearTimeout(state.changeUpdate); + state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600); + } + + function onViewportChange(cm) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + clearTimeout(state.changeUpdate); + state.changeUpdate = setTimeout(function() { + var vp = cm.getViewport(); + if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { + updateInViewport(cm); + } else { + cm.operation(function() { + if (vp.from < state.from) { + updateFoldInfo(cm, vp.from, state.from); + state.from = vp.from; + } + if (vp.to > state.to) { + updateFoldInfo(cm, state.to, vp.to); + state.to = vp.to; + } + }); + } + }, opts.updateViewportTimeSpan || 400); + } + + function onFold(cm, from) { + var state = cm.state.foldGutter; + if (!state) return; + var line = from.line; + if (line >= state.from && line < state.to) + updateFoldInfo(cm, line, line + 1); + } +}); diff --git a/lib/codemirror/fullscreen.css b/lib/codemirror/fullscreen.css new file mode 100644 index 0000000..437acd8 --- /dev/null +++ b/lib/codemirror/fullscreen.css @@ -0,0 +1,6 @@ +.CodeMirror-fullscreen { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + height: auto; + z-index: 9; +} diff --git a/lib/codemirror/fullscreen.js b/lib/codemirror/fullscreen.js new file mode 100644 index 0000000..eda7300 --- /dev/null +++ b/lib/codemirror/fullscreen.js @@ -0,0 +1,41 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("fullScreen", false, function(cm, val, old) { + if (old == CodeMirror.Init) old = false; + if (!old == !val) return; + if (val) setFullscreen(cm); + else setNormal(cm); + }); + + function setFullscreen(cm) { + var wrap = cm.getWrapperElement(); + cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset, + width: wrap.style.width, height: wrap.style.height}; + wrap.style.width = ""; + wrap.style.height = "auto"; + wrap.className += " CodeMirror-fullscreen"; + document.documentElement.style.overflow = "hidden"; + cm.refresh(); + } + + function setNormal(cm) { + var wrap = cm.getWrapperElement(); + wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, ""); + document.documentElement.style.overflow = ""; + var info = cm.state.fullScreenRestore; + wrap.style.width = info.width; wrap.style.height = info.height; + window.scrollTo(info.scrollLeft, info.scrollTop); + cm.refresh(); + } +}); diff --git a/lib/codemirror/python-hint.js b/lib/codemirror/python-hint.js new file mode 100644 index 0000000..60221b8 --- /dev/null +++ b/lib/codemirror/python-hint.js @@ -0,0 +1,93 @@ +(function () { + function forEach(arr, f) { + for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); + } + + function arrayContains(arr, item) { + if (!Array.prototype.indexOf) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + return true; + } + } + return false; + } + return arr.indexOf(item) != -1; + } + + function scriptHint(editor, _keywords, getToken) { + // Find the token at the cursor + var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token; + // If it's not a 'word-style' token, ignore the token. + + if (!/^[\w$_]*$/.test(token.string)) { + token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state, + className: token.string == ":" ? "python-type" : null}; + } + + if (!context) var context = []; + context.push(tprop); + + var completionList = getCompletions(token, context); + completionList = completionList.sort(); + //prevent autocomplete for last word, instead show dropdown with one word + if(completionList.length == 1) { + completionList.push(" "); + } + + return {list: completionList, + from: CodeMirror.Pos(cur.line, token.start), + to: CodeMirror.Pos(cur.line, token.end)}; + } + + CodeMirror.pythonHint = function(editor) { + return scriptHint(editor, pythonKeywordsU, function (e, cur) {return e.getTokenAt(cur);}); + }; + + var pythonKeywords = "and del from not while as elif global or with assert else if pass yield" ++ "break except import print class exec in raise continue finally is return def for lambda try"; + var pythonKeywordsL = pythonKeywords.split(" "); + var pythonKeywordsU = pythonKeywords.toUpperCase().split(" "); + + var pythonBuiltins = "abs divmod input open staticmethod all enumerate int ord str " ++ "any eval isinstance pow sum basestring execfile issubclass print super" ++ "bin file iter property tuple bool filter len range type" ++ "bytearray float list raw_input unichr callable format locals reduce unicode" ++ "chr frozenset long reload vars classmethod getattr map repr xrange" ++ "cmp globals max reversed zip compile hasattr memoryview round __import__" ++ "complex hash min set apply delattr help next setattr buffer" ++ "dict hex object slice coerce dir id oct sorted intern "; + var pythonBuiltinsL = pythonBuiltins.split(" ").join("() ").split(" "); + var pythonBuiltinsU = pythonBuiltins.toUpperCase().split(" ").join("() ").split(" "); + + function getCompletions(token, context) { + var found = [], start = token.string; + function maybeAdd(str) { + if (str.indexOf(start) == 0 && !arrayContains(found, str)) found.push(str); + } + + function gatherCompletions(_obj) { + forEach(pythonBuiltinsL, maybeAdd); + forEach(pythonBuiltinsU, maybeAdd); + forEach(pythonKeywordsL, maybeAdd); + forEach(pythonKeywordsU, maybeAdd); + } + + if (context) { + // If this is a property, see if it belongs to some object we can + // find in the current environment. + var obj = context.pop(), base; + + if (obj.type == "variable") + base = obj.string; + else if(obj.type == "variable-3") + base = ":" + obj.string; + + while (base != null && context.length) + base = base[context.pop().string]; + if (base != null) gatherCompletions(base); + } + return found; + } +})(); diff --git a/lib/codemirror/show-hint.css b/lib/codemirror/show-hint.css new file mode 100644 index 0000000..5617ccc --- /dev/null +++ b/lib/codemirror/show-hint.css @@ -0,0 +1,36 @@ +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + + margin: 0; + padding: 2px; + + -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + box-shadow: 2px 3px 5px rgba(0,0,0,.2); + border-radius: 3px; + border: 1px solid silver; + + background: white; + font-size: 90%; + font-family: monospace; + + max-height: 20em; + overflow-y: auto; +} + +.CodeMirror-hint { + margin: 0; + padding: 0 4px; + border-radius: 2px; + white-space: pre; + color: black; + cursor: pointer; +} + +li.CodeMirror-hint-active { + background: #08f; + color: white; +} diff --git a/lib/codemirror/show-hint.js b/lib/codemirror/show-hint.js new file mode 100644 index 0000000..d70b2ab --- /dev/null +++ b/lib/codemirror/show-hint.js @@ -0,0 +1,460 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var HINT_ELEMENT_CLASS = "CodeMirror-hint"; + var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; + + // This is the old interface, kept around for now to stay + // backwards-compatible. + CodeMirror.showHint = function(cm, getHints, options) { + if (!getHints) return cm.showHint(options); + if (options && options.async) getHints.async = true; + var newOpts = {hint: getHints}; + if (options) for (var prop in options) newOpts[prop] = options[prop]; + return cm.showHint(newOpts); + }; + + CodeMirror.defineExtension("showHint", function(options) { + options = parseOptions(this, this.getCursor("start"), options); + var selections = this.listSelections() + if (selections.length > 1) return; + // By default, don't allow completion when something is selected. + // A hint function can have a `supportsSelection` property to + // indicate that it can handle selections. + if (this.somethingSelected()) { + if (!options.hint.supportsSelection) return; + // Don't try with cross-line selections + for (var i = 0; i < selections.length; i++) + if (selections[i].head.line != selections[i].anchor.line) return; + } + + if (this.state.completionActive) this.state.completionActive.close(); + var completion = this.state.completionActive = new Completion(this, options); + if (!completion.options.hint) return; + + CodeMirror.signal(this, "startCompletion", this); + completion.update(true); + }); + + CodeMirror.defineExtension("closeHint", function() { + if (this.state.completionActive) this.state.completionActive.close() + }) + + function Completion(cm, options) { + this.cm = cm; + this.options = options; + this.widget = null; + this.debounce = 0; + this.tick = 0; + this.startPos = this.cm.getCursor("start"); + this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; + + var self = this; + cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); }); + } + + var requestAnimationFrame = window.requestAnimationFrame || function(fn) { + return setTimeout(fn, 1000/60); + }; + var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; + + Completion.prototype = { + close: function() { + if (!this.active()) return; + this.cm.state.completionActive = null; + this.tick = null; + this.cm.off("cursorActivity", this.activityFunc); + + if (this.widget && this.data) CodeMirror.signal(this.data, "close"); + if (this.widget) this.widget.close(); + CodeMirror.signal(this.cm, "endCompletion", this.cm); + }, + + active: function() { + return this.cm.state.completionActive == this; + }, + + pick: function(data, i) { + var completion = data.list[i]; + if (completion.hint) completion.hint(this.cm, data, completion); + else this.cm.replaceRange(getText(completion), completion.from || data.from, + completion.to || data.to, "complete"); + CodeMirror.signal(data, "pick", completion); + this.close(); + }, + + cursorActivity: function() { + if (this.debounce) { + cancelAnimationFrame(this.debounce); + this.debounce = 0; + } + + var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line); + if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch || + pos.ch < this.startPos.ch || this.cm.somethingSelected() || + (!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) { + this.close(); + } else { + var self = this; + this.debounce = requestAnimationFrame(function() {self.update();}); + if (this.widget) this.widget.disable(); + } + }, + + update: function(first) { + if (this.tick == null) return + var self = this, myTick = ++this.tick + fetchHints(this.options.hint, this.cm, this.options, function(data) { + if (self.tick == myTick) self.finishUpdate(data, first) + }) + }, + + finishUpdate: function(data, first) { + if (this.data) CodeMirror.signal(this.data, "update"); + + var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); + if (this.widget) this.widget.close(); + + this.data = data; + + if (data && data.list.length) { + if (picked && data.list.length == 1) { + this.pick(data, 0); + } else { + this.widget = new Widget(this, data); + CodeMirror.signal(data, "shown"); + } + } + } + }; + + function parseOptions(cm, pos, options) { + var editor = cm.options.hintOptions; + var out = {}; + for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; + if (editor) for (var prop in editor) + if (editor[prop] !== undefined) out[prop] = editor[prop]; + if (options) for (var prop in options) + if (options[prop] !== undefined) out[prop] = options[prop]; + if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos) + return out; + } + + function getText(completion) { + if (typeof completion == "string") return completion; + else return completion.text; + } + + function buildKeyMap(completion, handle) { + var baseMap = { + Up: function() {handle.moveFocus(-1);}, + Down: function() {handle.moveFocus(1);}, + PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);}, + PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);}, + Home: function() {handle.setFocus(0);}, + End: function() {handle.setFocus(handle.length - 1);}, + Enter: handle.pick, + Tab: handle.pick, + Esc: handle.close + }; + + var mac = /Mac/.test(navigator.platform); + + if (mac) { + baseMap["Ctrl-P"] = function() {handle.moveFocus(-1);}; + baseMap["Ctrl-N"] = function() {handle.moveFocus(1);}; + } + + var custom = completion.options.customKeys; + var ourMap = custom ? {} : baseMap; + function addBinding(key, val) { + var bound; + if (typeof val != "string") + bound = function(cm) { return val(cm, handle); }; + // This mechanism is deprecated + else if (baseMap.hasOwnProperty(val)) + bound = baseMap[val]; + else + bound = val; + ourMap[key] = bound; + } + if (custom) + for (var key in custom) if (custom.hasOwnProperty(key)) + addBinding(key, custom[key]); + var extra = completion.options.extraKeys; + if (extra) + for (var key in extra) if (extra.hasOwnProperty(key)) + addBinding(key, extra[key]); + return ourMap; + } + + function getHintElement(hintsElement, el) { + while (el && el != hintsElement) { + if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; + el = el.parentNode; + } + } + + function Widget(completion, data) { + this.completion = completion; + this.data = data; + this.picked = false; + var widget = this, cm = completion.cm; + var ownerDocument = cm.getInputField().ownerDocument; + var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow; + + var hints = this.hints = ownerDocument.createElement("ul"); + var theme = completion.cm.options.theme; + hints.className = "CodeMirror-hints " + theme; + this.selectedHint = data.selectedHint || 0; + + var completions = data.list; + for (var i = 0; i < completions.length; ++i) { + var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i]; + var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); + if (cur.className != null) className = cur.className + " " + className; + elt.className = className; + if (cur.render) cur.render(elt, data, cur); + else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur))); + elt.hintId = i; + } + + var container = completion.options.container || ownerDocument.body; + var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); + var left = pos.left, top = pos.bottom, below = true; + var offsetLeft = 0, offsetTop = 0; + if (container !== ownerDocument.body) { + // We offset the cursor position because left and top are relative to the offsetParent's top left corner. + var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1; + var offsetParent = isContainerPositioned ? container : container.offsetParent; + var offsetParentPosition = offsetParent.getBoundingClientRect(); + var bodyPosition = ownerDocument.body.getBoundingClientRect(); + offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft); + offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop); + } + hints.style.left = (left - offsetLeft) + "px"; + hints.style.top = (top - offsetTop) + "px"; + + // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. + var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth); + var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight); + container.appendChild(hints); + var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; + var scrolls = hints.scrollHeight > hints.clientHeight + 1 + var startScroll = cm.getScrollInfo(); + + if (overlapY > 0) { + var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); + if (curTop - height > 0) { // Fits above cursor + hints.style.top = (top = pos.top - height - offsetTop) + "px"; + below = false; + } else if (height > winH) { + hints.style.height = (winH - 5) + "px"; + hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px"; + var cursor = cm.getCursor(); + if (data.from.ch != cursor.ch) { + pos = cm.cursorCoords(cursor); + hints.style.left = (left = pos.left - offsetLeft) + "px"; + box = hints.getBoundingClientRect(); + } + } + } + var overlapX = box.right - winW; + if (overlapX > 0) { + if (box.right - box.left > winW) { + hints.style.width = (winW - 5) + "px"; + overlapX -= (box.right - box.left) - winW; + } + hints.style.left = (left = pos.left - overlapX - offsetLeft) + "px"; + } + if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling) + node.style.paddingRight = cm.display.nativeBarWidth + "px" + + cm.addKeyMap(this.keyMap = buildKeyMap(completion, { + moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, + setFocus: function(n) { widget.changeActive(n); }, + menuSize: function() { return widget.screenAmount(); }, + length: completions.length, + close: function() { completion.close(); }, + pick: function() { widget.pick(); }, + data: data + })); + + if (completion.options.closeOnUnfocus) { + var closingOnBlur; + cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); + cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); + } + + cm.on("scroll", this.onScroll = function() { + var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); + var newTop = top + startScroll.top - curScroll.top; + var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop); + if (!below) point += hints.offsetHeight; + if (point <= editor.top || point >= editor.bottom) return completion.close(); + hints.style.top = newTop + "px"; + hints.style.left = (left + startScroll.left - curScroll.left) + "px"; + }); + + CodeMirror.on(hints, "dblclick", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} + }); + + CodeMirror.on(hints, "click", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) { + widget.changeActive(t.hintId); + if (completion.options.completeOnSingleClick) widget.pick(); + } + }); + + CodeMirror.on(hints, "mousedown", function() { + setTimeout(function(){cm.focus();}, 20); + }); + + CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]); + return true; + } + + Widget.prototype = { + close: function() { + if (this.completion.widget != this) return; + this.completion.widget = null; + this.hints.parentNode.removeChild(this.hints); + this.completion.cm.removeKeyMap(this.keyMap); + + var cm = this.completion.cm; + if (this.completion.options.closeOnUnfocus) { + cm.off("blur", this.onBlur); + cm.off("focus", this.onFocus); + } + cm.off("scroll", this.onScroll); + }, + + disable: function() { + this.completion.cm.removeKeyMap(this.keyMap); + var widget = this; + this.keyMap = {Enter: function() { widget.picked = true; }}; + this.completion.cm.addKeyMap(this.keyMap); + }, + + pick: function() { + this.completion.pick(this.data, this.selectedHint); + }, + + changeActive: function(i, avoidWrap) { + if (i >= this.data.list.length) + i = avoidWrap ? this.data.list.length - 1 : 0; + else if (i < 0) + i = avoidWrap ? 0 : this.data.list.length - 1; + if (this.selectedHint == i) return; + var node = this.hints.childNodes[this.selectedHint]; + if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); + node = this.hints.childNodes[this.selectedHint = i]; + node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; + if (node.offsetTop < this.hints.scrollTop) + this.hints.scrollTop = node.offsetTop - 3; + else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) + this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; + CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); + }, + + screenAmount: function() { + return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; + } + }; + + function applicableHelpers(cm, helpers) { + if (!cm.somethingSelected()) return helpers + var result = [] + for (var i = 0; i < helpers.length; i++) + if (helpers[i].supportsSelection) result.push(helpers[i]) + return result + } + + function fetchHints(hint, cm, options, callback) { + if (hint.async) { + hint(cm, callback, options) + } else { + var result = hint(cm, options) + if (result && result.then) result.then(callback) + else callback(result) + } + } + + function resolveAutoHints(cm, pos) { + var helpers = cm.getHelpers(pos, "hint"), words + if (helpers.length) { + var resolved = function(cm, callback, options) { + var app = applicableHelpers(cm, helpers); + function run(i) { + if (i == app.length) return callback(null) + fetchHints(app[i], cm, options, function(result) { + if (result && result.list.length > 0) callback(result) + else run(i + 1) + }) + } + run(0) + } + resolved.async = true + resolved.supportsSelection = true + return resolved + } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { + return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) } + } else if (CodeMirror.hint.anyword) { + return function(cm, options) { return CodeMirror.hint.anyword(cm, options) } + } else { + return function() {} + } + } + + CodeMirror.registerHelper("hint", "auto", { + resolve: resolveAutoHints + }); + + CodeMirror.registerHelper("hint", "fromList", function(cm, options) { + var cur = cm.getCursor(), token = cm.getTokenAt(cur) + var term, from = CodeMirror.Pos(cur.line, token.start), to = cur + if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) { + term = token.string.substr(0, cur.ch - token.start) + } else { + term = "" + from = cur + } + var found = []; + for (var i = 0; i < options.words.length; i++) { + var word = options.words[i]; + if (word.slice(0, term.length) == term) + found.push(word); + } + + if (found.length) return {list: found, from: from, to: to}; + }); + + CodeMirror.commands.autocomplete = CodeMirror.showHint; + + var defaultOptions = { + hint: CodeMirror.hint.auto, + completeSingle: true, + alignWithWord: true, + closeCharacters: /[\s()\[\]{};:>,]/, + closeOnUnfocus: true, + completeOnSingleClick: true, + container: null, + customKeys: null, + extraKeys: null + }; + + CodeMirror.defineOption("hintOptions", null); +}); diff --git a/src/ast/ast_Call.js b/src/ast/ast_Call.js index c7e9fc3..bc9d6b3 100644 --- a/src/ast/ast_Call.js +++ b/src/ast/ast_Call.js @@ -1,5 +1,6 @@ // TODO: Support stuff like "append" where the message is after the value input // TODO: Handle updating function/method definition -> update call +// TODO: Do a pretraversal to determine if a given function returns Blockly.Blocks['ast_Call'] = { /** diff --git a/src/ast/ast_Comp.js b/src/ast/ast_Comp.js index b769876..c017a32 100644 --- a/src/ast/ast_Comp.js +++ b/src/ast/ast_Comp.js @@ -338,13 +338,15 @@ BlockMirrorTextToBlocks.COMP_SETTINGS = { }, DEFAULT_SETTINGS); g += 1; - for (let j = 0; j < ifs.length; j++) { - elements["GENERATOR" + g] = BlockMirrorTextToBlocks.create_block("ast_comprehensionIf", node.lineno, {}, - { - "TEST": this.convert(ifs[j], node) - }, - DEFAULT_SETTINGS); - g += 1; + if (ifs) { + for (let j = 0; j < ifs.length; j++) { + elements["GENERATOR" + g] = BlockMirrorTextToBlocks.create_block("ast_comprehensionIf", node.lineno, {}, + { + "TEST": this.convert(ifs[j], node) + }, + DEFAULT_SETTINGS); + g += 1; + } } } diff --git a/src/ast/ast_FunctionDef.js b/src/ast/ast_FunctionDef.js index ba71cce..22abe90 100644 --- a/src/ast/ast_FunctionDef.js +++ b/src/ast/ast_FunctionDef.js @@ -395,7 +395,7 @@ BlockMirrorTextToBlocks.prototype['ast_FunctionDef'] = function (node, parent) { let parsedArgs = 0; if (args !== null) { - parsedArgs = this.parseArgs(args, values, node.lineno); + parsedArgs = this.parseArgs(args, values, node.lineno, node); } let hasReturn = (returns !== null && diff --git a/src/block_mirror.js b/src/block_mirror.js index 28cd862..c8a0f45 100644 --- a/src/block_mirror.js +++ b/src/block_mirror.js @@ -80,6 +80,9 @@ BlockMirror.prototype.validateConfiguration = function (configuration) { // Need to load skulpt? this.configuration.skipSkulpt = configuration.skipSkulpt || false; + + // Delay? + this.configuration.blockDelay = configuration.blockDelay || false; } BlockMirror.prototype.initializeVariables = function () { @@ -185,4 +188,4 @@ BlockMirror.prototype.VISIBLE_MODES = { 'text': ['text', 'split'] }; - +exports = BlockMirror; \ No newline at end of file diff --git a/src/text_editor.js b/src/text_editor.js index 33cbcb1..8a4774f 100644 --- a/src/text_editor.js +++ b/src/text_editor.js @@ -9,7 +9,10 @@ function BlockMirrorTextEditor(blockMirror) { // Do we need to force an update? this.outOfDate_ = null; - + + // Use a timer to swallow updates + this.updateTimer_ = null; + let codeMirrorOptions = { mode: { name: 'python', @@ -25,16 +28,30 @@ function BlockMirrorTextEditor(blockMirror) { indentWithTabs: false, matchBrackets: true, extraKeys: { - 'Tab': 'indentMore', + 'Tab': 'indentMore', 'Shift-Tab': 'indentLess', 'Ctrl-Enter': blockMirror.run, - 'Esc': this.defocus.bind(this) - } + 'Esc': function(cm) { + if (cm.getOption("fullScreen")) { + cm.setOption("fullScreen", false); + } else { + cm.display.input.blur(); + } + }, + "F11": function(cm) { + cm.setOption("fullScreen", !cm.getOption("fullScreen")); + }, + "Esc": function(cm) { + + } + }, + foldGutter: true, + gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"] }; this.codeMirror = CodeMirror.fromTextArea(this.textArea, codeMirrorOptions); this.codeMirror.on('change', this.changed.bind(this)); this.codeMirror.setSize(null, '100%'); - this.textContainer.style.border= '1px solid lightgray'; + this.textContainer.style.border = '1px solid lightgray'; this.textContainer.style.float = 'left'; this.updateWidth(); this.textContainer.style.height = blockMirror.configuration.height; @@ -43,13 +60,22 @@ function BlockMirrorTextEditor(blockMirror) { this.textSidebar.style.float = 'left'; this.textSidebar.style.backgroundColor = '#ddd'; + // TODO: Finish implementing code completion + /*this.codeMirror.on('inputRead', function onChange(editor, input) { + if (input.text[0] === ';' || input.text[0] === ' ' || input.text[0] === ":") { + return; + } + editor.showHint({ + hint: CodeMirror.pythonHint + }); + });*/ } -BlockMirrorTextEditor.prototype.defocus = function() { +BlockMirrorTextEditor.prototype.defocus = function () { this.codeMirror.display.input.blur(); } -BlockMirrorTextEditor.prototype.updateWidth = function() { +BlockMirrorTextEditor.prototype.updateWidth = function () { var newWidth = '0%'; /*if (this.blockMirror.views.includes('text')) { newWidth = (100 / this.blockMirror.views.length)+'%'; @@ -57,7 +83,7 @@ BlockMirrorTextEditor.prototype.updateWidth = function() { this.textContainer.style.width = newWidth;*/ } -BlockMirrorTextEditor.prototype.setReadOnly = function(isReadOnly) { +BlockMirrorTextEditor.prototype.setReadOnly = function (isReadOnly) { this.codeMirror.setOption('readOnly', isReadOnly); }; @@ -79,7 +105,7 @@ BlockMirrorTextEditor.prototype.VIEW_CONFIGURATIONS = { } }; -BlockMirrorTextEditor.prototype.setMode = function(mode) { +BlockMirrorTextEditor.prototype.setMode = function (mode) { mode = mode.toLowerCase(); let configuration = this.VIEW_CONFIGURATIONS[mode]; // If there is an update waiting and we're visible, then update @@ -112,11 +138,11 @@ BlockMirrorTextEditor.prototype.setMode = function(mode) { } } -BlockMirrorTextEditor.prototype.setCode = function(code, quietly) { +BlockMirrorTextEditor.prototype.setCode = function (code, quietly) { this.silentEvents_ = quietly; // Defaults to a single blank line - code = (code === undefined || code.trim() === "") ? "\n": code; + code = (code === undefined || code.trim() === "") ? "\n" : code; if (this.isVisible()) { this.codeMirror.setValue(code); @@ -126,20 +152,29 @@ BlockMirrorTextEditor.prototype.setCode = function(code, quietly) { } }; -BlockMirrorTextEditor.prototype.getCode = function() { +BlockMirrorTextEditor.prototype.getCode = function () { return this.codeMirror.getValue(); }; -BlockMirrorTextEditor.prototype.changed = function(codeMirror, event) { +BlockMirrorTextEditor.prototype.changed = function (codeMirror, event) { if (!this.silentEvents_) { - let newCode = this.getCode(); - this.blockMirror.blockEditor.setCode(newCode, true); - this.blockMirror.code_ = newCode; + let handleChange = () => { + let newCode = this.getCode(); + this.blockMirror.blockEditor.setCode(newCode, true); + this.blockMirror.code_ = newCode; + }; + if (this.blockMirror.configuration.blockDelay === false) { + handleChange(); + } else { + if (this.updateTimer_ !== null) { + clearTimeout(this.updateTimer_); + } + this.updateTimer_ = setTimeout(handleChange, this.blockMirror.configuration.blockDelay); + } } this.silentEvents_ = false; - //console.log("Changed text"); }; -BlockMirrorTextEditor.prototype.isVisible = function() { +BlockMirrorTextEditor.prototype.isVisible = function () { return this.blockMirror.VISIBLE_MODES.text.indexOf(this.blockMirror.mode_) !== -1; }; \ No newline at end of file diff --git a/test/simple.html b/test/simple.html index 2ef7626..9385536 100644 --- a/test/simple.html +++ b/test/simple.html @@ -3,7 +3,35 @@ Simple BlockMirror Example + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/test/simple_dev.html b/test/simple_dev.html index a475417..d721bb4 100644 --- a/test/simple_dev.html +++ b/test/simple_dev.html @@ -18,7 +18,12 @@ + + + + + @@ -109,7 +114,7 @@ editor.addChangeListener(function (event) { console.log('Change! Better save:', event) }); - editor.setCode('a = 0'); + editor.setCode('class X:\n """Hello world"""\ndef add(self, a, b):\n a = 0\n return a\n\nx = X()\nx.add(5,3)'); Sk.configure({ __future__: Sk.python3, diff --git a/webpack.config.js b/webpack.config.js index 5a9a8a0..962d03c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,6 +2,85 @@ const path = require('path'); const MergeIntoSingleFilePlugin = require('webpack-merge-and-include-globally'); const Uglify = require("uglify-js"); +// Blockly +const JS_BLOCKLY_FILES = [ + path.resolve(__dirname, '../blockly/blockly_compressed.js'), + path.resolve(__dirname, '../blockly/blocks_compressed.js'), + path.resolve(__dirname, '../blockly/msg/js/en.js'), + path.resolve(__dirname, '../blockly/python_compressed.js') +]; + +// CodeMirror +const JS_CODEMIRROR_FILES = [ + path.resolve(__dirname, 'lib/codemirror/codemirror.js'), + /*path.resolve(__dirname, '../lib/codemirror/show-hint.js'), + path.resolve(__dirname, '../lib/codemirror/python-hint.js'),*/ + path.resolve(__dirname, 'lib/codemirror/fullscreen.js'), + path.resolve(__dirname, 'lib/codemirror/python.js'), +]; + +// Skulpt +const JS_SKULPT_FILES = [ + path.resolve(__dirname, '../skulpt/dist/skulpt.js'), + path.resolve(__dirname, '../skulpt/dist/skulpt-stdlib.js'), +]; + +// BlockMirror +const JS_BLOCKMIRROR_FILES = [ + path.resolve(__dirname, 'src/blockly_shims.js'), + path.resolve(__dirname, 'src/block_mirror.js'), + path.resolve(__dirname, 'src/text_editor.js'), + path.resolve(__dirname, 'src/block_editor.js'), + path.resolve(__dirname, 'src/text_to_blocks.js'), + // AST Handlers + path.resolve(__dirname, 'src/ast/ast_functions.js'), + path.resolve(__dirname, 'src/ast/ast_For.js'), + path.resolve(__dirname, 'src/ast/ast_If.js'), + path.resolve(__dirname, 'src/ast/ast_While.js'), + path.resolve(__dirname, 'src/ast/ast_Num.js'), + path.resolve(__dirname, 'src/ast/ast_BinOp.js'), + path.resolve(__dirname, 'src/ast/ast_Name.js'), + path.resolve(__dirname, 'src/ast/ast_Assign.js'), + path.resolve(__dirname, 'src/ast/ast_AnnAssign.js'), + path.resolve(__dirname, 'src/ast/ast_Str.js'), + path.resolve(__dirname, 'src/ast/ast_Expr.js'), + path.resolve(__dirname, 'src/ast/ast_UnaryOp.js'), + path.resolve(__dirname, 'src/ast/ast_BoolOp.js'), + path.resolve(__dirname, 'src/ast/ast_Compare.js'), + path.resolve(__dirname, 'src/ast/ast_Assert.js'), + path.resolve(__dirname, 'src/ast/ast_NameConstant.js'), + path.resolve(__dirname, 'src/ast/ast_List.js'), + path.resolve(__dirname, 'src/ast/ast_Tuple.js'), + path.resolve(__dirname, 'src/ast/ast_Set.js'), + path.resolve(__dirname, 'src/ast/ast_Dict.js'), + path.resolve(__dirname, 'src/ast/ast_Starred.js'), + path.resolve(__dirname, 'src/ast/ast_IfExp.js'), + path.resolve(__dirname, 'src/ast/ast_Attribute.js'), + path.resolve(__dirname, 'src/ast/ast_Call.js'), + path.resolve(__dirname, 'src/ast/ast_Raise.js'), + path.resolve(__dirname, 'src/ast/ast_Delete.js'), + path.resolve(__dirname, 'src/ast/ast_Subscript.js'), + path.resolve(__dirname, 'src/ast/ast_Comp.js'), + path.resolve(__dirname, 'src/ast/ast_FunctionDef.js'), + path.resolve(__dirname, 'src/ast/ast_Lambda.js'), + path.resolve(__dirname, 'src/ast/ast_Return.js'), + path.resolve(__dirname, 'src/ast/ast_Yield.js'), + path.resolve(__dirname, 'src/ast/ast_YieldFrom.js'), + path.resolve(__dirname, 'src/ast/ast_Global.js'), + + path.resolve(__dirname, 'src/ast/ast_Break.js'), + path.resolve(__dirname, 'src/ast/ast_Continue.js'), + path.resolve(__dirname, 'src/ast/ast_Try.js'), + path.resolve(__dirname, 'src/ast/ast_ClassDef.js'), + path.resolve(__dirname, 'src/ast/ast_Import.js'), + path.resolve(__dirname, 'src/ast/ast_With.js'), + path.resolve(__dirname, 'src/ast/ast_Comment.js'), + path.resolve(__dirname, 'src/ast/ast_Raw.js') +]; + +const JS_FILES = [].concat(JS_BLOCKLY_FILES, JS_CODEMIRROR_FILES, JS_SKULPT_FILES, + JS_BLOCKMIRROR_FILES) + const config = { entry: [ path.resolve(__dirname, 'src/main.js'), @@ -29,11 +108,11 @@ const config = { plugins: [ new MergeIntoSingleFilePlugin({ files: { - "block_mirror.js": [ - path.resolve(__dirname, 'src/text_editor.js'), - path.resolve(__dirname, 'src/block_mirror.js'), - ], + "block_mirror.js": JS_BLOCKMIRROR_FILES, "block_mirror.css": [ + path.resolve(__dirname, 'lib/codemirror/codemirror.css'), + path.resolve(__dirname, 'lib/codemirror/fullscreen.css'), + path.resolve(__dirname, 'lib/codemirror/show-hint.css'), path.resolve(__dirname, 'src/block_mirror.css'), ] }