/** * @license * Visual Blocks Editor * * Copyright 2017 Google Inc. * https://developers.google.com/blockly/ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Object representing a map of variables and their types. * @author marisaleung@google.com (Marisa Leung) */ 'use strict'; goog.provide('Blockly.VariableMap'); goog.require('Blockly.Events.VarDelete'); goog.require('Blockly.Events.VarRename'); goog.require('Blockly.VariableModel'); /** * Class for a variable map. This contains a dictionary data structure with * variable types as keys and lists of variables as values. The list of * variables are the type indicated by the key. * @param {!Blockly.Workspace} workspace The workspace this map belongs to. * @constructor */ Blockly.VariableMap = function(workspace) { /** * A map from variable type to list of variable names. The lists contain all * of the named variables in the workspace, including variables * that are not currently in use. * @type {!Object.<string, !Array.<Blockly.VariableModel>>} * @private */ this.variableMap_ = {}; /** * The workspace this map belongs to. * @type {!Blockly.Workspace} */ this.workspace = workspace; }; /** * Clear the variable map. */ Blockly.VariableMap.prototype.clear = function() { this.variableMap_ = new Object(null); }; /* Begin functions for renaming variables. */ /** * Rename the given variable by updating its name in the variable map. * @param {!Blockly.VariableModel} variable Variable to rename. * @param {string} newName New variable name. * @package */ Blockly.VariableMap.prototype.renameVariable = function(variable, newName) { var type = variable.type; var conflictVar = this.getVariable(newName, type); var blocks = this.workspace.getAllBlocks(); Blockly.Events.setGroup(true); try { if (!conflictVar) { this.renameVariableAndUses_(variable, newName, blocks); } else { // We don't want to rename the variable if one with the exact new name // already exists. console.warn('Unexpected conflict when attempting to rename ' + 'variable with name: ' + variable.name + ' and id: ' + variable.getId() + ' to new name: ' + newName + '. A variable with the new name already exists' + ' and has id: ' + conflictVar.getId()); } } finally { Blockly.Events.setGroup(false); } }; /** * Rename a variable by updating its name in the variable map. Identify the * variable to rename with the given ID. * @param {string} id ID of the variable to rename. * @param {string} newName New variable name. */ Blockly.VariableMap.prototype.renameVariableById = function(id, newName) { var variable = this.getVariableById(id); if (!variable) { throw new Error('Tried to rename a variable that didn\'t exist. ID: ' + id); } this.renameVariable(variable, newName); }; /** * Update the name of the given variable and refresh all references to it. * The new name must not conflict with any existing variable names. * @param {!Blockly.VariableModel} variable Variable to rename. * @param {string} newName New variable name. * @param {!Array.<!Blockly.Block>} blocks The list of all blocks in the * workspace. * @private */ Blockly.VariableMap.prototype.renameVariableAndUses_ = function(variable, newName, blocks) { Blockly.Events.fire(new Blockly.Events.VarRename(variable, newName)); variable.name = newName; for (var i = 0; i < blocks.length; i++) { blocks[i].updateVarName(variable); } }; /** * Update the name of the given variable to the same name as an existing * variable. The two variables are coalesced into a single variable with the ID * of the existing variable that was already using newName. * Refresh all references to the variable. * @param {!Blockly.VariableModel} variable Variable to rename. * @param {string} newName New variable name. * @param {!Blockly.VariableModel} conflictVar The variable that was already * using newName. * @param {!Array.<!Blockly.Block>} blocks The list of all blocks in the * workspace. * @private */ Blockly.VariableMap.prototype.renameVariableWithConflict_ = function(variable, newName, conflictVar, blocks) { var type = variable.type; var oldCase = conflictVar.name; if (newName != oldCase) { // Simple rename to change the case and update references. this.renameVariableAndUses_(conflictVar, newName, blocks); } // These blocks now refer to a different variable. // These will fire change events. for (var i = 0; i < blocks.length; i++) { blocks[i].renameVarById(variable.getId(), conflictVar.getId()); } // Finally delete the original variable, which is now unreferenced. Blockly.Events.fire(new Blockly.Events.VarDelete(variable)); // And remove it from the list. var variableList = this.getVariablesOfType(type); var variableIndex = variableList.indexOf(variable); this.variableMap_[type].splice(variableIndex, 1); }; /* End functions for renaming variabless. */ /** * Create a variable with a given name, optional type, and optional id. * @param {!string} name The name of the variable. This must be unique across * each variable type. * @param {?string} opt_type The type of the variable like 'int' or 'string'. * Does not need to be unique. Field_variable can filter variables based on * their type. This will default to '' which is a specific type. * @param {string=} opt_id The unique ID of the variable. This will default to * a UUID. * @param {boolean=} opt_isLocal Whether the variable is locally scoped. * @param {boolean=} opt_isCloud Whether the variable is a cloud variable. * @return {?Blockly.VariableModel} The newly created variable. */ Blockly.VariableMap.prototype.createVariable = function(name, opt_type, opt_id, opt_isLocal, opt_isCloud) { var variable = this.getVariable(name, opt_type); if (variable) { if (opt_id && variable.getId() != opt_id) { // There is a variable conflict. Variable conflicts should be eliminated // in the scratch-vm, or before we get to this point, // so log a warning, because throwing an error crashes projects. console.warn('Variable "' + name + '" is already in use and its id is "' + variable.getId() + '" which conflicts with the passed in ' + 'id, "' + opt_id + '".'); } // The variable already exists and has the same ID. return variable; } if (opt_id) { variable = this.getVariableById(opt_id); if (variable) { console.warn('Variable id, "' + opt_id + '", is already in use.'); return variable; } } opt_id = opt_id || Blockly.utils.genUid(); opt_type = opt_type || ''; variable = new Blockly.VariableModel(this.workspace, name, opt_type, opt_id, opt_isLocal, opt_isCloud); // If opt_type is not a key, create a new list. if (!this.variableMap_[opt_type]) { this.variableMap_[opt_type] = [variable]; } else { // Else append the variable to the preexisting list. this.variableMap_[opt_type].push(variable); } return variable; }; /* Begin functions for variable deletion. */ /** * Delete a variable. * @param {Blockly.VariableModel} variable Variable to delete. */ Blockly.VariableMap.prototype.deleteVariable = function(variable) { var variableList = this.variableMap_[variable.type]; for (var i = 0, tempVar; tempVar = variableList[i]; i++) { if (tempVar.getId() == variable.getId()) { variableList.splice(i, 1); Blockly.Events.fire(new Blockly.Events.VarDelete(variable)); return; } } }; /** * Delete a variable and all of its uses from this workspace by the passed * in ID. May prompt the user for confirmation. * @param {string} id ID of variable to delete. */ Blockly.VariableMap.prototype.deleteVariableById = function(id) { var variable = this.getVariableById(id); if (variable) { // Check whether this variable is a function parameter before deleting. var variableName = variable.name; var uses = this.getVariableUsesById(id); for (var i = 0, block; block = uses[i]; i++) { if (block.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE || block.type == 'procedures_defreturn') { var procedureName = block.getFieldValue('NAME'); var deleteText = Blockly.Msg.CANNOT_DELETE_VARIABLE_PROCEDURE. replace('%1', variableName). replace('%2', procedureName); Blockly.alert(deleteText); return; } } var map = this; if (uses.length > 1) { // Confirm before deleting multiple blocks. var confirmText = Blockly.Msg.DELETE_VARIABLE_CONFIRMATION. replace('%1', String(uses.length)). replace('%2', variableName); Blockly.confirm(confirmText, function(ok) { if (ok) { map.deleteVariableInternal_(variable, uses); } }); } else { // No confirmation necessary for a single block. map.deleteVariableInternal_(variable, uses); } } else { console.warn("Can't delete non-existent variable: " + id); } }; /** * Deletes a variable and all of its uses from this workspace without asking the * user for confirmation. * @param {!Blockly.VariableModel} variable Variable to delete. * @param {!Array.<!Blockly.Block>} uses An array of uses of the variable. * @private */ Blockly.VariableMap.prototype.deleteVariableInternal_ = function(variable, uses) { var existingGroup = Blockly.Events.getGroup(); if (!existingGroup) { Blockly.Events.setGroup(true); } try { for (var i = 0; i < uses.length; i++) { uses[i].dispose(true, false); } this.deleteVariable(variable); } finally { if (!existingGroup) { Blockly.Events.setGroup(false); } } }; /* End functions for variable deletion. */ /** * Find the variable by the given name and type and return it. Return null if * it is not found. * @param {string} name The name to check for. * @param {string=} opt_type The type of the variable. If not provided it * defaults to the empty string, which is a specific type. * @return {Blockly.VariableModel} The variable with the given name, or null if * it was not found. */ Blockly.VariableMap.prototype.getVariable = function(name, opt_type) { var type = opt_type || ''; var list = this.variableMap_[type]; if (list) { for (var j = 0, variable; variable = list[j]; j++) { if (variable.name == name) { return variable; } } } return null; }; /** * Find the variable by the given ID and return it. Return null if it is not * found. * @param {!string} id The id to check for. * @return {?Blockly.VariableModel} The variable with the given id. */ Blockly.VariableMap.prototype.getVariableById = function(id) { var keys = Object.keys(this.variableMap_); for (var i = 0; i < keys.length; i++ ) { var key = keys[i]; for (var j = 0, variable; variable = this.variableMap_[key][j]; j++) { if (variable.getId() == id) { return variable; } } } return null; }; /** * Get a list containing all of the variables of a specified type. If type is * null, return list of variables with empty string type. * @param {?string} type Type of the variables to find. * @return {!Array.<!Blockly.VariableModel>} The sought after variables of the * passed in type. An empty array if none are found. */ Blockly.VariableMap.prototype.getVariablesOfType = function(type) { type = type || ''; var variable_list = this.variableMap_[type]; if (variable_list) { return variable_list.slice(); } return []; }; /** * Return all variable types. This list always contains the empty string. * @return {!Array.<string>} List of variable types. * @package */ Blockly.VariableMap.prototype.getVariableTypes = function() { var types = Object.keys(this.variableMap_); var hasEmpty = false; for (var i = 0; i < types.length; i++) { if (types[i] == '') { hasEmpty = true; } } if (!hasEmpty) { types.push(''); } return types; }; /** * Return all variables of all types. * @return {!Array.<!Blockly.VariableModel>} List of variable models. */ Blockly.VariableMap.prototype.getAllVariables = function() { var all_variables = []; var keys = Object.keys(this.variableMap_); for (var i = 0; i < keys.length; i++ ) { all_variables = all_variables.concat(this.variableMap_[keys[i]]); } return all_variables; }; /** * Find all the uses of a named variable. * @param {string} id ID of the variable to find. * @return {!Array.<!Blockly.Block>} Array of block usages. */ Blockly.VariableMap.prototype.getVariableUsesById = function(id) { var uses = []; var blocks = this.workspace.getAllBlocks(); // Iterate through every block and check the name. for (var i = 0; i < blocks.length; i++) { var blockVariables = blocks[i].getVarModels(); if (blockVariables) { for (var j = 0; j < blockVariables.length; j++) { if (blockVariables[j].getId() == id) { uses.push(blocks[i]); } } } } return uses; };