mirror of
https://github.com/google/blockly.git
synced 2026-01-04 15:40:08 +01:00
475 lines
14 KiB
TypeScript
475 lines
14 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* Object representing a map of variables and their types.
|
|
*
|
|
* @class
|
|
*/
|
|
// Former goog.module ID: Blockly.VariableMap
|
|
|
|
// Unused import preserved for side-effects. Remove if unneeded.
|
|
import './events/events_var_delete.js';
|
|
// Unused import preserved for side-effects. Remove if unneeded.
|
|
import './events/events_var_rename.js';
|
|
|
|
import type {Block} from './block.js';
|
|
import {EventType} from './events/type.js';
|
|
import * as eventUtils from './events/utils.js';
|
|
import type {IVariableMap} from './interfaces/i_variable_map.js';
|
|
import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
|
|
import {Names} from './names.js';
|
|
import * as registry from './registry.js';
|
|
import * as deprecation from './utils/deprecation.js';
|
|
import * as idGenerator from './utils/idgenerator.js';
|
|
import {deleteVariable, getVariableUsesById} from './variables.js';
|
|
import type {Workspace} from './workspace.js';
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
export class VariableMap
|
|
implements IVariableMap<IVariableModel<IVariableState>>
|
|
{
|
|
/**
|
|
* A map from variable type to map of IDs to variables. The maps contain
|
|
* all of the named variables in the workspace, including variables that are
|
|
* not currently in use.
|
|
*/
|
|
private variableMap = new Map<
|
|
string,
|
|
Map<string, IVariableModel<IVariableState>>
|
|
>();
|
|
|
|
/**
|
|
* @param workspace The workspace this map belongs to.
|
|
* @param potentialMap True if this holds variables that don't exist in the
|
|
* workspace yet.
|
|
*/
|
|
constructor(
|
|
public workspace: Workspace,
|
|
public potentialMap = false,
|
|
) {}
|
|
|
|
/** Clear the variable map. Fires events for every deletion. */
|
|
clear() {
|
|
for (const variables of this.variableMap.values()) {
|
|
for (const variable of variables.values()) {
|
|
this.deleteVariable(variable);
|
|
}
|
|
}
|
|
if (this.variableMap.size !== 0) {
|
|
throw Error('Non-empty variable map');
|
|
}
|
|
}
|
|
|
|
/* Begin functions for renaming variables. */
|
|
/**
|
|
* Rename the given variable by updating its name in the variable map.
|
|
*
|
|
* @param variable Variable to rename.
|
|
* @param newName New variable name.
|
|
* @returns The newly renamed variable.
|
|
*/
|
|
renameVariable(
|
|
variable: IVariableModel<IVariableState>,
|
|
newName: string,
|
|
): IVariableModel<IVariableState> {
|
|
if (variable.getName() === newName) return variable;
|
|
const type = variable.getType();
|
|
const conflictVar = this.getVariable(newName, type);
|
|
const blocks = this.workspace.getAllBlocks(false);
|
|
let existingGroup = '';
|
|
if (!this.potentialMap) {
|
|
existingGroup = eventUtils.getGroup();
|
|
if (!existingGroup) {
|
|
eventUtils.setGroup(true);
|
|
}
|
|
}
|
|
try {
|
|
// The IDs may match if the rename is a simple case change (name1 ->
|
|
// Name1).
|
|
if (!conflictVar || conflictVar.getId() === variable.getId()) {
|
|
this.renameVariableAndUses(variable, newName, blocks);
|
|
} else {
|
|
this.renameVariableWithConflict(variable, newName, conflictVar, blocks);
|
|
}
|
|
} finally {
|
|
if (!this.potentialMap) eventUtils.setGroup(existingGroup);
|
|
}
|
|
return variable;
|
|
}
|
|
|
|
changeVariableType(
|
|
variable: IVariableModel<IVariableState>,
|
|
newType: string,
|
|
): IVariableModel<IVariableState> {
|
|
const oldType = variable.getType();
|
|
if (oldType === newType) return variable;
|
|
|
|
const oldTypeVariables = this.variableMap.get(oldType);
|
|
oldTypeVariables?.delete(variable.getId());
|
|
if (oldTypeVariables?.size === 0) {
|
|
this.variableMap.delete(oldType);
|
|
}
|
|
variable.setType(newType);
|
|
const newTypeVariables =
|
|
this.variableMap.get(newType) ??
|
|
new Map<string, IVariableModel<IVariableState>>();
|
|
newTypeVariables.set(variable.getId(), variable);
|
|
if (!this.variableMap.has(newType)) {
|
|
this.variableMap.set(newType, newTypeVariables);
|
|
}
|
|
eventUtils.fire(
|
|
new (eventUtils.get(EventType.VAR_TYPE_CHANGE))(
|
|
variable,
|
|
oldType,
|
|
newType,
|
|
),
|
|
);
|
|
return variable;
|
|
}
|
|
|
|
/**
|
|
* Rename a variable by updating its name in the variable map. Identify the
|
|
* variable to rename with the given ID.
|
|
*
|
|
* @deprecated v12: use VariableMap.renameVariable.
|
|
* @param id ID of the variable to rename.
|
|
* @param newName New variable name.
|
|
*/
|
|
renameVariableById(id: string, newName: string) {
|
|
deprecation.warn(
|
|
'VariableMap.renameVariableById',
|
|
'v12',
|
|
'v13',
|
|
'VariableMap.renameVariable',
|
|
);
|
|
const variable = this.getVariableById(id);
|
|
if (!variable) {
|
|
throw 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 variable Variable to rename.
|
|
* @param newName New variable name.
|
|
* @param blocks The list of all blocks in the workspace.
|
|
*/
|
|
private renameVariableAndUses(
|
|
variable: IVariableModel<IVariableState>,
|
|
newName: string,
|
|
blocks: Block[],
|
|
) {
|
|
if (!this.potentialMap) {
|
|
eventUtils.fire(
|
|
new (eventUtils.get(EventType.VAR_RENAME))(variable, newName),
|
|
);
|
|
}
|
|
variable.setName(newName);
|
|
for (let 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 variable Variable to rename.
|
|
* @param newName New variable name.
|
|
* @param conflictVar The variable that was already using newName.
|
|
* @param blocks The list of all blocks in the workspace.
|
|
*/
|
|
private renameVariableWithConflict(
|
|
variable: IVariableModel<IVariableState>,
|
|
newName: string,
|
|
conflictVar: IVariableModel<IVariableState>,
|
|
blocks: Block[],
|
|
) {
|
|
const type = variable.getType();
|
|
const oldCase = conflictVar.getName();
|
|
|
|
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 (let i = 0; i < blocks.length; i++) {
|
|
blocks[i].renameVarById(variable.getId(), conflictVar.getId());
|
|
}
|
|
if (!this.potentialMap) {
|
|
// Finally delete the original variable, which is now unreferenced.
|
|
eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable));
|
|
}
|
|
// And remove it from the map.
|
|
this.variableMap.get(type)?.delete(variable.getId());
|
|
}
|
|
|
|
/* End functions for renaming variables. */
|
|
/**
|
|
* Create a variable with a given name, optional type, and optional ID.
|
|
*
|
|
* @param name The name of the variable. This must be unique across variables
|
|
* and procedures.
|
|
* @param 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 opt_id The unique ID of the variable. This will default to a UUID.
|
|
* @returns The newly created variable.
|
|
*/
|
|
createVariable(
|
|
name: string,
|
|
opt_type?: string,
|
|
opt_id?: string,
|
|
): IVariableModel<IVariableState> {
|
|
let variable = this.getVariable(name, opt_type);
|
|
if (variable) {
|
|
if (opt_id && variable.getId() !== opt_id) {
|
|
throw Error(
|
|
'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 && this.getVariableById(opt_id)) {
|
|
throw Error('Variable id, "' + opt_id + '", is already in use.');
|
|
}
|
|
const id = opt_id || idGenerator.genUid();
|
|
const type = opt_type || '';
|
|
const VariableModel = registry.getClassFromOptions(
|
|
registry.Type.VARIABLE_MODEL,
|
|
this.workspace.options,
|
|
true,
|
|
);
|
|
if (!VariableModel) {
|
|
throw new Error('No variable model is registered.');
|
|
}
|
|
variable = new VariableModel(this.workspace, name, type, id);
|
|
|
|
const variables =
|
|
this.variableMap.get(type) ??
|
|
new Map<string, IVariableModel<IVariableState>>();
|
|
variables.set(variable.getId(), variable);
|
|
if (!this.variableMap.has(type)) {
|
|
this.variableMap.set(type, variables);
|
|
}
|
|
if (!this.potentialMap) {
|
|
eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable));
|
|
}
|
|
return variable;
|
|
}
|
|
|
|
/**
|
|
* Adds the given variable to this variable map.
|
|
*
|
|
* @param variable The variable to add.
|
|
*/
|
|
addVariable(variable: IVariableModel<IVariableState>) {
|
|
const type = variable.getType();
|
|
if (!this.variableMap.has(type)) {
|
|
this.variableMap.set(
|
|
type,
|
|
new Map<string, IVariableModel<IVariableState>>(),
|
|
);
|
|
}
|
|
this.variableMap.get(type)?.set(variable.getId(), variable);
|
|
}
|
|
|
|
/* Begin functions for variable deletion. */
|
|
/**
|
|
* Delete a variable and all of its uses without confirmation.
|
|
*
|
|
* @param variable Variable to delete.
|
|
*/
|
|
deleteVariable(variable: IVariableModel<IVariableState>) {
|
|
const uses = getVariableUsesById(this.workspace, variable.getId());
|
|
let existingGroup = '';
|
|
if (!this.potentialMap) {
|
|
existingGroup = eventUtils.getGroup();
|
|
if (!existingGroup) {
|
|
eventUtils.setGroup(true);
|
|
}
|
|
}
|
|
try {
|
|
for (let i = 0; i < uses.length; i++) {
|
|
uses[i].dispose(true);
|
|
}
|
|
const variables = this.variableMap.get(variable.getType());
|
|
if (!variables || !variables.has(variable.getId())) return;
|
|
variables.delete(variable.getId());
|
|
if (!this.potentialMap) {
|
|
eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable));
|
|
}
|
|
if (variables.size === 0) {
|
|
this.variableMap.delete(variable.getType());
|
|
}
|
|
} finally {
|
|
if (!this.potentialMap) {
|
|
eventUtils.setGroup(existingGroup);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a variables by the passed in ID and all of its uses from this
|
|
* workspace. May prompt the user for confirmation.
|
|
*
|
|
* @deprecated v12: use Blockly.Variables.deleteVariable.
|
|
* @param id ID of variable to delete.
|
|
*/
|
|
deleteVariableById(id: string) {
|
|
deprecation.warn(
|
|
'VariableMap.deleteVariableById',
|
|
'v12',
|
|
'v13',
|
|
'Blockly.Variables.deleteVariable',
|
|
);
|
|
const variable = this.getVariableById(id);
|
|
if (variable) {
|
|
deleteVariable(this.workspace, variable);
|
|
}
|
|
}
|
|
|
|
/* 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 name The name to check for.
|
|
* @param opt_type The type of the variable. If not provided it defaults to
|
|
* the empty string, which is a specific type.
|
|
* @returns The variable with the given name, or null if it was not found.
|
|
*/
|
|
getVariable(
|
|
name: string,
|
|
opt_type?: string,
|
|
): IVariableModel<IVariableState> | null {
|
|
const type = opt_type || '';
|
|
const variables = this.variableMap.get(type);
|
|
if (!variables) return null;
|
|
|
|
return (
|
|
[...variables.values()].find((variable) =>
|
|
Names.equals(variable.getName(), name),
|
|
) ?? null
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Find the variable by the given ID and return it. Return null if not found.
|
|
*
|
|
* @param id The ID to check for.
|
|
* @returns The variable with the given ID.
|
|
*/
|
|
getVariableById(id: string): IVariableModel<IVariableState> | null {
|
|
for (const variables of this.variableMap.values()) {
|
|
if (variables.has(id)) {
|
|
return variables.get(id) ?? null;
|
|
}
|
|
}
|
|
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 type Type of the variables to find.
|
|
* @returns The sought after variables of the passed in type. An empty array
|
|
* if none are found.
|
|
*/
|
|
getVariablesOfType(type: string | null): IVariableModel<IVariableState>[] {
|
|
type = type || '';
|
|
const variables = this.variableMap.get(type);
|
|
if (!variables) return [];
|
|
|
|
return [...variables.values()];
|
|
}
|
|
|
|
/**
|
|
* Returns a list of unique types of variables in this variable map.
|
|
*
|
|
* @returns A list of unique types of variables in this variable map.
|
|
*/
|
|
getTypes(): string[] {
|
|
return [...this.variableMap.keys()];
|
|
}
|
|
|
|
/**
|
|
* Return all variables of all types.
|
|
*
|
|
* @returns List of variable models.
|
|
*/
|
|
getAllVariables(): IVariableModel<IVariableState>[] {
|
|
let allVariables: IVariableModel<IVariableState>[] = [];
|
|
for (const variables of this.variableMap.values()) {
|
|
allVariables = allVariables.concat(...variables.values());
|
|
}
|
|
return allVariables;
|
|
}
|
|
|
|
/**
|
|
* Returns all of the variable names of all types.
|
|
*
|
|
* @deprecated v12: use Blockly.Variables.getAllVariables.
|
|
* @returns All of the variable names of all types.
|
|
*/
|
|
getAllVariableNames(): string[] {
|
|
deprecation.warn(
|
|
'VariableMap.getAllVariableNames',
|
|
'v12',
|
|
'v13',
|
|
'Blockly.Variables.getAllVariables',
|
|
);
|
|
const names: string[] = [];
|
|
for (const variables of this.variableMap.values()) {
|
|
for (const variable of variables.values()) {
|
|
names.push(variable.getName());
|
|
}
|
|
}
|
|
return names;
|
|
}
|
|
|
|
/**
|
|
* Find all the uses of a named variable.
|
|
*
|
|
* @deprecated v12: use Blockly.Variables.getVariableUsesById.
|
|
* @param id ID of the variable to find.
|
|
* @returns Array of block usages.
|
|
*/
|
|
getVariableUsesById(id: string): Block[] {
|
|
deprecation.warn(
|
|
'VariableMap.getVariableUsesById',
|
|
'v12',
|
|
'v13',
|
|
'Blockly.Variables.getVariableUsesById',
|
|
);
|
|
return getVariableUsesById(this.workspace, id);
|
|
}
|
|
}
|
|
|
|
registry.register(registry.Type.VARIABLE_MAP, registry.DEFAULT, VariableMap);
|