Files
atom/src/menu-sort-helpers.js
2019-05-31 18:33:56 +02:00

185 lines
4.8 KiB
JavaScript

// UTILS
function splitArray(arr, predicate) {
let lastArr = [];
const multiArr = [lastArr];
arr.forEach(item => {
if (predicate(item)) {
if (lastArr.length > 0) {
lastArr = [];
multiArr.push(lastArr);
}
} else {
lastArr.push(item);
}
});
return multiArr;
}
function joinArrays(arrays, joiner) {
const joinedArr = [];
arrays.forEach((arr, i) => {
if (i > 0 && arr.length > 0) {
joinedArr.push(joiner);
}
joinedArr.push(...arr);
});
return joinedArr;
}
const pushOntoMultiMap = (map, key, value) => {
if (!map.has(key)) {
map.set(key, []);
}
map.get(key).push(value);
};
function indexOfGroupContainingCommand(groups, command, ignoreGroup) {
return groups.findIndex(
candiateGroup =>
candiateGroup !== ignoreGroup &&
candiateGroup.some(candidateItem => candidateItem.command === command)
);
}
// Sort nodes topologically using a depth-first approach. Encountered cycles
// are broken.
function sortTopologically(originalOrder, edgesById) {
const sorted = [];
const marked = new Set();
function visit(id) {
if (marked.has(id)) {
// Either this node has already been placed, or we have encountered a
// cycle and need to exit.
return;
}
marked.add(id);
const edges = edgesById.get(id);
if (edges != null) {
edges.forEach(visit);
}
sorted.push(id);
}
originalOrder.forEach(visit);
return sorted;
}
function attemptToMergeAGroup(groups) {
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
for (const item of group) {
const toCommands = [...(item.before || []), ...(item.after || [])];
for (const command of toCommands) {
const index = indexOfGroupContainingCommand(groups, command, group);
if (index === -1) {
// No valid edge for this command
continue;
}
const mergeTarget = groups[index];
// Merge with group containing `command`
mergeTarget.push(...group);
groups.splice(i, 1);
return true;
}
}
}
return false;
}
// Merge groups based on before/after positions
// Mutates both the array of groups, and the individual group arrays.
function mergeGroups(groups) {
let mergedAGroup = true;
while (mergedAGroup) {
mergedAGroup = attemptToMergeAGroup(groups);
}
return groups;
}
function sortItemsInGroup(group) {
const originalOrder = group.map((node, i) => i);
const edges = new Map();
const commandToIndex = new Map(group.map((item, i) => [item.command, i]));
group.forEach((item, i) => {
if (item.before) {
item.before.forEach(toCommand => {
const to = commandToIndex.get(toCommand);
if (to != null) {
pushOntoMultiMap(edges, to, i);
}
});
}
if (item.after) {
item.after.forEach(toCommand => {
const to = commandToIndex.get(toCommand);
if (to != null) {
pushOntoMultiMap(edges, i, to);
}
});
}
});
const sortedNodes = sortTopologically(originalOrder, edges);
return sortedNodes.map(i => group[i]);
}
function findEdgesInGroup(groups, i, edges) {
const group = groups[i];
for (const item of group) {
if (item.beforeGroupContaining) {
for (const command of item.beforeGroupContaining) {
const to = indexOfGroupContainingCommand(groups, command, group);
if (to !== -1) {
pushOntoMultiMap(edges, to, i);
return;
}
}
}
if (item.afterGroupContaining) {
for (const command of item.afterGroupContaining) {
const to = indexOfGroupContainingCommand(groups, command, group);
if (to !== -1) {
pushOntoMultiMap(edges, i, to);
return;
}
}
}
}
}
function sortGroups(groups) {
const originalOrder = groups.map((item, i) => i);
const edges = new Map();
for (let i = 0; i < groups.length; i++) {
findEdgesInGroup(groups, i, edges);
}
const sortedGroupIndexes = sortTopologically(originalOrder, edges);
return sortedGroupIndexes.map(i => groups[i]);
}
function isSeparator(item) {
return item.type === 'separator';
}
function sortMenuItems(menuItems) {
// Split the items into their implicit groups based upon separators.
const groups = splitArray(menuItems, isSeparator);
// Merge groups that contain before/after references to eachother.
const mergedGroups = mergeGroups(groups);
// Sort each individual group internally.
const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup);
// Sort the groups based upon their beforeGroupContaining/afterGroupContaining
// references.
const sortedGroups = sortGroups(mergedGroupsWithSortedItems);
// Join the groups back
return joinArrays(sortedGroups, { type: 'separator' });
}
module.exports = { sortMenuItems };