<NME> notebook.ts
<BEF> // Vendor modules
import * as actionTypes from "@nteract/actions";
import {
  CellId,
  createFrozenMediaBundle,
  createImmutableOutput,
  deleteCell,
  emptyCodeCell,
  emptyMarkdownCell,
  emptyNotebook,
  ImmutableCell,
  ImmutableCodeCell,
  ImmutableNotebook,
  ImmutableOutput,
  insertCellAfter,
  insertCellAt,
  makeCodeCell,
  makeMarkdownCell,
  makeRawCell,
  markCellDeleting,
  markCellNotDeleting,
  OnDiskDisplayData,
  OnDiskExecuteResult,
  OnDiskOutput,
  OnDiskStreamOutput,
} from "@nteract/commutable";
import { UpdateDisplayDataContent } from "@nteract/messaging";
import {
  DocumentRecordProps,
  makeDocumentRecord,
  NotebookModel,
  PayloadMessage,
} from "@nteract/types";
import { escapeCarriageReturnSafe } from "escape-carriage";
import { fromJS, List, Map, RecordOf, Set } from "immutable";
import has from "lodash.has";
import { v4 as uuid } from "uuid";

type KeyPath = List<string | number>;
type KeyPaths = List<KeyPath>;

/**
 * An output can be a stream of data that does not arrive at a single time. This
 * function handles the different types of outputs and accumulates the data
 * into a reduced output.
 *
 * @param {Object} outputs - Kernel output messages
 * @param {Object} output - Outputted to be reduced into list of outputs
 * @return {List<Object>} updated-outputs - Outputs + Output
 */
export function reduceOutputs(
  outputs: List<ImmutableOutput> = List(),
  output: OnDiskOutput
): List<ImmutableOutput> {
  // Find the last output to see if it's a stream type
  // If we don't find one, default to null
  const last = outputs.last(null);

  if (!last || !last.output_type) {
    return outputs.push(createImmutableOutput(output));
  }

  if (output.output_type !== "stream" || last.output_type !== "stream") {
    // If the last output type or the incoming output type isn't a stream
    // we just add it to the outputs
    // This is kind of like a "break" between streams if we get error,
    // display_data, execute_result, etc.
    return outputs.push(createImmutableOutput(output));
  }

  const streamOutput: OnDiskStreamOutput = output;

  if (typeof streamOutput.name === "undefined") {
    return outputs.push(createImmutableOutput(streamOutput));
  }

  function appendText(text: string): string {
    if (typeof streamOutput.text === "string") {
      return escapeCarriageReturnSafe(text + streamOutput.text);
    }
    return text;
  }

  // Invariant: size > 0, outputs.last() exists
  if (last.name === streamOutput.name) {
    return outputs.updateIn([outputs.size - 1, "text"], appendText);
  }

  // Check if there's a separate stream to merge with
  const nextToLast = outputs.butLast().last(null);

  if (
    nextToLast &&
    nextToLast.output_type === "stream" &&
    nextToLast.name === streamOutput.name
  ) {
    return outputs.updateIn([outputs.size - 2, "text"], appendText);
  }
  // If nothing else matched, just append it
  return outputs.push(createImmutableOutput(streamOutput));
}

export function cleanCellTransient(
  state: NotebookModel,
  id: string
): RecordOf<DocumentRecordProps> {
  // Clear out key paths that should no longer be referenced
  return state
    .setIn(["cellPagers", id], List())
    .updateIn(
      ["transient", "keyPathsForDisplays"],
      (kpfd: Map<string, KeyPaths>) =>
        (kpfd || Map()).map((keyPaths: KeyPaths) =>
          keyPaths.filter((keyPath: KeyPath) => keyPath.get(2) !== id)
        )
    )
    .setIn(["transient", "cellMap", id], Map());
}

function setNotebookCheckpoint(
  state: NotebookModel
): RecordOf<DocumentRecordProps> {
  // Use the current version of the notebook document
  return state.set("savedNotebook", state.get("notebook"));
}

function focusCell(
  state: NotebookModel,
  action: actionTypes.FocusCell
): RecordOf<DocumentRecordProps> {
  return state.set("cellFocused", action.payload.id);
}

function clearOutputs(
  state: NotebookModel,
  action: actionTypes.ClearOutputs
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }

  const type = state.getIn(["notebook", "cellMap", id, "cell_type"]);

  const cleanedState = cleanCellTransient(state, id);

  if (type === "code") {
    return cleanedState
      .setIn(["notebook", "cellMap", id, "outputs"], List())
      .setIn(["notebook", "cellMap", id, "execution_count"], null)
      .setIn(["cellPrompts", id], List());
  }
  return cleanedState;
}

function toggleTagInCell(
  state: NotebookModel,
  action: actionTypes.ToggleTagInCell
): RecordOf<DocumentRecordProps> {
  const { id, tag } = action.payload;

  return state.updateIn(
    ["notebook", "cellMap", id, "metadata", "tags"],
    (tags) => {
      if (tags) {
        return tags.has(tag) ? tags.remove(tag) : tags.add(tag);
      } else {
        return Set([tag]);
      }
    }
  );
}

function clearAllOutputs(
  state: NotebookModel,
  action: actionTypes.ClearAllOutputs | actionTypes.RestartKernel
): RecordOf<DocumentRecordProps> {
  // If we get a restart kernel action that said to clear outputs, we'll
  // handle it
  if (
    action.type === actionTypes.RESTART_KERNEL &&
    action.payload.outputHandling !== "Clear All"
  ) {
    return state;
  }

  // For every cell, clear the outputs and execution counts
  const cellMap = state
    .getIn(["notebook", "cellMap"])
    // NOTE: My kingdom for a mergeMap
    .map((cell: ImmutableCell) => {
      if ((cell as any).get("cell_type") === "code") {
        return (cell as ImmutableCodeCell).merge({
          outputs: List(),
          execution_count: null,
        });
      }
      return cell;
    });

  // Clear all the transient data too
  const transient = Map({
    keyPathsForDisplays: Map(),
    cellMap: cellMap.map(() => Map()),
  });

  return state
    .setIn(["notebook", "cellMap"], cellMap)
    .set("transient", transient)
    .set("cellPrompts", Map());
}

type UpdatableOutputContent =
  | OnDiskExecuteResult
  | OnDiskDisplayData
  | UpdateDisplayDataContent;

// Utility function used in two reducers below
function updateAllDisplaysWithID(
  state: NotebookModel,
  content: UpdatableOutputContent
): NotebookModel {
  if (!content || !content.transient || !content.transient.display_id) {
    return state;
  }

  const keyPaths: KeyPaths =
    state.getIn([
      "transient",
      "keyPathsForDisplays",
      content.transient.display_id,
    ]) || List();

  const updateOutput = (output: any) => {
    if (output) {
      // We already have something here, don't change the other fields
      return output.merge({
        data: createFrozenMediaBundle(content.data),
        metadata: fromJS(content.metadata || {}),
      });
    } else if (content.output_type === "update_display_data") {
      // Nothing here and we have no valid output, just create a basic output
      return {
        data: createFrozenMediaBundle(content.data),
        metadata: fromJS(content.metadata || {}),
        output_type: "display_data",
      };
    } else {
      // Nothing here, but we have a valid output
      return createImmutableOutput(content);
    }
  };

  const updateOneDisplay = (currState: NotebookModel, keyPath: KeyPath) =>
    currState.updateIn(keyPath, updateOutput);

  return keyPaths.reduce(updateOneDisplay, state);
}

function appendOutput(
  state: NotebookModel,
  action: actionTypes.AppendOutput
): RecordOf<DocumentRecordProps> {
  const output = action.payload.output;
  const cellId = action.payload.id;

  /**
   * If it is not a display_data or execute_result with
   * a display_id, then treat it as a normal output and don't
   * add its index to the keyPaths.
   */
  if (
    (output.output_type !== "execute_result" &&
      output.output_type !== "display_data") ||
    !has(output, "transient.display_id")
  ) {
    return state.updateIn(
      ["notebook", "cellMap", cellId, "outputs"],
      (outputs: List<ImmutableOutput>): List<ImmutableOutput> =>
        reduceOutputs(outputs, output)
    );
  }

  // We now have a display_data that includes a transient display_id
  // output: {
  //   data: { 'text/html': '<b>woo</b>' }
  //   metadata: {}
  //   transient: { display_id: '12312' }
  // }

  // We now have a display to track
  const displayID = output.transient!.display_id;

  if (!displayID || typeof displayID !== "string") {
    return state;
  }

  // Every time we see a display id we're going to capture the keypath
  // to the output

  // Determine the next output index
  const outputIndex = state
    .getIn(["notebook", "cellMap", cellId, "outputs"])
    .count();

  // Construct the path to the output for updating later
  const keyPath: KeyPath = List([
    "notebook",
    "cellMap",
    cellId,
    "outputs",
    outputIndex,
  ]);

  const keyPaths: KeyPaths = (
    state
      // Extract the current list of keypaths for this displayID
      .getIn(["transient", "keyPathsForDisplays", displayID]) || List()
  )
    // Append our current output's keyPath
    .push(keyPath);

  return updateAllDisplaysWithID(
    state.setIn(["transient", "keyPathsForDisplays", displayID], keyPaths),
    output
  );
}

function updateDisplay(
  state: NotebookModel,
  action: actionTypes.UpdateDisplay
): RecordOf<DocumentRecordProps> {
  return updateAllDisplaysWithID(state, action.payload.content);
}

function focusNextCell(
  state: NotebookModel,
  action: actionTypes.FocusNextCell
): RecordOf<DocumentRecordProps> {
  const cellOrder = state.getIn(["notebook", "cellOrder"]);

  const id = action.payload.id ? action.payload.id : state.get("cellFocused");
  // If for some reason we neither have an ID here or a focused cell, we just
  // keep the state consistent
  if (!id) {
    return state;
  }

  const curIndex = cellOrder.findIndex((foundId: CellId) => id === foundId);
  const curCellType = state.getIn(["notebook", "cellMap", id, "cell_type"]);

  const nextIndex = curIndex + 1;

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

    const cellId: string = uuid();
    const cell = curCellType === "code" ? emptyCodeCell : emptyMarkdownCell;

    const notebook: ImmutableNotebook = state.get("notebook");

    return state
      .set("cellFocused", cellId)
      .set("notebook", insertCellAt(notebook, cell, cellId, nextIndex));
  }

  // When in the middle of the notebook document, move to the next cell
  return state.set("cellFocused", cellOrder.get(nextIndex));
}

function focusPreviousCell(
  state: NotebookModel,
  action: actionTypes.FocusPreviousCell
): RecordOf<DocumentRecordProps> {
  const cellOrder = state.getIn(["notebook", "cellOrder"]);
  const curIndex = cellOrder.findIndex(
    (id: CellId) => id === action.payload.id
  );
  const nextIndex = Math.max(0, curIndex - 1);

  return state.set("cellFocused", cellOrder.get(nextIndex));
}

function focusCellEditor(
  state: NotebookModel,
  action: actionTypes.FocusCellEditor
): RecordOf<DocumentRecordProps> {
  return state.set("editorFocused", action.payload.id);
}

function focusNextCellEditor(
  state: NotebookModel,
  action: actionTypes.FocusNextCellEditor
): RecordOf<DocumentRecordProps> {
  const cellOrder: List<CellId> = state.getIn(["notebook", "cellOrder"]);

  const id = action.payload.id ? action.payload.id : state.get("editorFocused");

  // If for some reason we neither have an ID here or a focused editor, we just
  // keep the state consistent
  if (!id) {
    return state;
  }

  const curIndex = cellOrder.findIndex((foundId: CellId) => id === foundId);
  const nextIndex = curIndex + 1;

  return state.set("editorFocused", cellOrder.get(nextIndex));
}

function focusPreviousCellEditor(
  state: NotebookModel,
  action: actionTypes.FocusPreviousCellEditor
): RecordOf<DocumentRecordProps> {
  const cellOrder: List<CellId> = state.getIn(["notebook", "cellOrder"]);
  const curIndex = cellOrder.findIndex(
    (id: CellId) => id === action.payload.id
  );
  const nextIndex = Math.max(0, curIndex - 1);

  return state.set("editorFocused", cellOrder.get(nextIndex));
}

function moveCell(
  state: NotebookModel,
  action: actionTypes.MoveCell
): RecordOf<DocumentRecordProps> {
  return state.updateIn(
    ["notebook", "cellOrder"],
    (cellOrder: List<CellId>) => {
      const oldIndex = cellOrder.findIndex(
        (id: string) => id === action.payload.id
      );
      const newIndex =
        cellOrder.findIndex(
          (id: string) => id === action.payload.destinationId
        ) + (action.payload.above ? 0 : 1);
      if (oldIndex === newIndex) {
        return cellOrder;
      }
      return cellOrder
        .splice(oldIndex, 1)
        .splice(newIndex - (oldIndex < newIndex ? 1 : 0), 0, action.payload.id);
    }
  );
}

function markCellAsDeleting(
  state: NotebookModel,
  action: actionTypes.MarkCellAsDeleting
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }
  return state.update("notebook", (notebook: ImmutableNotebook) =>
    markCellDeleting(notebook, id)
  );
}

function unmarkCellAsDeleting(
  state: NotebookModel,
  action: actionTypes.UnmarkCellAsDeleting
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }
  return state.update("notebook", (notebook: ImmutableNotebook) =>
    markCellNotDeleting(notebook, id)
  );
}

function deleteCellFromState(
  state: NotebookModel,
  action: actionTypes.DeleteCell
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }
  return cleanCellTransient(
    state.update("notebook", (notebook: ImmutableNotebook) =>
      deleteCell(notebook, id)
    ),
    id
  );
}

function createCellBelow(
  state: NotebookModel,
  action: actionTypes.CreateCellBelow
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }

  const { cellType } = action.payload;
  let cell: ImmutableCell =
    cellType === "markdown" ? emptyMarkdownCell : emptyCodeCell;
  if (action.payload.cell) {
    cell = action.payload.cell;
  }

  const cellId = uuid();
  return state.update("notebook", (notebook: ImmutableNotebook) => {
    const index = notebook.get("cellOrder", List()).indexOf(id) + 1;
    return insertCellAt(notebook, cell, cellId, index);
  });
}

function createCellAbove(
  state: NotebookModel,
  action: actionTypes.CreateCellAbove
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }

  const { cellType } = action.payload;
  let cell: ImmutableCell =
    cellType === "markdown" ? emptyMarkdownCell : emptyCodeCell;
  if (action.payload.cell) {
    cell = action.payload.cell;
  }
  const cellId = uuid();
  return state.update("notebook", (notebook: ImmutableNotebook) => {
    const cellOrder: List<CellId> = notebook.get("cellOrder", List());
    const index = cellOrder.indexOf(id);
    return insertCellAt(notebook, cell, cellId, index);
  });
}

function createCellAppend(
  state: NotebookModel,
  action: actionTypes.CreateCellAppend
): RecordOf<DocumentRecordProps> {
  const { cellType } = action.payload;
  const notebook: ImmutableNotebook = state.get("notebook");
  const cellOrder: List<CellId> = notebook.get("cellOrder", List());
  const cell: ImmutableCell = action.payload.cell ?? (cellType === "markdown" ? emptyMarkdownCell : emptyCodeCell);
  const index = cellOrder.count();
  const cellId = uuid();
  return state.set("notebook", insertCellAt(notebook, cell, cellId, index));
}

function acceptPayloadMessage(
  state: NotebookModel,
  action: actionTypes.AcceptPayloadMessage
): NotebookModel {
  const id: string = action.payload.id;
  const payload: PayloadMessage = action.payload.payload;

  if (payload.source === "page") {
    // append pager
    return state.updateIn(["cellPagers", id], (l) =>
      (l || List()).push(payload.data)
    );
  } else if (payload.source === "set_next_input") {
    if (payload.replace) {
      // this payload is sent in IPython when you use %load
      // and is intended to replace cell source
      return state.setIn(["notebook", "cellMap", id, "source"], payload.text);
    } else {
      // create the next cell
      // FIXME: This is a weird pattern. We're basically faking a dispatch here
      // inside a reducer and then appending to the result. I think that both of
      // these reducers should just handle the original action.
      return createCellBelow(state, {
        type: actionTypes.CREATE_CELL_BELOW,
        payload: {
          cellType: "code",
          cell: emptyCodeCell.setIn(["source"], payload.text || ""),
          id,
          contentRef: action.payload.contentRef,
        },
      });
    }
  }
  // If the payload is unsupported, just return the current state
  return state;
}

function setInCell(
  state: NotebookModel,
  action: actionTypes.SetInCell<string>
): RecordOf<DocumentRecordProps> {
  return state.setIn(
    ["notebook", "cellMap", action.payload.id].concat(action.payload.path),
    action.payload.value
  );
}

function toggleCellOutputVisibility(
  state: NotebookModel,
  action: actionTypes.ToggleCellOutputVisibility
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }

  return state.setIn(
    ["notebook", "cellMap", id, "metadata", "jupyter", "outputs_hidden"],
    !state.getIn([
      "notebook",
      "cellMap",
      id,
      "metadata",
      "jupyter",
      "outputs_hidden",
    ])
  );
}

function unhideAll(
  state: NotebookModel,
  action: actionTypes.UnhideAll
): RecordOf<DocumentRecordProps> {
  const { outputHidden, inputHidden } = action.payload;
  let metadataMixin = Map<string, boolean>();

  if (outputHidden !== undefined) {
    metadataMixin = metadataMixin.set("outputs_hidden", outputHidden);
  }
  if (inputHidden !== undefined) {
    metadataMixin = metadataMixin.set("source_hidden", inputHidden);
  }

  return state.updateIn(["notebook", "cellMap"], (cellMap) =>
    cellMap.map((cell: ImmutableCell) => {
      if ((cell as any).get("cell_type") === "code") {
        return cell.mergeIn(["metadata", "jupyter"], metadataMixin);
      }
      return cell;
    })
  );
}

function toggleCellInputVisibility(
  state: NotebookModel,
  action: actionTypes.ToggleCellInputVisibility
  state: NotebookModel,
  action: actionTypes.UpdateOutputMetadata
) {
  const { id, metadata, index } = action.payload;
  const currentOutputs = state.getIn(["notebook", "cellMap", id, "outputs"]);

  const updatedOutputs = currentOutputs.update(index, (item: any) =>
    item.set("metadata", fromJS(metadata))
  );

  return state.setIn(["notebook", "cellMap", id, "outputs"], updatedOutputs);
  );
}

function updateCellStatus(
  state: NotebookModel,
  action: actionTypes.UpdateCellStatus
): RecordOf<DocumentRecordProps> {
  const { id, status } = action.payload;
  return state.setIn(["transient", "cellMap", id, "status"], status);
}

function updateOutputMetadata(
  state: NotebookModel,
  action: actionTypes.UpdateOutputMetadata
): RecordOf<DocumentRecordProps> {
  const { id, metadata, index, mediaType } = action.payload;
  const currentOutputs = state.getIn(["notebook", "cellMap", id, "outputs"]);

  const updatedOutputs = currentOutputs.update(index, (item: any) =>
    item.set(
      "metadata",
      fromJS({
        [mediaType]: metadata,
      })
    )
  );

  return state.setIn(["notebook", "cellMap", id, "outputs"], updatedOutputs);
}

function setLanguageInfo(
  state: NotebookModel,
  action: actionTypes.SetLanguageInfo
): RecordOf<DocumentRecordProps> {
  const langInfo = fromJS(action.payload.langInfo);
  return state.setIn(["notebook", "metadata", "language_info"], langInfo);
}

function setKernelMetadata(
  state: NotebookModel,
  action: actionTypes.SetKernelMetadata
): RecordOf<DocumentRecordProps> {
  const { kernelInfo } = action.payload;
  if (kernelInfo) {
    return state
      .setIn(
        ["notebook", "metadata", "kernelspec"],
        fromJS({
          name: kernelInfo.name,
          language: kernelInfo.language,
          display_name: kernelInfo.displayName,
        })
      )
      .setIn(["notebook", "metadata", "kernel_info", "name"], kernelInfo.name);
  }
  return state;
}

function overwriteMetadataField(
  state: NotebookModel,
  action: actionTypes.OverwriteMetadataField
): RecordOf<DocumentRecordProps> {
  const { field, value } = action.payload;
  return state.setIn(["notebook", "metadata", field], fromJS(value));
}
function deleteMetadataField(
  state: NotebookModel,
  action: actionTypes.DeleteMetadataField
): RecordOf<DocumentRecordProps> {
  const { field } = action.payload;
  return state.deleteIn(["notebook", "metadata", field]);
}

function copyCell(
  state: NotebookModel,
  action: actionTypes.CopyCell
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id || state.cellFocused;

  const cell = state.getIn(["notebook", "cellMap", id]);
  if (!cell) {
    return state;
  }
  return state.set("copied", cell);
}

function cutCell(
  state: NotebookModel,
  action: actionTypes.CutCell
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }

  const cell = state.getIn(["notebook", "cellMap", id]);

  if (!cell) {
    return state;
  }

  // FIXME: If the cell that was cut was the focused cell, focus the cell below
  return state
    .set("copied", cell)
    .update("notebook", (notebook: ImmutableNotebook) =>
      deleteCell(notebook, id)
    );
}

function pasteCell(state: NotebookModel): RecordOf<DocumentRecordProps> {
  const copiedCell = state.get("copied");

  const pasteAfter = state.cellFocused;

  if (!copiedCell || !pasteAfter) {
    return state;
  }

  // Create a new cell with `id` that will come after the currently focused cell
  // using the contents of the originally copied cell
  const id = uuid();

  return state.update("notebook", (notebook: ImmutableNotebook) =>
    insertCellAfter(notebook, copiedCell, id, pasteAfter)
  );
}

function changeCellType(
  state: NotebookModel,
  action: actionTypes.ChangeCellType
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }

  const { to } = action.payload;

  const cell = state.getIn(["notebook", "cellMap", id]);

  const from = cell.cell_type;

  // NOOP, since we're already that cell type
  if (from === to) {
    return state;
  }

  let nextState = state;

  // from === "code"
  if (from === "code") {
    nextState = cleanCellTransient(
      state
        .deleteIn(["notebook", "cellMap", id, "execution_count"])
        .deleteIn(["notebook", "cellMap", id, "outputs"]),
      id
    );
  }

  switch (to) {
    case "code":
      return nextState.setIn(
        ["notebook", "cellMap", id],
        makeCodeCell({
          source: cell.source,
        })
      );
    case "markdown":
      return nextState.setIn(
        ["notebook", "cellMap", id],
        makeMarkdownCell({
          source: cell.source,
        })
      );
    case "raw":
      return nextState.setIn(
        ["notebook", "cellMap", id],
        makeRawCell({
          source: cell.source,
        })
      );
  }

  // If we didn't match on the `to`, we should change nothing as we don't implement
  // other cell types (as there aren't any)
  return state;
}

function toggleOutputExpansion(
  state: NotebookModel,
  action: actionTypes.ToggleCellExpansion
): RecordOf<DocumentRecordProps> {
  const id = action.payload.id ? action.payload.id : state.cellFocused;
  if (!id) {
    return state;
  }

  return state.updateIn(
    ["notebook", "cellMap"],
    (cells: Map<CellId, ImmutableCell>) => {
      const scrolled = cells.getIn([id, "metadata", "scrolled"]);
      const isCurrentlyScrolled = scrolled !== false;
      return cells.setIn(
        [id, "metadata", "scrolled"],
        !isCurrentlyScrolled
      )
    }
  );
}

function promptInputRequest(
  state: NotebookModel,
  action: actionTypes.PromptInputRequest
): RecordOf<DocumentRecordProps> {
  const { id, password, prompt } = action.payload;
  return state.updateIn(["cellPrompts", id], (prompts) =>
    prompts.push({
      prompt,
      password,
    })
  );
}

function interruptKernelSuccessful(
  state: NotebookModel,
  action: actionTypes.InterruptKernelSuccessful
): RecordOf<DocumentRecordProps> {
  return state.updateIn(["transient", "cellMap"], (cells) =>
    cells.map((cell: Map<string, string>) => {
      if (cell.get("status") === "queued" || cell.get("status") === "running") {
        return cell.set("status", "");
      }
      return cell;
    })
  );
}

type DocumentAction =
  | actionTypes.ToggleTagInCell
  | actionTypes.FocusPreviousCellEditor
  | actionTypes.FocusPreviousCell
  | actionTypes.FocusNextCellEditor
  | actionTypes.FocusNextCell
  | actionTypes.FocusCellEditor
  | actionTypes.FocusCell
  | actionTypes.ClearOutputs
  | actionTypes.AppendOutput
  | actionTypes.UpdateDisplay
  | actionTypes.MoveCell
  | actionTypes.MarkCellAsDeleting
  | actionTypes.UnmarkCellAsDeleting
  | actionTypes.DeleteCell
  | actionTypes.CreateCellBelow
  | actionTypes.CreateCellAbove
  | actionTypes.CreateCellAppend
  | actionTypes.ToggleCellOutputVisibility
  | actionTypes.ToggleCellInputVisibility
  | actionTypes.UpdateCellStatus
  | actionTypes.UpdateOutputMetadata
  | actionTypes.SetLanguageInfo
  | actionTypes.SetKernelMetadata
  | actionTypes.OverwriteMetadataField
  | actionTypes.DeleteMetadataField
  | actionTypes.CopyCell
  | actionTypes.CutCell
  | actionTypes.PasteCell
  | actionTypes.ChangeCellType
  | actionTypes.ToggleCellExpansion
  | actionTypes.AcceptPayloadMessage
  | actionTypes.SendExecuteRequest
  | actionTypes.SaveFulfilled
  | actionTypes.RestartKernel
  | actionTypes.ClearAllOutputs
  | actionTypes.SetInCell<any>
  | actionTypes.UnhideAll
  | actionTypes.PromptInputRequest
  | actionTypes.InterruptKernelSuccessful;

const defaultDocument: NotebookModel = makeDocumentRecord({
  notebook: emptyNotebook,
});

export function notebook(
  state: NotebookModel = defaultDocument,
  action: DocumentAction
): RecordOf<DocumentRecordProps> {
  switch (action.type) {
    case actionTypes.TOGGLE_TAG_IN_CELL:
      return toggleTagInCell(state, action);
    case actionTypes.SAVE_FULFILLED:
      return setNotebookCheckpoint(state);
    case actionTypes.FOCUS_CELL:
      return focusCell(state, action);
    case actionTypes.CLEAR_OUTPUTS:
      return clearOutputs(state, action);
    case actionTypes.CLEAR_ALL_OUTPUTS:
    case actionTypes.RESTART_KERNEL:
      return clearAllOutputs(state, action);
    case actionTypes.APPEND_OUTPUT:
      return appendOutput(state, action);
    case actionTypes.UPDATE_DISPLAY:
      return updateDisplay(state, action);
    case actionTypes.FOCUS_NEXT_CELL:
      return focusNextCell(state, action);
    case actionTypes.FOCUS_PREVIOUS_CELL:
      return focusPreviousCell(state, action);
    case actionTypes.FOCUS_CELL_EDITOR:
      return focusCellEditor(state, action);
    case actionTypes.FOCUS_NEXT_CELL_EDITOR:
      return focusNextCellEditor(state, action);
    case actionTypes.FOCUS_PREVIOUS_CELL_EDITOR:
      return focusPreviousCellEditor(state, action);
    case actionTypes.SET_IN_CELL:
      return setInCell(state, action);
    case actionTypes.MOVE_CELL:
      return moveCell(state, action);
    case actionTypes.MARK_CELL_AS_DELETING:
      return markCellAsDeleting(state, action);
    case actionTypes.UNMARK_CELL_AS_DELETING:
      return unmarkCellAsDeleting(state, action);
    case actionTypes.DELETE_CELL:
      return deleteCellFromState(state, action);
    case actionTypes.CREATE_CELL_BELOW:
      return createCellBelow(state, action);
    case actionTypes.CREATE_CELL_ABOVE:
      return createCellAbove(state, action);
    case actionTypes.CREATE_CELL_APPEND:
      return createCellAppend(state, action);
    case actionTypes.TOGGLE_CELL_OUTPUT_VISIBILITY:
      return toggleCellOutputVisibility(state, action);
    case actionTypes.TOGGLE_CELL_INPUT_VISIBILITY:
      return toggleCellInputVisibility(state, action);
    case actionTypes.ACCEPT_PAYLOAD_MESSAGE:
      return acceptPayloadMessage(state, action);
    case actionTypes.UPDATE_CELL_STATUS:
      return updateCellStatus(state, action);
    case actionTypes.UPDATE_OUTPUT_METADATA:
      return updateOutputMetadata(state, action);
    case actionTypes.SET_LANGUAGE_INFO:
      return setLanguageInfo(state, action);
    case actionTypes.SET_KERNEL_METADATA:
      return setKernelMetadata(state, action);
    case actionTypes.OVERWRITE_METADATA_FIELD:
      return overwriteMetadataField(state, action);
    case actionTypes.DELETE_METADATA_FIELD:
      return deleteMetadataField(state, action);
    case actionTypes.COPY_CELL:
      return copyCell(state, action);
    case actionTypes.CUT_CELL:
      return cutCell(state, action);
    case actionTypes.PASTE_CELL:
      return pasteCell(state);
    case actionTypes.CHANGE_CELL_TYPE:
      return changeCellType(state, action);
    case actionTypes.TOGGLE_OUTPUT_EXPANSION:
      return toggleOutputExpansion(state, action);
    case actionTypes.UNHIDE_ALL:
      return unhideAll(state, action);
    case actionTypes.PROMPT_INPUT_REQUEST:
      return promptInputRequest(state, action);
    case actionTypes.INTERRUPT_KERNEL_SUCCESSFUL:
      return interruptKernelSuccessful(state, action);
    default:
      return state;
  }
}

<MSG> Store and retrieve metadata based on mimetype

<DFF> @@ -648,11 +648,16 @@ function updateOutputMetadata(
   state: NotebookModel,
   action: actionTypes.UpdateOutputMetadata
 ) {
-  const { id, metadata, index } = action.payload;
+  const { id, metadata, index, mediaType } = action.payload;
   const currentOutputs = state.getIn(["notebook", "cellMap", id, "outputs"]);
 
   const updatedOutputs = currentOutputs.update(index, (item: any) =>
-    item.set("metadata", fromJS(metadata))
+    item.set(
+      "metadata",
+      fromJS({
+        [mediaType]: metadata
+      })
+    )
   );
 
   return state.setIn(["notebook", "cellMap", id, "outputs"], updatedOutputs);

{'commit': 'diff --git a/packages/data-explorer/src/index.tsx b/packages/data-explorer/src/index.tsx\nindex 271638285..d48a95ed3 100644\n--- a/packages/data-explorer/src/index.tsx\n+++ b/packages/data-explorer/src/index.tsx\n@@ -48,7 +48,7 @@ interface Props {\n   height?: number;\n   mediaType: "application/vnd.dataresource+json";\n   initialView: View;\n-  onMetadataChange?: ({ dx }: { dx: dxMetaProps }) => void;\n+  onMetadataChange?: ({ dx }: { dx: dxMetaProps }, mediaType: string) => void;\n }\n \n interface State {\n@@ -392,22 +392,25 @@ class DataExplorer extends React.PureComponent<Partial<Props>, State> {\n     // If you pass an onMetadataChange function, then fire it and pass the updated dx settings so someone upstream can update the metadata or otherwise use it\n \n     if (onMetadataChange) {\n-      onMetadataChange({\n-        ...this.props.metadata,\n-        dx: {\n-          view,\n-          lineType,\n-          areaType,\n-          selectedDimensions,\n-          selectedMetrics,\n-          pieceType,\n-          summaryType,\n-          networkType,\n-          hierarchyType,\n-          colors,\n-          chart\n-        }\n-      });\n+      onMetadataChange(\n+        {\n+          ...this.props.metadata,\n+          dx: {\n+            view,\n+            lineType,\n+            areaType,\n+            selectedDimensions,\n+            selectedMetrics,\n+            pieceType,\n+            summaryType,\n+            networkType,\n+            hierarchyType,\n+            colors,\n+            chart\n+          }\n+        },\n+        mediaType\n+      );\n     }\n \n     this.setState(\ndiff --git a/packages/notebook-app-component/src/transform-media.tsx b/packages/notebook-app-component/src/transform-media.tsx\nindex 973b7881a..da6dc1a6d 100644\n--- a/packages/notebook-app-component/src/transform-media.tsx\n+++ b/packages/notebook-app-component/src/transform-media.tsx\n@@ -12,6 +12,9 @@ import {\n import { actions, selectors } from "@nteract/core";\n import { AppState, ContentRef } from "@nteract/types";\n \n+import memoizeOne from "memoize-one";\n+import { get } from "https";\n+\n interface OwnProps {\n   output_type: string;\n   cellId: string;\n@@ -27,25 +30,19 @@ interface MappedProps {\n \n interface DispatchProps {\n   mediaActions: {\n-    onMetadataChange: (metadata: JSONObject) => void;\n+    onMetadataChange: (metadata: JSONObject, mediaType: string) => void;\n   };\n }\n \n const PureTransformMedia = (props: MappedProps & DispatchProps) => {\n-  const { Media, mediaActions, mediaType, output } = props;\n+  const { Media, mediaActions, mediaType, data, metadata } = props;\n \n   // If we had no valid result, return an empty output\n-  if (!mediaType || !output) {\n+  if (!mediaType || !data) {\n     return null;\n   }\n \n-  return (\n-    <Media\n-      {...mediaActions}\n-      data={output.data[mediaType]}\n-      metadata={output.metadata.get(mediaType) || output.metadata.toJS()}\n-    />\n-  );\n+  return <Media {...mediaActions} data={data} metadata={metadata} />;\n };\n \n const richestMediaType = (\n@@ -72,6 +69,10 @@ const makeMapStateToProps = (\n ) => {\n   const { contentRef, index, cellId } = initialProps;\n \n+  const memoizedMetadata = memoizeOne(immutableMetadata =>\n+    immutableMetadata ? immutableMetadata.toJS() : {}\n+  );\n+\n   const mapStateToProps = (state: AppState): MappedProps => {\n     const output: ImmutableOutput = state.core.entities.contents.byRef.getIn(\n       [contentRef, "model", "notebook", "cellMap", cellId, "outputs", index],\n@@ -98,12 +99,17 @@ const makeMapStateToProps = (\n     const order = selectors.displayOrder(state);\n \n     const mediaType = richestMediaType(output, order, handlers);\n+\n+    const metadata = memoizedMetadata(output.metadata.get(mediaType));\n+    const data = output.data[mediaType];\n+\n     if (mediaType) {\n       const Media = selectors.transform(state, { id: mediaType });\n       return {\n         Media,\n         mediaType,\n-        output\n+        data,\n+        metadata\n       };\n     }\n     return {\n@@ -123,13 +129,14 @@ const makeMapDispatchToProps = (\n   const mapDispatchToProps = (dispatch: Dispatch) => {\n     return {\n       mediaActions: {\n-        onMetadataChange: (metadata: JSONObject) => {\n+        onMetadataChange: (metadata: JSONObject, mediaType: string) => {\n           dispatch(\n             actions.updateOutputMetadata({\n               id: cellId,\n               contentRef,\n               metadata,\n-              index\n+              index,\n+              mediaType\n             })\n           );\n         }\ndiff --git a/packages/reducers/src/core/entities/contents/notebook.ts b/packages/reducers/src/core/entities/contents/notebook.ts\nindex 8725753ab..3fbb2a7cc 100644\n--- a/packages/reducers/src/core/entities/contents/notebook.ts\n+++ b/packages/reducers/src/core/entities/contents/notebook.ts\n@@ -648,11 +648,16 @@ function updateOutputMetadata(\n   state: NotebookModel,\n   action: actionTypes.UpdateOutputMetadata\n ) {\n-  const { id, metadata, index } = action.payload;\n+  const { id, metadata, index, mediaType } = action.payload;\n   const currentOutputs = state.getIn(["notebook", "cellMap", id, "outputs"]);\n \n   const updatedOutputs = currentOutputs.update(index, (item: any) =>\n-    item.set("metadata", fromJS(metadata))\n+    item.set(\n+      "metadata",\n+      fromJS({\n+        [mediaType]: metadata\n+      })\n+    )\n   );\n \n   return state.setIn(["notebook", "cellMap", id, "outputs"], updatedOutputs);\n', 'commit_subject': 'Store and retrieve metadata based on mimetype', 'repo_name': 'nteract/nteract', 'committer': {'name': 'Elijah Meeks', 'email': 'a63c273dcb55572d928b89961122ad3a684ba912@gmail.com', 'time_sec': '1549411408', 'tz_offset': '-480', 'date': {'seconds': '1549411408'}}, 'commit_tree': 'a7793ddd83bd9d58a68d80b5ff54225f70c07aca', 'commit_message': 'Store and retrieve metadata based on mimetype\n', 'language_name': 'TypeScript', 'license': 'bsd-3-clause', 'hunks': '@@ -648,11 +648,16 @@ function updateOutputMetadata(\n   state: NotebookModel,\n   action: actionTypes.UpdateOutputMetadata\n ) {\n-  const { id, metadata, index } = action.payload;\n+  const { id, metadata, index, mediaType } = action.payload;\n   const currentOutputs = state.getIn(["notebook", "cellMap", id, "outputs"]);\n \n   const updatedOutputs = currentOutputs.update(index, (item: any) =>\n-    item.set("metadata", fromJS(metadata))\n+    item.set(\n+      "metadata",\n+      fromJS({\n+        [mediaType]: metadata\n+      })\n+    )\n   );\n \n   return state.setIn(["notebook", "cellMap", id, "outputs"], updatedOutputs);\n', 'addition_count': 7, 'deletion_count': 2, 'src_file': 'a/packages/reducers/src/core/entities/contents/notebook.ts', 'tgt_file': 'b/packages/reducers/src/core/entities/contents/notebook.ts', 'file_extension': '.ts', 'before_file': '// Vendor modules\nimport * as actionTypes from "@nteract/actions";\nimport {\n  CellId,\n  createFrozenMediaBundle,\n  createImmutableOutput,\n  deleteCell,\n  emptyCodeCell,\n  emptyMarkdownCell,\n  emptyNotebook,\n  ImmutableCell,\n  ImmutableCodeCell,\n  ImmutableNotebook,\n  ImmutableOutput,\n  insertCellAfter,\n  insertCellAt,\n  makeCodeCell,\n  makeMarkdownCell,\n  makeRawCell,\n  markCellDeleting,\n  markCellNotDeleting,\n  OnDiskDisplayData,\n  OnDiskExecuteResult,\n  OnDiskOutput,\n  OnDiskStreamOutput,\n} from "@nteract/commutable";\nimport { UpdateDisplayDataContent } from "@nteract/messaging";\nimport {\n  DocumentRecordProps,\n  makeDocumentRecord,\n  NotebookModel,\n  PayloadMessage,\n} from "@nteract/types";\nimport { escapeCarriageReturnSafe } from "escape-carriage";\nimport { fromJS, List, Map, RecordOf, Set } from "immutable";\nimport has from "lodash.has";\nimport { v4 as uuid } from "uuid";\n\ntype KeyPath = List<string | number>;\ntype KeyPaths = List<KeyPath>;\n\n/**\n * An output can be a stream of data that does not arrive at a single time. This\n * function handles the different types of outputs and accumulates the data\n * into a reduced output.\n *\n * @param {Object} outputs - Kernel output messages\n * @param {Object} output - Outputted to be reduced into list of outputs\n * @return {List<Object>} updated-outputs - Outputs + Output\n */\nexport function reduceOutputs(\n  outputs: List<ImmutableOutput> = List(),\n  output: OnDiskOutput\n): List<ImmutableOutput> {\n  // Find the last output to see if it\'s a stream type\n  // If we don\'t find one, default to null\n  const last = outputs.last(null);\n\n  if (!last || !last.output_type) {\n    return outputs.push(createImmutableOutput(output));\n  }\n\n  if (output.output_type !== "stream" || last.output_type !== "stream") {\n    // If the last output type or the incoming output type isn\'t a stream\n    // we just add it to the outputs\n    // This is kind of like a "break" between streams if we get error,\n    // display_data, execute_result, etc.\n    return outputs.push(createImmutableOutput(output));\n  }\n\n  const streamOutput: OnDiskStreamOutput = output;\n\n  if (typeof streamOutput.name === "undefined") {\n    return outputs.push(createImmutableOutput(streamOutput));\n  }\n\n  function appendText(text: string): string {\n    if (typeof streamOutput.text === "string") {\n      return escapeCarriageReturnSafe(text + streamOutput.text);\n    }\n    return text;\n  }\n\n  // Invariant: size > 0, outputs.last() exists\n  if (last.name === streamOutput.name) {\n    return outputs.updateIn([outputs.size - 1, "text"], appendText);\n  }\n\n  // Check if there\'s a separate stream to merge with\n  const nextToLast = outputs.butLast().last(null);\n\n  if (\n    nextToLast &&\n    nextToLast.output_type === "stream" &&\n    nextToLast.name === streamOutput.name\n  ) {\n    return outputs.updateIn([outputs.size - 2, "text"], appendText);\n  }\n  // If nothing else matched, just append it\n  return outputs.push(createImmutableOutput(streamOutput));\n}\n\nexport function cleanCellTransient(\n  state: NotebookModel,\n  id: string\n): RecordOf<DocumentRecordProps> {\n  // Clear out key paths that should no longer be referenced\n  return state\n    .setIn(["cellPagers", id], List())\n    .updateIn(\n      ["transient", "keyPathsForDisplays"],\n      (kpfd: Map<string, KeyPaths>) =>\n        (kpfd || Map()).map((keyPaths: KeyPaths) =>\n          keyPaths.filter((keyPath: KeyPath) => keyPath.get(2) !== id)\n        )\n    )\n    .setIn(["transient", "cellMap", id], Map());\n}\n\nfunction setNotebookCheckpoint(\n  state: NotebookModel\n): RecordOf<DocumentRecordProps> {\n  // Use the current version of the notebook document\n  return state.set("savedNotebook", state.get("notebook"));\n}\n\nfunction focusCell(\n  state: NotebookModel,\n  action: actionTypes.FocusCell\n): RecordOf<DocumentRecordProps> {\n  return state.set("cellFocused", action.payload.id);\n}\n\nfunction clearOutputs(\n  state: NotebookModel,\n  action: actionTypes.ClearOutputs\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n\n  const type = state.getIn(["notebook", "cellMap", id, "cell_type"]);\n\n  const cleanedState = cleanCellTransient(state, id);\n\n  if (type === "code") {\n    return cleanedState\n      .setIn(["notebook", "cellMap", id, "outputs"], List())\n      .setIn(["notebook", "cellMap", id, "execution_count"], null)\n      .setIn(["cellPrompts", id], List());\n  }\n  return cleanedState;\n}\n\nfunction toggleTagInCell(\n  state: NotebookModel,\n  action: actionTypes.ToggleTagInCell\n): RecordOf<DocumentRecordProps> {\n  const { id, tag } = action.payload;\n\n  return state.updateIn(\n    ["notebook", "cellMap", id, "metadata", "tags"],\n    (tags) => {\n      if (tags) {\n        return tags.has(tag) ? tags.remove(tag) : tags.add(tag);\n      } else {\n        return Set([tag]);\n      }\n    }\n  );\n}\n\nfunction clearAllOutputs(\n  state: NotebookModel,\n  action: actionTypes.ClearAllOutputs | actionTypes.RestartKernel\n): RecordOf<DocumentRecordProps> {\n  // If we get a restart kernel action that said to clear outputs, we\'ll\n  // handle it\n  if (\n    action.type === actionTypes.RESTART_KERNEL &&\n    action.payload.outputHandling !== "Clear All"\n  ) {\n    return state;\n  }\n\n  // For every cell, clear the outputs and execution counts\n  const cellMap = state\n    .getIn(["notebook", "cellMap"])\n    // NOTE: My kingdom for a mergeMap\n    .map((cell: ImmutableCell) => {\n      if ((cell as any).get("cell_type") === "code") {\n        return (cell as ImmutableCodeCell).merge({\n          outputs: List(),\n          execution_count: null,\n        });\n      }\n      return cell;\n    });\n\n  // Clear all the transient data too\n  const transient = Map({\n    keyPathsForDisplays: Map(),\n    cellMap: cellMap.map(() => Map()),\n  });\n\n  return state\n    .setIn(["notebook", "cellMap"], cellMap)\n    .set("transient", transient)\n    .set("cellPrompts", Map());\n}\n\ntype UpdatableOutputContent =\n  | OnDiskExecuteResult\n  | OnDiskDisplayData\n  | UpdateDisplayDataContent;\n\n// Utility function used in two reducers below\nfunction updateAllDisplaysWithID(\n  state: NotebookModel,\n  content: UpdatableOutputContent\n): NotebookModel {\n  if (!content || !content.transient || !content.transient.display_id) {\n    return state;\n  }\n\n  const keyPaths: KeyPaths =\n    state.getIn([\n      "transient",\n      "keyPathsForDisplays",\n      content.transient.display_id,\n    ]) || List();\n\n  const updateOutput = (output: any) => {\n    if (output) {\n      // We already have something here, don\'t change the other fields\n      return output.merge({\n        data: createFrozenMediaBundle(content.data),\n        metadata: fromJS(content.metadata || {}),\n      });\n    } else if (content.output_type === "update_display_data") {\n      // Nothing here and we have no valid output, just create a basic output\n      return {\n        data: createFrozenMediaBundle(content.data),\n        metadata: fromJS(content.metadata || {}),\n        output_type: "display_data",\n      };\n    } else {\n      // Nothing here, but we have a valid output\n      return createImmutableOutput(content);\n    }\n  };\n\n  const updateOneDisplay = (currState: NotebookModel, keyPath: KeyPath) =>\n    currState.updateIn(keyPath, updateOutput);\n\n  return keyPaths.reduce(updateOneDisplay, state);\n}\n\nfunction appendOutput(\n  state: NotebookModel,\n  action: actionTypes.AppendOutput\n): RecordOf<DocumentRecordProps> {\n  const output = action.payload.output;\n  const cellId = action.payload.id;\n\n  /**\n   * If it is not a display_data or execute_result with\n   * a display_id, then treat it as a normal output and don\'t\n   * add its index to the keyPaths.\n   */\n  if (\n    (output.output_type !== "execute_result" &&\n      output.output_type !== "display_data") ||\n    !has(output, "transient.display_id")\n  ) {\n    return state.updateIn(\n      ["notebook", "cellMap", cellId, "outputs"],\n      (outputs: List<ImmutableOutput>): List<ImmutableOutput> =>\n        reduceOutputs(outputs, output)\n    );\n  }\n\n  // We now have a display_data that includes a transient display_id\n  // output: {\n  //   data: { \'text/html\': \'<b>woo</b>\' }\n  //   metadata: {}\n  //   transient: { display_id: \'12312\' }\n  // }\n\n  // We now have a display to track\n  const displayID = output.transient!.display_id;\n\n  if (!displayID || typeof displayID !== "string") {\n    return state;\n  }\n\n  // Every time we see a display id we\'re going to capture the keypath\n  // to the output\n\n  // Determine the next output index\n  const outputIndex = state\n    .getIn(["notebook", "cellMap", cellId, "outputs"])\n    .count();\n\n  // Construct the path to the output for updating later\n  const keyPath: KeyPath = List([\n    "notebook",\n    "cellMap",\n    cellId,\n    "outputs",\n    outputIndex,\n  ]);\n\n  const keyPaths: KeyPaths = (\n    state\n      // Extract the current list of keypaths for this displayID\n      .getIn(["transient", "keyPathsForDisplays", displayID]) || List()\n  )\n    // Append our current output\'s keyPath\n    .push(keyPath);\n\n  return updateAllDisplaysWithID(\n    state.setIn(["transient", "keyPathsForDisplays", displayID], keyPaths),\n    output\n  );\n}\n\nfunction updateDisplay(\n  state: NotebookModel,\n  action: actionTypes.UpdateDisplay\n): RecordOf<DocumentRecordProps> {\n  return updateAllDisplaysWithID(state, action.payload.content);\n}\n\nfunction focusNextCell(\n  state: NotebookModel,\n  action: actionTypes.FocusNextCell\n): RecordOf<DocumentRecordProps> {\n  const cellOrder = state.getIn(["notebook", "cellOrder"]);\n\n  const id = action.payload.id ? action.payload.id : state.get("cellFocused");\n  // If for some reason we neither have an ID here or a focused cell, we just\n  // keep the state consistent\n  if (!id) {\n    return state;\n  }\n\n  const curIndex = cellOrder.findIndex((foundId: CellId) => id === foundId);\n  const curCellType = state.getIn(["notebook", "cellMap", id, "cell_type"]);\n\n  const nextIndex = curIndex + 1;\n\n  // When at the end, create a new cell\n  if (nextIndex >= cellOrder.size) {\n    if (!action.payload.createCellIfUndefined) {\n      return state;\n    }\n\n    const cellId: string = uuid();\n    const cell = curCellType === "code" ? emptyCodeCell : emptyMarkdownCell;\n\n    const notebook: ImmutableNotebook = state.get("notebook");\n\n    return state\n      .set("cellFocused", cellId)\n      .set("notebook", insertCellAt(notebook, cell, cellId, nextIndex));\n  }\n\n  // When in the middle of the notebook document, move to the next cell\n  return state.set("cellFocused", cellOrder.get(nextIndex));\n}\n\nfunction focusPreviousCell(\n  state: NotebookModel,\n  action: actionTypes.FocusPreviousCell\n): RecordOf<DocumentRecordProps> {\n  const cellOrder = state.getIn(["notebook", "cellOrder"]);\n  const curIndex = cellOrder.findIndex(\n    (id: CellId) => id === action.payload.id\n  );\n  const nextIndex = Math.max(0, curIndex - 1);\n\n  return state.set("cellFocused", cellOrder.get(nextIndex));\n}\n\nfunction focusCellEditor(\n  state: NotebookModel,\n  action: actionTypes.FocusCellEditor\n): RecordOf<DocumentRecordProps> {\n  return state.set("editorFocused", action.payload.id);\n}\n\nfunction focusNextCellEditor(\n  state: NotebookModel,\n  action: actionTypes.FocusNextCellEditor\n): RecordOf<DocumentRecordProps> {\n  const cellOrder: List<CellId> = state.getIn(["notebook", "cellOrder"]);\n\n  const id = action.payload.id ? action.payload.id : state.get("editorFocused");\n\n  // If for some reason we neither have an ID here or a focused editor, we just\n  // keep the state consistent\n  if (!id) {\n    return state;\n  }\n\n  const curIndex = cellOrder.findIndex((foundId: CellId) => id === foundId);\n  const nextIndex = curIndex + 1;\n\n  return state.set("editorFocused", cellOrder.get(nextIndex));\n}\n\nfunction focusPreviousCellEditor(\n  state: NotebookModel,\n  action: actionTypes.FocusPreviousCellEditor\n): RecordOf<DocumentRecordProps> {\n  const cellOrder: List<CellId> = state.getIn(["notebook", "cellOrder"]);\n  const curIndex = cellOrder.findIndex(\n    (id: CellId) => id === action.payload.id\n  );\n  const nextIndex = Math.max(0, curIndex - 1);\n\n  return state.set("editorFocused", cellOrder.get(nextIndex));\n}\n\nfunction moveCell(\n  state: NotebookModel,\n  action: actionTypes.MoveCell\n): RecordOf<DocumentRecordProps> {\n  return state.updateIn(\n    ["notebook", "cellOrder"],\n    (cellOrder: List<CellId>) => {\n      const oldIndex = cellOrder.findIndex(\n        (id: string) => id === action.payload.id\n      );\n      const newIndex =\n        cellOrder.findIndex(\n          (id: string) => id === action.payload.destinationId\n        ) + (action.payload.above ? 0 : 1);\n      if (oldIndex === newIndex) {\n        return cellOrder;\n      }\n      return cellOrder\n        .splice(oldIndex, 1)\n        .splice(newIndex - (oldIndex < newIndex ? 1 : 0), 0, action.payload.id);\n    }\n  );\n}\n\nfunction markCellAsDeleting(\n  state: NotebookModel,\n  action: actionTypes.MarkCellAsDeleting\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n  return state.update("notebook", (notebook: ImmutableNotebook) =>\n    markCellDeleting(notebook, id)\n  );\n}\n\nfunction unmarkCellAsDeleting(\n  state: NotebookModel,\n  action: actionTypes.UnmarkCellAsDeleting\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n  return state.update("notebook", (notebook: ImmutableNotebook) =>\n    markCellNotDeleting(notebook, id)\n  );\n}\n\nfunction deleteCellFromState(\n  state: NotebookModel,\n  action: actionTypes.DeleteCell\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n  return cleanCellTransient(\n    state.update("notebook", (notebook: ImmutableNotebook) =>\n      deleteCell(notebook, id)\n    ),\n    id\n  );\n}\n\nfunction createCellBelow(\n  state: NotebookModel,\n  action: actionTypes.CreateCellBelow\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n\n  const { cellType } = action.payload;\n  let cell: ImmutableCell =\n    cellType === "markdown" ? emptyMarkdownCell : emptyCodeCell;\n  if (action.payload.cell) {\n    cell = action.payload.cell;\n  }\n\n  const cellId = uuid();\n  return state.update("notebook", (notebook: ImmutableNotebook) => {\n    const index = notebook.get("cellOrder", List()).indexOf(id) + 1;\n    return insertCellAt(notebook, cell, cellId, index);\n  });\n}\n\nfunction createCellAbove(\n  state: NotebookModel,\n  action: actionTypes.CreateCellAbove\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n\n  const { cellType } = action.payload;\n  let cell: ImmutableCell =\n    cellType === "markdown" ? emptyMarkdownCell : emptyCodeCell;\n  if (action.payload.cell) {\n    cell = action.payload.cell;\n  }\n  const cellId = uuid();\n  return state.update("notebook", (notebook: ImmutableNotebook) => {\n    const cellOrder: List<CellId> = notebook.get("cellOrder", List());\n    const index = cellOrder.indexOf(id);\n    return insertCellAt(notebook, cell, cellId, index);\n  });\n}\n\nfunction createCellAppend(\n  state: NotebookModel,\n  action: actionTypes.CreateCellAppend\n): RecordOf<DocumentRecordProps> {\n  const { cellType } = action.payload;\n  const notebook: ImmutableNotebook = state.get("notebook");\n  const cellOrder: List<CellId> = notebook.get("cellOrder", List());\n  const cell: ImmutableCell = action.payload.cell ?? (cellType === "markdown" ? emptyMarkdownCell : emptyCodeCell);\n  const index = cellOrder.count();\n  const cellId = uuid();\n  return state.set("notebook", insertCellAt(notebook, cell, cellId, index));\n}\n\nfunction acceptPayloadMessage(\n  state: NotebookModel,\n  action: actionTypes.AcceptPayloadMessage\n): NotebookModel {\n  const id: string = action.payload.id;\n  const payload: PayloadMessage = action.payload.payload;\n\n  if (payload.source === "page") {\n    // append pager\n    return state.updateIn(["cellPagers", id], (l) =>\n      (l || List()).push(payload.data)\n    );\n  } else if (payload.source === "set_next_input") {\n    if (payload.replace) {\n      // this payload is sent in IPython when you use %load\n      // and is intended to replace cell source\n      return state.setIn(["notebook", "cellMap", id, "source"], payload.text);\n    } else {\n      // create the next cell\n      // FIXME: This is a weird pattern. We\'re basically faking a dispatch here\n      // inside a reducer and then appending to the result. I think that both of\n      // these reducers should just handle the original action.\n      return createCellBelow(state, {\n        type: actionTypes.CREATE_CELL_BELOW,\n        payload: {\n          cellType: "code",\n          cell: emptyCodeCell.setIn(["source"], payload.text || ""),\n          id,\n          contentRef: action.payload.contentRef,\n        },\n      });\n    }\n  }\n  // If the payload is unsupported, just return the current state\n  return state;\n}\n\nfunction setInCell(\n  state: NotebookModel,\n  action: actionTypes.SetInCell<string>\n): RecordOf<DocumentRecordProps> {\n  return state.setIn(\n    ["notebook", "cellMap", action.payload.id].concat(action.payload.path),\n    action.payload.value\n  );\n}\n\nfunction toggleCellOutputVisibility(\n  state: NotebookModel,\n  action: actionTypes.ToggleCellOutputVisibility\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n\n  return state.setIn(\n    ["notebook", "cellMap", id, "metadata", "jupyter", "outputs_hidden"],\n    !state.getIn([\n      "notebook",\n      "cellMap",\n      id,\n      "metadata",\n      "jupyter",\n      "outputs_hidden",\n    ])\n  );\n}\n\nfunction unhideAll(\n  state: NotebookModel,\n  action: actionTypes.UnhideAll\n): RecordOf<DocumentRecordProps> {\n  const { outputHidden, inputHidden } = action.payload;\n  let metadataMixin = Map<string, boolean>();\n\n  if (outputHidden !== undefined) {\n    metadataMixin = metadataMixin.set("outputs_hidden", outputHidden);\n  }\n  if (inputHidden !== undefined) {\n    metadataMixin = metadataMixin.set("source_hidden", inputHidden);\n  }\n\n  return state.updateIn(["notebook", "cellMap"], (cellMap) =>\n    cellMap.map((cell: ImmutableCell) => {\n      if ((cell as any).get("cell_type") === "code") {\n        return cell.mergeIn(["metadata", "jupyter"], metadataMixin);\n      }\n      return cell;\n    })\n  );\n}\n\nfunction toggleCellInputVisibility(\n  state: NotebookModel,\n  action: actionTypes.ToggleCellInputVisibility\n  state: NotebookModel,\n  action: actionTypes.UpdateOutputMetadata\n) {\n  const { id, metadata, index } = action.payload;\n  const currentOutputs = state.getIn(["notebook", "cellMap", id, "outputs"]);\n\n  const updatedOutputs = currentOutputs.update(index, (item: any) =>\n    item.set("metadata", fromJS(metadata))\n  );\n\n  return state.setIn(["notebook", "cellMap", id, "outputs"], updatedOutputs);\n  );\n}\n\nfunction updateCellStatus(\n  state: NotebookModel,\n  action: actionTypes.UpdateCellStatus\n): RecordOf<DocumentRecordProps> {\n  const { id, status } = action.payload;\n  return state.setIn(["transient", "cellMap", id, "status"], status);\n}\n\nfunction updateOutputMetadata(\n  state: NotebookModel,\n  action: actionTypes.UpdateOutputMetadata\n): RecordOf<DocumentRecordProps> {\n  const { id, metadata, index, mediaType } = action.payload;\n  const currentOutputs = state.getIn(["notebook", "cellMap", id, "outputs"]);\n\n  const updatedOutputs = currentOutputs.update(index, (item: any) =>\n    item.set(\n      "metadata",\n      fromJS({\n        [mediaType]: metadata,\n      })\n    )\n  );\n\n  return state.setIn(["notebook", "cellMap", id, "outputs"], updatedOutputs);\n}\n\nfunction setLanguageInfo(\n  state: NotebookModel,\n  action: actionTypes.SetLanguageInfo\n): RecordOf<DocumentRecordProps> {\n  const langInfo = fromJS(action.payload.langInfo);\n  return state.setIn(["notebook", "metadata", "language_info"], langInfo);\n}\n\nfunction setKernelMetadata(\n  state: NotebookModel,\n  action: actionTypes.SetKernelMetadata\n): RecordOf<DocumentRecordProps> {\n  const { kernelInfo } = action.payload;\n  if (kernelInfo) {\n    return state\n      .setIn(\n        ["notebook", "metadata", "kernelspec"],\n        fromJS({\n          name: kernelInfo.name,\n          language: kernelInfo.language,\n          display_name: kernelInfo.displayName,\n        })\n      )\n      .setIn(["notebook", "metadata", "kernel_info", "name"], kernelInfo.name);\n  }\n  return state;\n}\n\nfunction overwriteMetadataField(\n  state: NotebookModel,\n  action: actionTypes.OverwriteMetadataField\n): RecordOf<DocumentRecordProps> {\n  const { field, value } = action.payload;\n  return state.setIn(["notebook", "metadata", field], fromJS(value));\n}\nfunction deleteMetadataField(\n  state: NotebookModel,\n  action: actionTypes.DeleteMetadataField\n): RecordOf<DocumentRecordProps> {\n  const { field } = action.payload;\n  return state.deleteIn(["notebook", "metadata", field]);\n}\n\nfunction copyCell(\n  state: NotebookModel,\n  action: actionTypes.CopyCell\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id || state.cellFocused;\n\n  const cell = state.getIn(["notebook", "cellMap", id]);\n  if (!cell) {\n    return state;\n  }\n  return state.set("copied", cell);\n}\n\nfunction cutCell(\n  state: NotebookModel,\n  action: actionTypes.CutCell\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n\n  const cell = state.getIn(["notebook", "cellMap", id]);\n\n  if (!cell) {\n    return state;\n  }\n\n  // FIXME: If the cell that was cut was the focused cell, focus the cell below\n  return state\n    .set("copied", cell)\n    .update("notebook", (notebook: ImmutableNotebook) =>\n      deleteCell(notebook, id)\n    );\n}\n\nfunction pasteCell(state: NotebookModel): RecordOf<DocumentRecordProps> {\n  const copiedCell = state.get("copied");\n\n  const pasteAfter = state.cellFocused;\n\n  if (!copiedCell || !pasteAfter) {\n    return state;\n  }\n\n  // Create a new cell with `id` that will come after the currently focused cell\n  // using the contents of the originally copied cell\n  const id = uuid();\n\n  return state.update("notebook", (notebook: ImmutableNotebook) =>\n    insertCellAfter(notebook, copiedCell, id, pasteAfter)\n  );\n}\n\nfunction changeCellType(\n  state: NotebookModel,\n  action: actionTypes.ChangeCellType\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n\n  const { to } = action.payload;\n\n  const cell = state.getIn(["notebook", "cellMap", id]);\n\n  const from = cell.cell_type;\n\n  // NOOP, since we\'re already that cell type\n  if (from === to) {\n    return state;\n  }\n\n  let nextState = state;\n\n  // from === "code"\n  if (from === "code") {\n    nextState = cleanCellTransient(\n      state\n        .deleteIn(["notebook", "cellMap", id, "execution_count"])\n        .deleteIn(["notebook", "cellMap", id, "outputs"]),\n      id\n    );\n  }\n\n  switch (to) {\n    case "code":\n      return nextState.setIn(\n        ["notebook", "cellMap", id],\n        makeCodeCell({\n          source: cell.source,\n        })\n      );\n    case "markdown":\n      return nextState.setIn(\n        ["notebook", "cellMap", id],\n        makeMarkdownCell({\n          source: cell.source,\n        })\n      );\n    case "raw":\n      return nextState.setIn(\n        ["notebook", "cellMap", id],\n        makeRawCell({\n          source: cell.source,\n        })\n      );\n  }\n\n  // If we didn\'t match on the `to`, we should change nothing as we don\'t implement\n  // other cell types (as there aren\'t any)\n  return state;\n}\n\nfunction toggleOutputExpansion(\n  state: NotebookModel,\n  action: actionTypes.ToggleCellExpansion\n): RecordOf<DocumentRecordProps> {\n  const id = action.payload.id ? action.payload.id : state.cellFocused;\n  if (!id) {\n    return state;\n  }\n\n  return state.updateIn(\n    ["notebook", "cellMap"],\n    (cells: Map<CellId, ImmutableCell>) => {\n      const scrolled = cells.getIn([id, "metadata", "scrolled"]);\n      const isCurrentlyScrolled = scrolled !== false;\n      return cells.setIn(\n        [id, "metadata", "scrolled"],\n        !isCurrentlyScrolled\n      )\n    }\n  );\n}\n\nfunction promptInputRequest(\n  state: NotebookModel,\n  action: actionTypes.PromptInputRequest\n): RecordOf<DocumentRecordProps> {\n  const { id, password, prompt } = action.payload;\n  return state.updateIn(["cellPrompts", id], (prompts) =>\n    prompts.push({\n      prompt,\n      password,\n    })\n  );\n}\n\nfunction interruptKernelSuccessful(\n  state: NotebookModel,\n  action: actionTypes.InterruptKernelSuccessful\n): RecordOf<DocumentRecordProps> {\n  return state.updateIn(["transient", "cellMap"], (cells) =>\n    cells.map((cell: Map<string, string>) => {\n      if (cell.get("status") === "queued" || cell.get("status") === "running") {\n        return cell.set("status", "");\n      }\n      return cell;\n    })\n  );\n}\n\ntype DocumentAction =\n  | actionTypes.ToggleTagInCell\n  | actionTypes.FocusPreviousCellEditor\n  | actionTypes.FocusPreviousCell\n  | actionTypes.FocusNextCellEditor\n  | actionTypes.FocusNextCell\n  | actionTypes.FocusCellEditor\n  | actionTypes.FocusCell\n  | actionTypes.ClearOutputs\n  | actionTypes.AppendOutput\n  | actionTypes.UpdateDisplay\n  | actionTypes.MoveCell\n  | actionTypes.MarkCellAsDeleting\n  | actionTypes.UnmarkCellAsDeleting\n  | actionTypes.DeleteCell\n  | actionTypes.CreateCellBelow\n  | actionTypes.CreateCellAbove\n  | actionTypes.CreateCellAppend\n  | actionTypes.ToggleCellOutputVisibility\n  | actionTypes.ToggleCellInputVisibility\n  | actionTypes.UpdateCellStatus\n  | actionTypes.UpdateOutputMetadata\n  | actionTypes.SetLanguageInfo\n  | actionTypes.SetKernelMetadata\n  | actionTypes.OverwriteMetadataField\n  | actionTypes.DeleteMetadataField\n  | actionTypes.CopyCell\n  | actionTypes.CutCell\n  | actionTypes.PasteCell\n  | actionTypes.ChangeCellType\n  | actionTypes.ToggleCellExpansion\n  | actionTypes.AcceptPayloadMessage\n  | actionTypes.SendExecuteRequest\n  | actionTypes.SaveFulfilled\n  | actionTypes.RestartKernel\n  | actionTypes.ClearAllOutputs\n  | actionTypes.SetInCell<any>\n  | actionTypes.UnhideAll\n  | actionTypes.PromptInputRequest\n  | actionTypes.InterruptKernelSuccessful;\n\nconst defaultDocument: NotebookModel = makeDocumentRecord({\n  notebook: emptyNotebook,\n});\n\nexport function notebook(\n  state: NotebookModel = defaultDocument,\n  action: DocumentAction\n): RecordOf<DocumentRecordProps> {\n  switch (action.type) {\n    case actionTypes.TOGGLE_TAG_IN_CELL:\n      return toggleTagInCell(state, action);\n    case actionTypes.SAVE_FULFILLED:\n      return setNotebookCheckpoint(state);\n    case actionTypes.FOCUS_CELL:\n      return focusCell(state, action);\n    case actionTypes.CLEAR_OUTPUTS:\n      return clearOutputs(state, action);\n    case actionTypes.CLEAR_ALL_OUTPUTS:\n    case actionTypes.RESTART_KERNEL:\n      return clearAllOutputs(state, action);\n    case actionTypes.APPEND_OUTPUT:\n      return appendOutput(state, action);\n    case actionTypes.UPDATE_DISPLAY:\n      return updateDisplay(state, action);\n    case actionTypes.FOCUS_NEXT_CELL:\n      return focusNextCell(state, action);\n    case actionTypes.FOCUS_PREVIOUS_CELL:\n      return focusPreviousCell(state, action);\n    case actionTypes.FOCUS_CELL_EDITOR:\n      return focusCellEditor(state, action);\n    case actionTypes.FOCUS_NEXT_CELL_EDITOR:\n      return focusNextCellEditor(state, action);\n    case actionTypes.FOCUS_PREVIOUS_CELL_EDITOR:\n      return focusPreviousCellEditor(state, action);\n    case actionTypes.SET_IN_CELL:\n      return setInCell(state, action);\n    case actionTypes.MOVE_CELL:\n      return moveCell(state, action);\n    case actionTypes.MARK_CELL_AS_DELETING:\n      return markCellAsDeleting(state, action);\n    case actionTypes.UNMARK_CELL_AS_DELETING:\n      return unmarkCellAsDeleting(state, action);\n    case actionTypes.DELETE_CELL:\n      return deleteCellFromState(state, action);\n    case actionTypes.CREATE_CELL_BELOW:\n      return createCellBelow(state, action);\n    case actionTypes.CREATE_CELL_ABOVE:\n      return createCellAbove(state, action);\n    case actionTypes.CREATE_CELL_APPEND:\n      return createCellAppend(state, action);\n    case actionTypes.TOGGLE_CELL_OUTPUT_VISIBILITY:\n      return toggleCellOutputVisibility(state, action);\n    case actionTypes.TOGGLE_CELL_INPUT_VISIBILITY:\n      return toggleCellInputVisibility(state, action);\n    case actionTypes.ACCEPT_PAYLOAD_MESSAGE:\n      return acceptPayloadMessage(state, action);\n    case actionTypes.UPDATE_CELL_STATUS:\n      return updateCellStatus(state, action);\n    case actionTypes.UPDATE_OUTPUT_METADATA:\n      return updateOutputMetadata(state, action);\n    case actionTypes.SET_LANGUAGE_INFO:\n      return setLanguageInfo(state, action);\n    case actionTypes.SET_KERNEL_METADATA:\n      return setKernelMetadata(state, action);\n    case actionTypes.OVERWRITE_METADATA_FIELD:\n      return overwriteMetadataField(state, action);\n    case actionTypes.DELETE_METADATA_FIELD:\n      return deleteMetadataField(state, action);\n    case actionTypes.COPY_CELL:\n      return copyCell(state, action);\n    case actionTypes.CUT_CELL:\n      return cutCell(state, action);\n    case actionTypes.PASTE_CELL:\n      return pasteCell(state);\n    case actionTypes.CHANGE_CELL_TYPE:\n      return changeCellType(state, action);\n    case actionTypes.TOGGLE_OUTPUT_EXPANSION:\n      return toggleOutputExpansion(state, action);\n    case actionTypes.UNHIDE_ALL:\n      return unhideAll(state, action);\n    case actionTypes.PROMPT_INPUT_REQUEST:\n      return promptInputRequest(state, action);\n    case actionTypes.INTERRUPT_KERNEL_SUCCESSFUL:\n      return interruptKernelSuccessful(state, action);\n    default:\n      return state;\n  }\n}\n', 'message': 'Store and retrieve metadata based on mimetype\n', 'author_name': 'Elijah Meeks', 'lang': 'ts'}