hyper/lib/reducers/term-groups.js

219 lines
7.3 KiB
JavaScript

import uuid from 'uuid';
import Immutable from 'seamless-immutable';
import {TERM_GROUP_EXIT, TERM_GROUP_RESIZE} from '../constants/term-groups';
import {SESSION_ADD, SESSION_SET_ACTIVE} from '../constants/sessions';
import findBySession from '../utils/term-groups';
import {decorateTermGroupsReducer} from '../utils/plugins';
const MIN_SIZE = 0.05;
const initialState = Immutable({
termGroups: {},
activeSessions: {},
activeRootGroup: null
});
function TermGroup(obj) {
return Immutable({
uid: null,
sessionUid: null,
parentUid: null,
direction: null,
sizes: null,
children: []
}).merge(obj);
}
// Recurse upwards until we find a root term group (no parent).
const findRootGroup = (termGroups, uid) => {
const current = termGroups[uid];
if (!current.parentUid) {
return current;
}
return findRootGroup(termGroups, current.parentUid);
};
const setActiveGroup = (state, action) => {
if (!action.uid) {
return state.set('activeRootGroup', null);
}
const childGroup = findBySession(state, action.uid);
const rootGroup = findRootGroup(state.termGroups, childGroup.uid);
return state.set('activeRootGroup', rootGroup.uid).setIn(['activeSessions', rootGroup.uid], action.uid);
};
// Reduce existing sizes to fit a new split:
const insertRebalance = (oldSizes, index) => {
const newSize = 1 / (oldSizes.length + 1);
// We spread out how much each pane should be reduced
// with based on their existing size:
const balanced = oldSizes.map(size => size - newSize * size);
return [...balanced.slice(0, index), newSize, ...balanced.slice(index)];
};
// Spread out the removed size to all the existing sizes:
const removalRebalance = (oldSizes, index) => {
const removedSize = oldSizes[index];
const increase = removedSize / (oldSizes.length - 1);
return oldSizes.filter((_size, i) => i !== index).map(size => size + increase);
};
const splitGroup = (state, action) => {
const {splitDirection, uid, activeUid} = action;
const activeGroup = findBySession(state, activeUid);
// If we're splitting in the same direction as the current active
// group's parent - or if it's the first split for that group -
// we want the parent to get another child:
let parentGroup = activeGroup.parentUid ? state.termGroups[activeGroup.parentUid] : activeGroup;
// If we're splitting in a different direction, we want the current
// active group to become a new parent instead:
if (parentGroup.direction && parentGroup.direction !== splitDirection) {
parentGroup = activeGroup;
}
// If the group has a session (i.e. we're creating a new parent)
// we need to create two new groups,
// one for the existing session and one for the new split:
// P
// P -> / \
// G G
const newSession = TermGroup({
uid: uuid.v4(),
sessionUid: uid,
parentUid: parentGroup.uid
});
state = state.setIn(['termGroups', newSession.uid], newSession);
if (parentGroup.sessionUid) {
const existingSession = TermGroup({
uid: uuid.v4(),
sessionUid: parentGroup.sessionUid,
parentUid: parentGroup.uid
});
return state.setIn(['termGroups', existingSession.uid], existingSession).setIn(
['termGroups', parentGroup.uid],
parentGroup.merge({
sessionUid: null,
direction: splitDirection,
children: [existingSession.uid, newSession.uid]
})
);
}
const {children} = parentGroup;
// Insert the new child pane right after the active one:
const index = children.indexOf(activeGroup.uid) + 1;
const newChildren = [...children.slice(0, index), newSession.uid, ...children.slice(index)];
state = state.setIn(
['termGroups', parentGroup.uid],
parentGroup.merge({
direction: splitDirection,
children: newChildren
})
);
if (parentGroup.sizes) {
const newSizes = insertRebalance(parentGroup.sizes, index);
state = state.setIn(['termGroups', parentGroup.uid, 'sizes'], newSizes);
}
return state;
};
// Replace the parent by the given child in the tree,
// used when we remove another child and we're left
// with a one-to-one mapping between parent and child.
const replaceParent = (state, parent, child) => {
if (parent.parentUid) {
const parentParent = state.termGroups[parent.parentUid];
// If the parent we're replacing has a parent,
// we need to change the uid in its children array
// with `child`:
const newChildren = parentParent.children.map(uid => (uid === parent.uid ? child.uid : uid));
state = state.setIn(['termGroups', parentParent.uid, 'children'], newChildren);
} else {
// This means the given child will be
// a root group, so we need to set it up as such:
const newSessions = state.activeSessions.without(parent.uid).set(child.uid, state.activeSessions[parent.uid]);
state = state
.set('activeTermGroup', child.uid)
.set('activeRootGroup', child.uid)
.set('activeSessions', newSessions);
}
return state
.set('termGroups', state.termGroups.without(parent.uid))
.setIn(['termGroups', child.uid, 'parentUid'], parent.parentUid);
};
const removeGroup = (state, uid) => {
const group = state.termGroups[uid];
// when close tab with multiple panes, it remove group from parent to child. so maybe the parentUid exists but parent group have removed.
// it's safe to remove the group.
if (group.parentUid && state.termGroups[group.parentUid]) {
const parent = state.termGroups[group.parentUid];
const newChildren = parent.children.filter(childUid => childUid !== uid);
if (newChildren.length === 1) {
// Since we only have one child left,
// we can merge the parent and child into one group:
const child = state.termGroups[newChildren[0]];
state = replaceParent(state, parent, child);
} else {
state = state.setIn(['termGroups', group.parentUid, 'children'], newChildren);
if (parent.sizes) {
const childIndex = parent.children.indexOf(uid);
const newSizes = removalRebalance(parent.sizes, childIndex);
state = state.setIn(['termGroups', group.parentUid, 'sizes'], newSizes);
}
}
}
return state
.set('termGroups', state.termGroups.without(uid))
.set('activeSessions', state.activeSessions.without(uid));
};
const resizeGroup = (state, uid, sizes) => {
// Make sure none of the sizes fall below MIN_SIZE:
if (sizes.find(size => size < MIN_SIZE)) {
return state;
}
return state.setIn(['termGroups', uid, 'sizes'], sizes);
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case SESSION_ADD: {
if (action.splitDirection) {
state = splitGroup(state, action);
return setActiveGroup(state, action);
}
const uid = uuid.v4();
const termGroup = TermGroup({
uid,
sessionUid: action.uid
});
return state
.setIn(['termGroups', uid], termGroup)
.setIn(['activeSessions', uid], action.uid)
.set('activeRootGroup', uid);
}
case SESSION_SET_ACTIVE:
return setActiveGroup(state, action);
case TERM_GROUP_RESIZE:
return resizeGroup(state, action.uid, action.sizes);
case TERM_GROUP_EXIT:
return removeGroup(state, action.uid);
default:
return state;
}
};
export default decorateTermGroupsReducer(reducer);