mirror of
https://github.com/google/blockly.git
synced 2025-12-16 14:20:10 +01:00
773 lines
25 KiB
TypeScript
773 lines
25 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2012 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
// Former goog.module ID: Blockly.Variables
|
|
|
|
import type {Block} from './block.js';
|
|
import {Blocks} from './blocks.js';
|
|
import * as dialog from './dialog.js';
|
|
import {isLegacyProcedureDefBlock} from './interfaces/i_legacy_procedure_blocks.js';
|
|
import {isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js';
|
|
import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
|
|
import {Msg} from './msg.js';
|
|
import * as utilsXml from './utils/xml.js';
|
|
import type {Workspace} from './workspace.js';
|
|
import type {WorkspaceSvg} from './workspace_svg.js';
|
|
|
|
/**
|
|
* String for use in the "custom" attribute of a category in toolbox XML.
|
|
* This string indicates that the category should be dynamically populated with
|
|
* variable blocks.
|
|
* See also Blockly.Procedures.CATEGORY_NAME and
|
|
* Blockly.VariablesDynamic.CATEGORY_NAME.
|
|
*/
|
|
export const CATEGORY_NAME = 'VARIABLE';
|
|
|
|
/**
|
|
* Find all user-created variables that are in use in the workspace.
|
|
* For use by generators.
|
|
* To get a list of all variables on a workspace, including unused variables,
|
|
* call Workspace.getAllVariables.
|
|
*
|
|
* @param ws The workspace to search for variables.
|
|
* @returns Array of variable models.
|
|
*/
|
|
export function allUsedVarModels(
|
|
ws: Workspace,
|
|
): IVariableModel<IVariableState>[] {
|
|
const blocks = ws.getAllBlocks(false);
|
|
const variables = new Set<IVariableModel<IVariableState>>();
|
|
// Iterate through every block and add each variable to the set.
|
|
for (let i = 0; i < blocks.length; i++) {
|
|
const blockVariables = blocks[i].getVarModels();
|
|
if (blockVariables) {
|
|
for (let j = 0; j < blockVariables.length; j++) {
|
|
const variable = blockVariables[j];
|
|
const id = variable.getId();
|
|
if (id) {
|
|
variables.add(variable);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Convert the set into a list.
|
|
return Array.from(variables.values());
|
|
}
|
|
|
|
/**
|
|
* Find all developer variables used by blocks in the workspace.
|
|
* Developer variables are never shown to the user, but are declared as global
|
|
* variables in the generated code.
|
|
* To declare developer variables, define the getDeveloperVariables function on
|
|
* your block and return a list of variable names.
|
|
* For use by generators.
|
|
*
|
|
* @param workspace The workspace to search.
|
|
* @returns A list of non-duplicated variable names.
|
|
*/
|
|
export function allDeveloperVariables(workspace: Workspace): string[] {
|
|
const blocks = workspace.getAllBlocks(false);
|
|
const variables = new Set<string>();
|
|
for (let i = 0, block; (block = blocks[i]); i++) {
|
|
const getDeveloperVariables = block.getDeveloperVariables;
|
|
if (getDeveloperVariables) {
|
|
const devVars = getDeveloperVariables();
|
|
for (let j = 0; j < devVars.length; j++) {
|
|
variables.add(devVars[j]);
|
|
}
|
|
}
|
|
}
|
|
// Convert the set into a list.
|
|
return Array.from(variables.values());
|
|
}
|
|
|
|
/**
|
|
* Construct the elements (blocks and button) required by the flyout for the
|
|
* variable category.
|
|
*
|
|
* @param workspace The workspace containing variables.
|
|
* @returns Array of XML elements.
|
|
*/
|
|
export function flyoutCategory(workspace: WorkspaceSvg): Element[] {
|
|
let xmlList = new Array<Element>();
|
|
const button = document.createElement('button');
|
|
button.setAttribute('text', '%{BKY_NEW_VARIABLE}');
|
|
button.setAttribute('callbackKey', 'CREATE_VARIABLE');
|
|
|
|
workspace.registerButtonCallback('CREATE_VARIABLE', function (button) {
|
|
createVariableButtonHandler(button.getTargetWorkspace());
|
|
});
|
|
|
|
xmlList.push(button);
|
|
|
|
const blockList = flyoutCategoryBlocks(workspace);
|
|
xmlList = xmlList.concat(blockList);
|
|
return xmlList;
|
|
}
|
|
|
|
/**
|
|
* Construct the blocks required by the flyout for the variable category.
|
|
*
|
|
* @param workspace The workspace containing variables.
|
|
* @returns Array of XML block elements.
|
|
*/
|
|
export function flyoutCategoryBlocks(workspace: Workspace): Element[] {
|
|
const variableModelList = workspace.getVariablesOfType('');
|
|
|
|
const xmlList = [];
|
|
if (variableModelList.length > 0) {
|
|
// New variables are added to the end of the variableModelList.
|
|
const mostRecentVariable = variableModelList[variableModelList.length - 1];
|
|
if (Blocks['variables_set']) {
|
|
const block = utilsXml.createElement('block');
|
|
block.setAttribute('type', 'variables_set');
|
|
block.setAttribute('gap', Blocks['math_change'] ? '8' : '24');
|
|
block.appendChild(generateVariableFieldDom(mostRecentVariable));
|
|
xmlList.push(block);
|
|
}
|
|
if (Blocks['math_change']) {
|
|
const block = utilsXml.createElement('block');
|
|
block.setAttribute('type', 'math_change');
|
|
block.setAttribute('gap', Blocks['variables_get'] ? '20' : '8');
|
|
block.appendChild(generateVariableFieldDom(mostRecentVariable));
|
|
const value = utilsXml.textToDom(
|
|
'<value name="DELTA">' +
|
|
'<shadow type="math_number">' +
|
|
'<field name="NUM">1</field>' +
|
|
'</shadow>' +
|
|
'</value>',
|
|
);
|
|
block.appendChild(value);
|
|
xmlList.push(block);
|
|
}
|
|
|
|
if (Blocks['variables_get']) {
|
|
variableModelList.sort(compareByName);
|
|
for (let i = 0, variable; (variable = variableModelList[i]); i++) {
|
|
const block = utilsXml.createElement('block');
|
|
block.setAttribute('type', 'variables_get');
|
|
block.setAttribute('gap', '8');
|
|
block.appendChild(generateVariableFieldDom(variable));
|
|
xmlList.push(block);
|
|
}
|
|
}
|
|
}
|
|
return xmlList;
|
|
}
|
|
|
|
export const VAR_LETTER_OPTIONS = 'ijkmnopqrstuvwxyzabcdefgh';
|
|
|
|
/**
|
|
* Return a new variable name that is not yet being used. This will try to
|
|
* generate single letter variable names in the range 'i' to 'z' to start with.
|
|
* If no unique name is located it will try 'i' to 'z', 'a' to 'h',
|
|
* then 'i2' to 'z2' etc. Skip 'l'.
|
|
*
|
|
* @param workspace The workspace to be unique in.
|
|
* @returns New variable name.
|
|
*/
|
|
export function generateUniqueName(workspace: Workspace): string {
|
|
return TEST_ONLY.generateUniqueNameInternal(workspace);
|
|
}
|
|
|
|
/**
|
|
* Private version of generateUniqueName for stubbing in tests.
|
|
*/
|
|
function generateUniqueNameInternal(workspace: Workspace): string {
|
|
return generateUniqueNameFromOptions(
|
|
VAR_LETTER_OPTIONS.charAt(0),
|
|
workspace.getAllVariableNames(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns a unique name that is not present in the usedNames array. This
|
|
* will try to generate single letter names in the range a - z (skip l). It
|
|
* will start with the character passed to startChar.
|
|
*
|
|
* @param startChar The character to start the search at.
|
|
* @param usedNames A list of all of the used names.
|
|
* @returns A unique name that is not present in the usedNames array.
|
|
*/
|
|
export function generateUniqueNameFromOptions(
|
|
startChar: string,
|
|
usedNames: string[],
|
|
): string {
|
|
if (!usedNames.length) {
|
|
return startChar;
|
|
}
|
|
|
|
const letters = VAR_LETTER_OPTIONS;
|
|
let suffix = '';
|
|
let letterIndex = letters.indexOf(startChar);
|
|
let potName = startChar;
|
|
|
|
while (true) {
|
|
let inUse = false;
|
|
for (let i = 0; i < usedNames.length; i++) {
|
|
if (usedNames[i].toLowerCase() === potName) {
|
|
inUse = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!inUse) {
|
|
break;
|
|
}
|
|
|
|
letterIndex++;
|
|
if (letterIndex === letters.length) {
|
|
// Reached the end of the character sequence so back to 'i'.
|
|
letterIndex = 0;
|
|
suffix = `${Number(suffix) + 1}`;
|
|
}
|
|
potName = letters.charAt(letterIndex) + suffix;
|
|
}
|
|
return potName;
|
|
}
|
|
|
|
/**
|
|
* Handles "Create Variable" button in the default variables toolbox category.
|
|
* It will prompt the user for a variable name, including re-prompts if a name
|
|
* is already in use among the workspace's variables.
|
|
*
|
|
* Custom button handlers can delegate to this function, allowing variables
|
|
* types and after-creation processing. More complex customization (e.g.,
|
|
* prompting for variable type) is beyond the scope of this function.
|
|
*
|
|
* @param workspace The workspace on which to create the variable.
|
|
* @param opt_callback A callback. It will be passed an acceptable new variable
|
|
* name, or null if change is to be aborted (cancel button), or undefined if
|
|
* an existing variable was chosen.
|
|
* @param opt_type The type of the variable like 'int', 'string', or ''. This
|
|
* will default to '', which is a specific type.
|
|
*/
|
|
export function createVariableButtonHandler(
|
|
workspace: Workspace,
|
|
opt_callback?: (p1?: string | null) => void,
|
|
opt_type?: string,
|
|
) {
|
|
const type = opt_type || '';
|
|
// This function needs to be named so it can be called recursively.
|
|
function promptAndCheckWithAlert(defaultName: string) {
|
|
promptName(Msg['NEW_VARIABLE_TITLE'], defaultName, function (text) {
|
|
if (!text) {
|
|
// User canceled prompt.
|
|
if (opt_callback) opt_callback(null);
|
|
return;
|
|
}
|
|
|
|
const existing = nameUsedWithAnyType(text, workspace);
|
|
if (!existing) {
|
|
// No conflict
|
|
workspace.createVariable(text, type);
|
|
if (opt_callback) opt_callback(text);
|
|
return;
|
|
}
|
|
|
|
let msg;
|
|
if (existing.getType() === type) {
|
|
msg = Msg['VARIABLE_ALREADY_EXISTS'].replace('%1', existing.getName());
|
|
} else {
|
|
msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE'];
|
|
msg = msg
|
|
.replace('%1', existing.getName())
|
|
.replace('%2', existing.getType());
|
|
}
|
|
dialog.alert(msg, function () {
|
|
promptAndCheckWithAlert(text);
|
|
});
|
|
});
|
|
}
|
|
promptAndCheckWithAlert('');
|
|
}
|
|
|
|
/**
|
|
* Opens a prompt that allows the user to enter a new name for a variable.
|
|
* Triggers a rename if the new name is valid. Or re-prompts if there is a
|
|
* collision.
|
|
*
|
|
* @param workspace The workspace on which to rename the variable.
|
|
* @param variable Variable to rename.
|
|
* @param opt_callback A callback. It will be passed an acceptable new variable
|
|
* name, or null if change is to be aborted (cancel button), or undefined if
|
|
* an existing variable was chosen.
|
|
*/
|
|
export function renameVariable(
|
|
workspace: Workspace,
|
|
variable: IVariableModel<IVariableState>,
|
|
opt_callback?: (p1?: string | null) => void,
|
|
) {
|
|
// This function needs to be named so it can be called recursively.
|
|
function promptAndCheckWithAlert(defaultName: string) {
|
|
const promptText = Msg['RENAME_VARIABLE_TITLE'].replace(
|
|
'%1',
|
|
variable.getName(),
|
|
);
|
|
promptName(promptText, defaultName, function (newName) {
|
|
if (!newName) {
|
|
// User canceled prompt.
|
|
if (opt_callback) opt_callback(null);
|
|
return;
|
|
}
|
|
|
|
const existing = nameUsedWithOtherType(
|
|
newName,
|
|
variable.getType(),
|
|
workspace,
|
|
);
|
|
const procedure = nameUsedWithConflictingParam(
|
|
variable.getName(),
|
|
newName,
|
|
workspace,
|
|
);
|
|
if (!existing && !procedure) {
|
|
// No conflict.
|
|
workspace.renameVariableById(variable.getId(), newName);
|
|
if (opt_callback) opt_callback(newName);
|
|
return;
|
|
}
|
|
|
|
let msg = '';
|
|
if (existing) {
|
|
msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE']
|
|
.replace('%1', existing.getName())
|
|
.replace('%2', existing.getType());
|
|
} else if (procedure) {
|
|
msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_A_PARAMETER']
|
|
.replace('%1', newName)
|
|
.replace('%2', procedure);
|
|
}
|
|
dialog.alert(msg, function () {
|
|
promptAndCheckWithAlert(newName);
|
|
});
|
|
});
|
|
}
|
|
promptAndCheckWithAlert('');
|
|
}
|
|
|
|
/**
|
|
* Prompt the user for a new variable name.
|
|
*
|
|
* @param promptText The string of the prompt.
|
|
* @param defaultText The default value to show in the prompt's field.
|
|
* @param callback A callback. It will be passed the new variable name, or null
|
|
* if the user picked something illegal.
|
|
*/
|
|
export function promptName(
|
|
promptText: string,
|
|
defaultText: string,
|
|
callback: (p1: string | null) => void,
|
|
) {
|
|
dialog.prompt(promptText, defaultText, function (newVar) {
|
|
// Merge runs of whitespace. Strip leading and trailing whitespace.
|
|
// Beyond this, all names are legal.
|
|
if (newVar) {
|
|
newVar = newVar.replace(/[\s\xa0]+/g, ' ').trim();
|
|
if (newVar === Msg['RENAME_VARIABLE'] || newVar === Msg['NEW_VARIABLE']) {
|
|
// Ok, not ALL names are legal...
|
|
newVar = null;
|
|
}
|
|
}
|
|
callback(newVar);
|
|
});
|
|
}
|
|
/**
|
|
* Check whether there exists a variable with the given name but a different
|
|
* type.
|
|
*
|
|
* @param name The name to search for.
|
|
* @param type The type to exclude from the search.
|
|
* @param workspace The workspace to search for the variable.
|
|
* @returns The variable with the given name and a different type, or null if
|
|
* none was found.
|
|
*/
|
|
function nameUsedWithOtherType(
|
|
name: string,
|
|
type: string,
|
|
workspace: Workspace,
|
|
): IVariableModel<IVariableState> | null {
|
|
const allVariables = workspace.getVariableMap().getAllVariables();
|
|
|
|
name = name.toLowerCase();
|
|
for (let i = 0, variable; (variable = allVariables[i]); i++) {
|
|
if (
|
|
variable.getName().toLowerCase() === name &&
|
|
variable.getType() !== type
|
|
) {
|
|
return variable;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Check whether there exists a variable with the given name of any type.
|
|
*
|
|
* @param name The name to search for.
|
|
* @param workspace The workspace to search for the variable.
|
|
* @returns The variable with the given name, or null if none was found.
|
|
*/
|
|
export function nameUsedWithAnyType(
|
|
name: string,
|
|
workspace: Workspace,
|
|
): IVariableModel<IVariableState> | null {
|
|
const allVariables = workspace.getVariableMap().getAllVariables();
|
|
|
|
name = name.toLowerCase();
|
|
for (let i = 0, variable; (variable = allVariables[i]); i++) {
|
|
if (variable.getName().toLowerCase() === name) {
|
|
return variable;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the procedure with a conflicting parameter name, or null
|
|
* if one does not exist.
|
|
*
|
|
* This checks the procedure map if it contains models, and the legacy procedure
|
|
* blocks otherwise.
|
|
*
|
|
* @param oldName The old name of the variable.
|
|
* @param newName The proposed name of the variable.
|
|
* @param workspace The workspace to search for conflicting parameters.
|
|
* @internal
|
|
*/
|
|
export function nameUsedWithConflictingParam(
|
|
oldName: string,
|
|
newName: string,
|
|
workspace: Workspace,
|
|
): string | null {
|
|
return workspace.getProcedureMap().getProcedures().length
|
|
? checkForConflictingParamWithProcedureModels(oldName, newName, workspace)
|
|
: checkForConflictingParamWithLegacyProcedures(oldName, newName, workspace);
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the procedure model with a conflicting param name, or
|
|
* null if one does not exist.
|
|
*/
|
|
function checkForConflictingParamWithProcedureModels(
|
|
oldName: string,
|
|
newName: string,
|
|
workspace: Workspace,
|
|
): string | null {
|
|
oldName = oldName.toLowerCase();
|
|
newName = newName.toLowerCase();
|
|
|
|
const procedures = workspace.getProcedureMap().getProcedures();
|
|
for (const procedure of procedures) {
|
|
const params = procedure
|
|
.getParameters()
|
|
.filter(isVariableBackedParameterModel)
|
|
.map((param) => param.getVariableModel().getName());
|
|
if (!params) continue;
|
|
const procHasOld = params.some((param) => param.toLowerCase() === oldName);
|
|
const procHasNew = params.some((param) => param.toLowerCase() === newName);
|
|
if (procHasOld && procHasNew) return procedure.getName();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the procedure block with a conflicting param name, or
|
|
* null if one does not exist.
|
|
*/
|
|
function checkForConflictingParamWithLegacyProcedures(
|
|
oldName: string,
|
|
newName: string,
|
|
workspace: Workspace,
|
|
): string | null {
|
|
oldName = oldName.toLowerCase();
|
|
newName = newName.toLowerCase();
|
|
|
|
const blocks = workspace.getAllBlocks(false);
|
|
for (const block of blocks) {
|
|
if (!isLegacyProcedureDefBlock(block)) continue;
|
|
const def = block.getProcedureDef();
|
|
const params = def[1];
|
|
const blockHasOld = params.some((param) => param.toLowerCase() === oldName);
|
|
const blockHasNew = params.some((param) => param.toLowerCase() === newName);
|
|
if (blockHasOld && blockHasNew) return def[0];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Generate DOM objects representing a variable field.
|
|
*
|
|
* @param variableModel The variable model to represent.
|
|
* @returns The generated DOM.
|
|
*/
|
|
export function generateVariableFieldDom(
|
|
variableModel: IVariableModel<IVariableState>,
|
|
): Element {
|
|
/* Generates the following XML:
|
|
* <field name="VAR" id="goKTKmYJ8DhVHpruv" variabletype="int">foo</field>
|
|
*/
|
|
const field = utilsXml.createElement('field');
|
|
field.setAttribute('name', 'VAR');
|
|
field.setAttribute('id', variableModel.getId());
|
|
field.setAttribute('variabletype', variableModel.getType());
|
|
const name = utilsXml.createTextNode(variableModel.getName());
|
|
field.appendChild(name);
|
|
return field;
|
|
}
|
|
|
|
/**
|
|
* Helper function to look up or create a variable on the given workspace.
|
|
* If no variable exists, creates and returns it.
|
|
*
|
|
* @param workspace The workspace to search for the variable. It may be a
|
|
* flyout workspace or main workspace.
|
|
* @param id The ID to use to look up or create the variable, or null.
|
|
* @param opt_name The string to use to look up or create the variable.
|
|
* @param opt_type The type to use to look up or create the variable.
|
|
* @returns The variable corresponding to the given ID or name + type
|
|
* combination.
|
|
*/
|
|
export function getOrCreateVariablePackage(
|
|
workspace: Workspace,
|
|
id: string | null,
|
|
opt_name?: string,
|
|
opt_type?: string,
|
|
): IVariableModel<IVariableState> {
|
|
let variable = getVariable(workspace, id, opt_name, opt_type);
|
|
if (!variable) {
|
|
variable = createVariable(workspace, id, opt_name, opt_type);
|
|
}
|
|
return variable;
|
|
}
|
|
|
|
/**
|
|
* Look up a variable on the given workspace.
|
|
* Always looks in the main workspace before looking in the flyout workspace.
|
|
* Always prefers lookup by ID to lookup by name + type.
|
|
*
|
|
* @param workspace The workspace to search for the variable. It may be a
|
|
* flyout workspace or main workspace.
|
|
* @param id The ID to use to look up the variable, or null.
|
|
* @param opt_name The string to use to look up the variable.
|
|
* Only used if lookup by ID fails.
|
|
* @param opt_type The type to use to look up the variable.
|
|
* Only used if lookup by ID fails.
|
|
* @returns The variable corresponding to the given ID or name + type
|
|
* combination, or null if not found.
|
|
*/
|
|
export function getVariable(
|
|
workspace: Workspace,
|
|
id: string | null,
|
|
opt_name?: string,
|
|
opt_type?: string,
|
|
): IVariableModel<IVariableState> | null {
|
|
const potentialVariableMap = workspace.getPotentialVariableMap();
|
|
let variable = null;
|
|
// Try to just get the variable, by ID if possible.
|
|
if (id) {
|
|
// Look in the real variable map before checking the potential variable map.
|
|
variable = workspace.getVariableById(id);
|
|
if (!variable && potentialVariableMap) {
|
|
variable = potentialVariableMap.getVariableById(id);
|
|
}
|
|
if (variable) {
|
|
return variable;
|
|
}
|
|
}
|
|
// If there was no ID, or there was an ID but it didn't match any variables,
|
|
// look up by name and type.
|
|
if (opt_name) {
|
|
if (opt_type === undefined) {
|
|
throw Error('Tried to look up a variable by name without a type');
|
|
}
|
|
// Otherwise look up by name and type.
|
|
variable = workspace.getVariable(opt_name, opt_type);
|
|
if (!variable && potentialVariableMap) {
|
|
variable = potentialVariableMap.getVariable(opt_name, opt_type);
|
|
}
|
|
}
|
|
return variable;
|
|
}
|
|
|
|
/**
|
|
* Helper function to create a variable on the given workspace.
|
|
*
|
|
* @param workspace The workspace in which to create the variable. It may be a
|
|
* flyout workspace or main workspace.
|
|
* @param id The ID to use to create the variable, or null.
|
|
* @param opt_name The string to use to create the variable.
|
|
* @param opt_type The type to use to create the variable.
|
|
* @returns The variable corresponding to the given ID or name + type
|
|
* combination.
|
|
*/
|
|
function createVariable(
|
|
workspace: Workspace,
|
|
id: string | null,
|
|
opt_name?: string,
|
|
opt_type?: string,
|
|
): IVariableModel<IVariableState> {
|
|
const potentialVariableMap = workspace.getPotentialVariableMap();
|
|
// Variables without names get uniquely named for this workspace.
|
|
if (!opt_name) {
|
|
const ws = workspace.isFlyout
|
|
? (workspace as WorkspaceSvg).targetWorkspace
|
|
: workspace;
|
|
opt_name = generateUniqueName(ws!);
|
|
}
|
|
|
|
// Create a potential variable if in the flyout.
|
|
let variable = null;
|
|
if (potentialVariableMap) {
|
|
variable = potentialVariableMap.createVariable(
|
|
opt_name,
|
|
opt_type,
|
|
id ?? undefined,
|
|
);
|
|
} else {
|
|
// In the main workspace, create a real variable.
|
|
variable = workspace.createVariable(opt_name, opt_type, id);
|
|
}
|
|
return variable;
|
|
}
|
|
|
|
/**
|
|
* Helper function to get the list of variables that have been added to the
|
|
* workspace after adding a new block, using the given list of variables that
|
|
* were in the workspace before the new block was added.
|
|
*
|
|
* @param workspace The workspace to inspect.
|
|
* @param originalVariables The array of variables that existed in the workspace
|
|
* before adding the new block.
|
|
* @returns The new array of variables that were freshly added to the workspace
|
|
* after creating the new block, or [] if no new variables were added to the
|
|
* workspace.
|
|
* @internal
|
|
*/
|
|
export function getAddedVariables(
|
|
workspace: Workspace,
|
|
originalVariables: IVariableModel<IVariableState>[],
|
|
): IVariableModel<IVariableState>[] {
|
|
const allCurrentVariables = workspace.getAllVariables();
|
|
const addedVariables = [];
|
|
if (originalVariables.length !== allCurrentVariables.length) {
|
|
for (let i = 0; i < allCurrentVariables.length; i++) {
|
|
const variable = allCurrentVariables[i];
|
|
// For any variable that is present in allCurrentVariables but not
|
|
// present in originalVariables, add the variable to addedVariables.
|
|
if (!originalVariables.includes(variable)) {
|
|
addedVariables.push(variable);
|
|
}
|
|
}
|
|
}
|
|
return addedVariables;
|
|
}
|
|
|
|
/**
|
|
* A custom compare function for the VariableModel objects.
|
|
*
|
|
* @param var1 First variable to compare.
|
|
* @param var2 Second variable to compare.
|
|
* @returns -1 if name of var1 is less than name of var2, 0 if equal, and 1 if
|
|
* greater.
|
|
* @internal
|
|
*/
|
|
export function compareByName(
|
|
var1: IVariableModel<IVariableState>,
|
|
var2: IVariableModel<IVariableState>,
|
|
): number {
|
|
return var1
|
|
.getName()
|
|
.localeCompare(var2.getName(), undefined, {sensitivity: 'base'});
|
|
}
|
|
|
|
/**
|
|
* Find all the uses of a named variable.
|
|
*
|
|
* @param workspace The workspace to search for the variable.
|
|
* @param id ID of the variable to find.
|
|
* @returns Array of block usages.
|
|
*/
|
|
export function getVariableUsesById(workspace: Workspace, id: string): Block[] {
|
|
const uses = [];
|
|
const blocks = workspace.getAllBlocks(false);
|
|
// Iterate through every block and check the name.
|
|
for (let i = 0; i < blocks.length; i++) {
|
|
const blockVariables = blocks[i].getVarModels();
|
|
if (blockVariables) {
|
|
for (let j = 0; j < blockVariables.length; j++) {
|
|
if (blockVariables[j].getId() === id) {
|
|
uses.push(blocks[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return uses;
|
|
}
|
|
|
|
/**
|
|
* Delete a variable and all of its uses from the given workspace. May prompt
|
|
* the user for confirmation.
|
|
*
|
|
* @param workspace The workspace from which to delete the variable.
|
|
* @param variable The variable to delete.
|
|
* @param triggeringBlock The block from which this deletion was triggered, if
|
|
* any. Used to exclude it from checking and warning about blocks
|
|
* referencing the variable being deleted.
|
|
*/
|
|
export function deleteVariable(
|
|
workspace: Workspace,
|
|
variable: IVariableModel<IVariableState>,
|
|
triggeringBlock?: Block,
|
|
) {
|
|
// Check whether this variable is a function parameter before deleting.
|
|
const variableName = variable.getName();
|
|
const uses = getVariableUsesById(workspace, variable.getId());
|
|
for (let i = uses.length - 1; i >= 0; i--) {
|
|
const block = uses[i];
|
|
if (
|
|
block.type === 'procedures_defnoreturn' ||
|
|
block.type === 'procedures_defreturn'
|
|
) {
|
|
const procedureName = String(block.getFieldValue('NAME'));
|
|
const deleteText = Msg['CANNOT_DELETE_VARIABLE_PROCEDURE']
|
|
.replace('%1', variableName)
|
|
.replace('%2', procedureName);
|
|
dialog.alert(deleteText);
|
|
return;
|
|
}
|
|
if (block === triggeringBlock) {
|
|
uses.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
if ((triggeringBlock && uses.length) || uses.length > 1) {
|
|
// Confirm before deleting multiple blocks.
|
|
const confirmText = Msg['DELETE_VARIABLE_CONFIRMATION']
|
|
.replace(
|
|
'%1',
|
|
String(
|
|
uses.length +
|
|
(triggeringBlock && !triggeringBlock.workspace.isFlyout ? 1 : 0),
|
|
),
|
|
)
|
|
.replace('%2', variableName);
|
|
dialog.confirm(confirmText, (ok) => {
|
|
if (ok && variable) {
|
|
workspace.getVariableMap().deleteVariable(variable);
|
|
}
|
|
});
|
|
} else {
|
|
// No confirmation necessary when the block that triggered the deletion is
|
|
// the only block referencing this variable or if only one block referencing
|
|
// this variable exists and the deletion was triggered programmatically.
|
|
workspace.getVariableMap().deleteVariable(variable);
|
|
}
|
|
}
|
|
|
|
export const TEST_ONLY = {
|
|
generateUniqueNameInternal,
|
|
};
|