refactor: make VariableMap implement IVariableMap. (#8395)

* refactor: make VariableMap implement IVariableMap.

* chore: remove unused arrayUtils import.

* chore: fix comment on variable map backing store.

* chore: Added JSDoc to new VariableMap methods.

* chore: Improve test descriptions.
This commit is contained in:
Aaron Dodson
2024-07-19 10:53:16 -07:00
committed by GitHub
parent 107403bc0f
commit 02e64bebbe
4 changed files with 164 additions and 71 deletions

View File

@@ -19,25 +19,26 @@ import './events/events_var_rename.js';
import type {Block} from './block.js';
import * as dialog from './dialog.js';
import * as eventUtils from './events/utils.js';
import * as registry from './registry.js';
import {Msg} from './msg.js';
import {Names} from './names.js';
import * as arrayUtils from './utils/array.js';
import * as idGenerator from './utils/idgenerator.js';
import {VariableModel} from './variable_model.js';
import type {Workspace} from './workspace.js';
import type {IVariableMap} from './interfaces/i_variable_map.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 {
export class VariableMap implements IVariableMap<VariableModel> {
/**
* A map from variable type to list of variable names. The lists contain
* 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, VariableModel[]>();
private variableMap = new Map<string, Map<string, VariableModel>>();
/** @param workspace The workspace this map belongs to. */
constructor(public workspace: Workspace) {}
@@ -45,8 +46,8 @@ export class VariableMap {
/** Clear the variable map. Fires events for every deletion. */
clear() {
for (const variables of this.variableMap.values()) {
while (variables.length > 0) {
this.deleteVariable(variables[0]);
for (const variable of variables.values()) {
this.deleteVariable(variable);
}
}
if (this.variableMap.size !== 0) {
@@ -60,10 +61,10 @@ export class VariableMap {
*
* @param variable Variable to rename.
* @param newName New variable name.
* @internal
* @returns The newly renamed variable.
*/
renameVariable(variable: VariableModel, newName: string) {
if (variable.name === newName) return;
renameVariable(variable: VariableModel, newName: string): VariableModel {
if (variable.name === newName) return variable;
const type = variable.type;
const conflictVar = this.getVariable(newName, type);
const blocks = this.workspace.getAllBlocks(false);
@@ -87,6 +88,20 @@ export class VariableMap {
} finally {
eventUtils.setGroup(existingGroup);
}
return variable;
}
changeVariableType(variable: VariableModel, newType: string): VariableModel {
this.variableMap.get(variable.getType())?.delete(variable.getId());
variable.setType(newType);
const newTypeVariables =
this.variableMap.get(newType) ?? new Map<string, VariableModel>();
newTypeVariables.set(variable.getId(), variable);
if (!this.variableMap.has(newType)) {
this.variableMap.set(newType, newTypeVariables);
}
return variable;
}
/**
@@ -159,8 +174,8 @@ export class VariableMap {
}
// Finally delete the original variable, which is now unreferenced.
eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable));
// And remove it from the list.
arrayUtils.removeElem(this.variableMap.get(type)!, variable);
// And remove it from the map.
this.variableMap.get(type)?.delete(variable.getId());
}
/* End functions for renaming variables. */
@@ -177,8 +192,8 @@ export class VariableMap {
*/
createVariable(
name: string,
opt_type?: string | null,
opt_id?: string | null,
opt_type?: string,
opt_id?: string,
): VariableModel {
let variable = this.getVariable(name, opt_type);
if (variable) {
@@ -204,20 +219,30 @@ export class VariableMap {
const type = opt_type || '';
variable = new VariableModel(this.workspace, name, type, id);
const variables = this.variableMap.get(type) || [];
variables.push(variable);
// Delete the list of variables of this type, and re-add it so that
// the most recent addition is at the end.
// This is used so the toolbox's set block is set to the most recent
// variable.
this.variableMap.delete(type);
this.variableMap.set(type, variables);
const variables =
this.variableMap.get(type) ?? new Map<string, VariableModel>();
variables.set(variable.getId(), variable);
if (!this.variableMap.has(type)) {
this.variableMap.set(type, variables);
}
eventUtils.fire(new (eventUtils.get(eventUtils.VAR_CREATE))(variable));
return variable;
}
/**
* Adds the given variable to this variable map.
*
* @param variable The variable to add.
*/
addVariable(variable: VariableModel) {
const type = variable.getType();
if (!this.variableMap.has(type)) {
this.variableMap.set(type, new Map<string, VariableModel>());
}
this.variableMap.get(type)?.set(variable.getId(), variable);
}
/* Begin functions for variable deletion. */
/**
* Delete a variable.
@@ -225,22 +250,12 @@ export class VariableMap {
* @param variable Variable to delete.
*/
deleteVariable(variable: VariableModel) {
const variableId = variable.getId();
const variableList = this.variableMap.get(variable.type);
if (variableList) {
for (let i = 0; i < variableList.length; i++) {
const tempVar = variableList[i];
if (tempVar.getId() === variableId) {
variableList.splice(i, 1);
eventUtils.fire(
new (eventUtils.get(eventUtils.VAR_DELETE))(variable),
);
if (variableList.length === 0) {
this.variableMap.delete(variable.type);
}
return;
}
}
const variables = this.variableMap.get(variable.type);
if (!variables || !variables.has(variable.getId())) return;
variables.delete(variable.getId());
eventUtils.fire(new (eventUtils.get(eventUtils.VAR_DELETE))(variable));
if (variables.size === 0) {
this.variableMap.delete(variable.type);
}
}
@@ -321,17 +336,16 @@ export class VariableMap {
* 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 | null): VariableModel | null {
getVariable(name: string, opt_type?: string): VariableModel | null {
const type = opt_type || '';
const list = this.variableMap.get(type);
if (list) {
for (let j = 0, variable; (variable = list[j]); j++) {
if (Names.equals(variable.name, name)) {
return variable;
}
}
}
return null;
const variables = this.variableMap.get(type);
if (!variables) return null;
return (
[...variables.values()].find((variable) =>
Names.equals(variable.getName(), name),
) ?? null
);
}
/**
@@ -342,10 +356,8 @@ export class VariableMap {
*/
getVariableById(id: string): VariableModel | null {
for (const variables of this.variableMap.values()) {
for (const variable of variables) {
if (variable.getId() === id) {
return variable;
}
if (variables.has(id)) {
return variables.get(id) ?? null;
}
}
return null;
@@ -361,11 +373,19 @@ export class VariableMap {
*/
getVariablesOfType(type: string | null): VariableModel[] {
type = type || '';
const variableList = this.variableMap.get(type);
if (variableList) {
return variableList.slice();
}
return [];
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()];
}
/**
@@ -399,7 +419,7 @@ export class VariableMap {
getAllVariables(): VariableModel[] {
let allVariables: VariableModel[] = [];
for (const variables of this.variableMap.values()) {
allVariables = allVariables.concat(variables);
allVariables = allVariables.concat(...variables.values());
}
return allVariables;
}
@@ -410,9 +430,13 @@ export class VariableMap {
* @returns All of the variable names of all types.
*/
getAllVariableNames(): string[] {
return Array.from(this.variableMap.values())
.flat()
.map((variable) => variable.name);
const names: string[] = [];
for (const variables of this.variableMap.values()) {
for (const variable of variables.values()) {
names.push(variable.getName());
}
}
return names;
}
/**
@@ -438,3 +462,5 @@ export class VariableMap {
return uses;
}
}
registry.register(registry.Type.VARIABLE_MAP, registry.DEFAULT, VariableMap);

View File

@@ -610,7 +610,11 @@ function createVariable(
// Create a potential variable if in the flyout.
let variable = null;
if (potentialVariableMap) {
variable = potentialVariableMap.createVariable(opt_name, opt_type, id);
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);

View File

@@ -400,7 +400,11 @@ export class Workspace implements IASTNodeLocation {
opt_type?: string | null,
opt_id?: string | null,
): VariableModel {
return this.variableMap.createVariable(name, opt_type, opt_id);
return this.variableMap.createVariable(
name,
opt_type ?? undefined,
opt_id ?? undefined,
);
}
/**
@@ -456,7 +460,7 @@ export class Workspace implements IASTNodeLocation {
* if none are found.
*/
getVariablesOfType(type: string | null): VariableModel[] {
return this.variableMap.getVariablesOfType(type);
return this.variableMap.getVariablesOfType(type ?? '');
}
/**

View File

@@ -39,17 +39,17 @@ suite('Variable Map', function () {
this.variableMap.createVariable('name1', 'type1', 'id1');
// Assert there is only one variable in the this.variableMap.
let keys = Array.from(this.variableMap.variableMap.keys());
let keys = this.variableMap.getTypes();
assert.equal(keys.length, 1);
let varMapLength = this.variableMap.variableMap.get(keys[0]).length;
let varMapLength = this.variableMap.getVariablesOfType(keys[0]).length;
assert.equal(varMapLength, 1);
this.variableMap.createVariable('name1', 'type1');
assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
// Check that the size of the variableMap did not change.
keys = Array.from(this.variableMap.variableMap.keys());
keys = this.variableMap.getTypes();
assert.equal(keys.length, 1);
varMapLength = this.variableMap.variableMap.get(keys[0]).length;
varMapLength = this.variableMap.getVariablesOfType(keys[0]).length;
assert.equal(varMapLength, 1);
});
@@ -59,16 +59,16 @@ suite('Variable Map', function () {
this.variableMap.createVariable('name1', 'type1', 'id1');
// Assert there is only one variable in the this.variableMap.
let keys = Array.from(this.variableMap.variableMap.keys());
let keys = this.variableMap.getTypes();
assert.equal(keys.length, 1);
const varMapLength = this.variableMap.variableMap.get(keys[0]).length;
const varMapLength = this.variableMap.getVariablesOfType(keys[0]).length;
assert.equal(varMapLength, 1);
this.variableMap.createVariable('name1', 'type2', 'id2');
assertVariableValues(this.variableMap, 'name1', 'type1', 'id1');
assertVariableValues(this.variableMap, 'name1', 'type2', 'id2');
// Check that the size of the variableMap did change.
keys = Array.from(this.variableMap.variableMap.keys());
keys = this.variableMap.getTypes();
assert.equal(keys.length, 2);
});
@@ -246,6 +246,65 @@ suite('Variable Map', function () {
});
});
suite(
'Using changeVariableType to change the type of a variable',
function () {
test('updates it to a new non-empty value', function () {
const variable = this.variableMap.createVariable(
'name1',
'type1',
'id1',
);
this.variableMap.changeVariableType(variable, 'type2');
const oldTypeVariables = this.variableMap.getVariablesOfType('type1');
const newTypeVariables = this.variableMap.getVariablesOfType('type2');
assert.deepEqual(oldTypeVariables, []);
assert.deepEqual(newTypeVariables, [variable]);
assert.equal(variable.getType(), 'type2');
});
test('updates it to a new empty value', function () {
const variable = this.variableMap.createVariable(
'name1',
'type1',
'id1',
);
this.variableMap.changeVariableType(variable, '');
const oldTypeVariables = this.variableMap.getVariablesOfType('type1');
const newTypeVariables = this.variableMap.getVariablesOfType('');
assert.deepEqual(oldTypeVariables, []);
assert.deepEqual(newTypeVariables, [variable]);
assert.equal(variable.getType(), '');
});
},
);
suite('addVariable', function () {
test('normally', function () {
const variable = new Blockly.VariableModel(this.workspace, 'foo', 'int');
assert.isNull(this.variableMap.getVariableById(variable.getId()));
this.variableMap.addVariable(variable);
assert.equal(
this.variableMap.getVariableById(variable.getId()),
variable,
);
});
});
suite('getTypes', function () {
test('when map is empty', function () {
const types = this.variableMap.getTypes();
assert.deepEqual(types, []);
});
test('with various types', function () {
this.variableMap.createVariable('name1', 'type1', 'id1');
this.variableMap.createVariable('name2', '', 'id2');
const types = this.variableMap.getTypes();
assert.deepEqual(types, ['type1', '']);
});
});
suite('getAllVariables', function () {
test('Trivial', function () {
const var1 = this.variableMap.createVariable('name1', 'type1', 'id1');