Files
blockly/core/variable_map.ts

461 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> {
this.variableMap.get(variable.getType())?.delete(variable.getId());
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);
}
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.getObject(
registry.Type.VARIABLE_MODEL,
registry.DEFAULT,
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);