mirror of
https://github.com/google/blockly.git
synced 2026-01-08 17:40:09 +01:00
* docs: Make JSDoc @deprecated usage more consistent * refactor(VariableMap)!: Refresh toolbox when map modified Delete the following methods from WorkspaceSvg: - renameVariableById - deleteVariableById - createVariable Modify the following methods on VariableMap to call this.workspace.refreshToolboxSelection() if this.workspace is a WorkspaceSvg, replicating the behaviour of the aforementioned deleted methods and additionally ensuring that that method is called following any change to the variable map: - renameVariable - changeVariableType - renameVariableAndUses - createVariable - addVariable - deleteVariable BREAKING CHANGE: This change ensures that the toolbox will be refreshed regardless of what route the VaribleMap was updated, rather than only being refreshed when it is updated via calls to methods on WorkspaceSvg. Overall this is much more likely to fix a bug (where the toolbox wasn't being refreshed when it should have been) than cause one (by refreshing the toolbox when it shouldn't be), but this is still a behaviour change which could _conceivably_ result an unexpected regression. * refactor(VariableMap): Remove calls to deprecated getVariableUsesById Also refactor to use named imports core/variables.ts methods. * refactor(Workspace): Use named imports for core/variables.ts methods * refactor(FieldVariable): Remove call to deprecated getVariablesOfType * refactor(variables): Remove calls to deprecated methods * refactor(variables_dynamic): Remove call to deprecated getAllVariables * refactor(xml): Remove calls to deprecated createVariable * refactor(Events.VarCreate): Remove calls to deprecated methods * refactor(Events.VarDelete): Remove calls to deprecated methods * refactor(Events.VarRename): Remove calls to deprecated methods
457 lines
14 KiB
TypeScript
457 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';
|
|
import {WorkspaceSvg} from './workspace_svg.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. */
|
|
constructor(public workspace: Workspace) {}
|
|
|
|
/** 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);
|
|
const 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 {
|
|
eventUtils.setGroup(existingGroup);
|
|
}
|
|
if (this.workspace instanceof WorkspaceSvg) {
|
|
this.workspace.refreshToolboxSelection();
|
|
}
|
|
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);
|
|
}
|
|
if (this.workspace instanceof WorkspaceSvg) {
|
|
this.workspace.refreshToolboxSelection();
|
|
}
|
|
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[],
|
|
) {
|
|
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);
|
|
}
|
|
if (this.workspace instanceof WorkspaceSvg) {
|
|
this.workspace.refreshToolboxSelection();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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());
|
|
}
|
|
// 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);
|
|
}
|
|
eventUtils.fire(new (eventUtils.get(EventType.VAR_CREATE))(variable));
|
|
if (this.workspace instanceof WorkspaceSvg) {
|
|
this.workspace.refreshToolboxSelection();
|
|
}
|
|
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);
|
|
if (this.workspace instanceof WorkspaceSvg) {
|
|
this.workspace.refreshToolboxSelection();
|
|
}
|
|
}
|
|
|
|
/* 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());
|
|
const 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());
|
|
eventUtils.fire(new (eventUtils.get(EventType.VAR_DELETE))(variable));
|
|
if (variables.size === 0) {
|
|
this.variableMap.delete(variable.getType());
|
|
}
|
|
} finally {
|
|
eventUtils.setGroup(existingGroup);
|
|
}
|
|
if (this.workspace instanceof WorkspaceSvg) {
|
|
this.workspace.refreshToolboxSelection();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|