Home Reference Source Repository

src/notebook/reducers/document.js

import Immutable from 'immutable';
import { handleActions } from 'redux-actions';
import * as uuid from 'uuid';
import * as commutable from 'commutable';

import * as constants from '../constants';

export default handleActions({
  [constants.SET_NOTEBOOK]: function setNotebook(state, action) {
    const notebook = action.notebook
      .update('cellMap', (cells) =>
        cells.map((value) =>
          value.set('inputHidden', false)
                .set('outputHidden', false)
                .set('status', '')));

    return state.set('notebook', notebook)
      .set('focusedCell', notebook.getIn(['cellOrder', 0]));
  },
  [constants.FOCUS_CELL]: function focusCell(state, action) {
    return state.set('focusedCell', action.id);
  },
  [constants.FOCUS_NEXT_CELL]: function focusNextCell(state, action) {
    const cellOrder = state.getIn(['notebook', 'cellOrder']);
    const curIndex = cellOrder.findIndex(id => id === action.id);

    const nextIndex = curIndex + 1;

    // When at the end, create a new cell
    if (nextIndex >= cellOrder.size) {
      if (!action.createCellIfUndefined) {
        return state;
      }

      const cellID = uuid.v4();
      // TODO: condition on state.defaultCellType (markdown vs. code)
      const cell = commutable.emptyCodeCell;
      return state.set('focusedCell', cellID)
        .update('notebook',
          (notebook) => commutable.insertCellAt(notebook, cell, cellID, nextIndex))
        .setIn(['notebook', 'cellMap', cellID, 'outputHidden'], false)
        .setIn(['notebook', 'cellMap', cellID, 'inputHidden'], false);
    }

    // When in the middle of the notebook document, move to the next cell
    return state.set('focusedCell', cellOrder.get(nextIndex));
  },
  [constants.FOCUS_PREVIOUS_CELL]: function focusPreviousCell(state, action) {
    const cellOrder = state.getIn(['notebook', 'cellOrder']);
    const curIndex = cellOrder.findIndex(id => id === action.id);
    const nextIndex = Math.max(0, curIndex - 1);

    return state.set('focusedCell', cellOrder.get(nextIndex));
  },
  [constants.TOGGLE_STICKY_CELL]: function toggleStickyCell(state, action) {
    const { id } = action;
    // TODO: Switch this structure to an Immutable.Set
    const stickyCells = state.get('stickyCells');
    if (stickyCells.get(id)) {
      return state.set('stickyCells', stickyCells.delete(id));
    }
    return state.setIn(['stickyCells', id], true);
  },
  [constants.UPDATE_CELL_EXECUTION_COUNT]: function updateExecutionCount(state, action) {
    const { id, count } = action;
    return state.update('notebook',
      (notebook) => commutable.updateExecutionCount(notebook, id, count));
  },
  [constants.MOVE_CELL]: function moveCell(state, action) {
    return state.updateIn(['notebook', 'cellOrder'],
      cellOrder => {
        const oldIndex = cellOrder.findIndex(id => id === action.id);
        const newIndex = cellOrder.findIndex(id => id === action.destinationId)
                          + (action.above ? 0 : 1);
        if (oldIndex === newIndex) {
          return cellOrder;
        }
        return cellOrder
          .splice(oldIndex, 1)
          .splice(newIndex - (oldIndex < newIndex ? 1 : 0), 0, action.id);
      }
    );
  },
  [constants.REMOVE_CELL]: function removeCell(state, action) {
    const { id } = action;
    return state.update('notebook',
      (notebook) => commutable.removeCell(notebook, id)
    );
  },
  [constants.NEW_CELL_AFTER]: function newCellAfter(state, action) {
    const { cellType, id, source } = action;
    const cell = cellType === 'markdown' ? commutable.emptyMarkdownCell :
                                           commutable.emptyCodeCell;
    const cellID = uuid.v4();
    return state.update('notebook', (notebook) => {
      const index = notebook.get('cellOrder').indexOf(id) + 1;
      return commutable.insertCellAt(notebook, cell.set('source', source), cellID, index);
    })
      .setIn(['notebook', 'cellMap', cellID, 'outputHidden'], false)
      .setIn(['notebook', 'cellMap', cellID, 'inputHidden'], false);
  },
  [constants.NEW_CELL_BEFORE]: function newCellBefore(state, action) {
    // Draft API
    const { cellType, id } = action;
    const cell = cellType === 'markdown' ? commutable.emptyMarkdownCell :
                                           commutable.emptyCodeCell;
    const cellID = uuid.v4();
    return state.update('notebook', (notebook) => {
      const index = notebook.get('cellOrder').indexOf(id);
      return commutable.insertCellAt(notebook, cell, cellID, index);
    })
      .setIn(['notebook', 'cellMap', cellID, 'outputHidden'], false)
      .setIn(['notebook', 'cellMap', cellID, 'inputHidden'], false);
  },
  [constants.MERGE_CELL_AFTER]: function mergeCellAfter(state, action) {
    const { id } = action;
    const cellOrder = state.getIn(['notebook', 'cellOrder']);
    const index = cellOrder.indexOf(id);
    // do nothing if this is the last cell
    if (cellOrder.size === index + 1) {
      return state;
    }
    const cellMap = state.getIn(['notebook', 'cellMap']);

    const nextId = cellOrder.get(index + 1);
    const source = cellMap.getIn([id, 'source'])
      .concat('\n', '\n', cellMap.getIn([nextId, 'source']));

    return state.update('notebook',
      (notebook) => commutable.removeCell(commutable.updateSource(notebook, id, source), nextId)
    );
  },
  [constants.NEW_CELL_APPEND]: function newCellAppend(state, action) {
    // Draft API
    const { cellType } = action;
    const notebook = state.get('notebook');
    const cell = cellType === 'markdown' ? commutable.emptyMarkdownCell :
                                           commutable.emptyCodeCell;
    const index = notebook.get('cellOrder').count();
    const cellID = uuid.v4();
    return state.set('notebook', commutable.insertCellAt(notebook, cell, cellID, index))
      .setIn(['notebook', 'cellMap', cellID, 'outputHidden'], false)
      .setIn(['notebook', 'cellMap', cellID, 'inputHidden'], false);
  },
  [constants.UPDATE_CELL_SOURCE]: function updateSource(state, action) {
    const { id, source } = action;
    return state.update('notebook', (notebook) => commutable.updateSource(notebook, id, source));
  },
  [constants.CLEAR_CELL_OUTPUT]: function clearCellOutput(state, action) {
    const { id } = action;
    return state.update('notebook', (notebook) => commutable.clearCellOutput(notebook, id));
  },
  [constants.SPLIT_CELL]: function splitCell(state, action) {
    const { id, position } = action;
    const index = state.getIn(['notebook', 'cellOrder']).indexOf(id);
    const updatedState = state.update('notebook',
        (notebook) => commutable.splitCell(notebook, id, position));
    const newCell = updatedState.getIn(['notebook', 'cellOrder', index + 1]);
    return updatedState
      .setIn(['notebook', 'cellMap', newCell, 'outputHidden'], false)
      .setIn(['notebook', 'cellMap', newCell, 'inputHidden'], false);
  },
  [constants.CHANGE_OUTPUT_VISIBILITY]: function changeOutputVisibility(state, action) {
    const { id } = action;
    return state.updateIn(['notebook', 'cellMap'], (cells) => cells.setIn([id, 'outputHidden'],
          !cells.getIn([id, 'outputHidden'])));
  },
  [constants.CHANGE_INPUT_VISIBILITY]: function changeInputVisibility(state, action) {
    const { id } = action;
    return state.updateIn(['notebook', 'cellMap'], (cells) => cells.setIn([id, 'inputHidden'],
          !cells.getIn([id, 'inputHidden'])));
  },
  [constants.UPDATE_CELL_OUTPUTS]: function updateOutputs(state, action) {
    const { id, outputs } = action;
    return state.update('notebook', (notebook) => commutable.updateOutputs(notebook, id, outputs));
  },
  [constants.UPDATE_CELL_PAGERS]: function updateCellPagers(state, action) {
    const { id, pagers } = action;
    return state.setIn(['cellPagers', id], pagers);
  },
  [constants.UPDATE_CELL_STATUS]: function updateCellStatus(state, action) {
    const { id, status } = action;
    return state.setIn(['notebook', 'cellMap', id, 'status'], status);
  },
  [constants.SET_LANGUAGE_INFO]: function setLanguageInfo(state, action) {
    const langInfo = Immutable.fromJS(action.langInfo);
    return state.setIn(['notebook', 'metadata', 'language_info'], langInfo);
  },
  [constants.SET_KERNEL_INFO]: function setKernelSpec(state, action) {
    const { kernelInfo } = action;
    return state
      .setIn(['notebook', 'metadata', 'kernelspec'], Immutable.fromJS({
        name: kernelInfo.name,
        language: kernelInfo.spec.language,
        display_name: kernelInfo.spec.display_name,
      }))
      .setIn(['notebook', 'metadata', 'kernel_info', 'name'], kernelInfo.name);
  },
  [constants.OVERWRITE_METADATA_FIELD]: function overwriteMetadata(state, action) {
    const { field, value } = action;
    return state.setIn(['notebook', 'metadata', field], Immutable.fromJS(value));
  },
  [constants.COPY_CELL]: function copyCell(state, action) {
    const { id } = action;
    const cellMap = state.getIn(['notebook', 'cellMap']);
    const cell = cellMap.get(id);
    return state.set('copied', new Immutable.Map({ id, cell }));
  },
  [constants.CUT_CELL]: function cutCell(state, action) {
    const { id } = action;
    const cellMap = state.getIn(['notebook', 'cellMap']);
    const cell = cellMap.get(id);
    return state
      .set('copied', new Immutable.Map({ id, cell }))
      .update('notebook', (notebook) => commutable.removeCell(notebook, id));
  },
  [constants.PASTE_CELL]: function pasteCell(state) {
    const copiedCell = state.getIn(['copied', 'cell']);
    const copiedId = state.getIn(['copied', 'id']);
    const id = uuid.v4();

    return state.update('notebook', (notebook) =>
        commutable.insertCellAfter(notebook, copiedCell, id, copiedId))
          .setIn(['notebook', 'cellMap', id, 'outputHidden'], false)
          .setIn(['notebook', 'cellMap', id, 'inputHidden'], false);
  },
  [constants.CHANGE_CELL_TYPE]: function changeCellType(state, action) {
    const { id, to } = action;
    const from = state.getIn(['notebook', 'cellMap', id, 'cell_type']);

    if (from === to) {
      return state;
    } else if (from === 'markdown') {
      return state.setIn(['notebook', 'cellMap', id, 'cell_type'], to)
        .setIn(['notebook', 'cellMap', id, 'execution_count'], null)
        .setIn(['notebook', 'cellMap', id, 'outputs'], new Immutable.List());
    }

    return state.setIn(['notebook', 'cellMap', id, 'cell_type'], to)
      .delete(['notebook', 'cellMap', id, 'execution_count'])
      .delete(['notebook', 'cellMap', id, 'outputs']);
  },
}, {});