1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3'use strict';
4
5import * as nbformat from '@jupyterlab/nbformat';
6
7import {
8  IDiffEntry, IDiffArrayEntry
9} from '../diffentries';
10
11import {
12  getSubDiffByKey
13} from '../util';
14
15import {
16  IStringDiffModel, createPatchStringDiffModel
17} from './string';
18
19import {
20  CellDiffModel,
21  createUnchangedCellDiffModel, createAddedCellDiffModel,
22  createDeletedCellDiffModel, createPatchedCellDiffModel
23} from './cell';
24
25
26/**
27 * Diff model for a Jupyter Notebook
28 */
29export class NotebookDiffModel {
30
31  /**
32   * Create a new NotebookDiffModel from a base notebook and a list of diffs.
33   *
34   * The base as well as the diff entries are normally supplied by the nbdime
35   * server.
36   */
37  constructor(base: nbformat.INotebookContent, diff: IDiffEntry[]) {
38    // Process global notebook metadata field
39    let metaDiff = getSubDiffByKey(diff, 'metadata');
40    if (base.metadata && metaDiff) {
41      this.metadata = createPatchStringDiffModel(base.metadata, metaDiff);
42    } else {
43      this.metadata = null;
44    }
45    if (this.metadata) {
46      this.metadata.collapsible = true;
47      this.metadata.collapsibleHeader = 'Notebook metadata changed';
48      this.metadata.startCollapsed = true;
49    }
50    // The notebook metadata MIME type is used for determining the MIME type
51    // of source cells, so store it easily accessible:
52    let mimetype: string | undefined;
53    try {
54      mimetype = base.metadata.language_info!.mimetype;
55    } catch (e) {
56      // missing metadata (probably old notebook)
57    }
58    this.mimetype = mimetype || 'text/python';
59
60    // Build cell diff models. Follows similar logic to patching code:
61    this.cells = [];
62    this.chunkedCells = [];
63    let take = 0;
64    let skip = 0;
65    let previousChunkIndex = -1;
66    let currentChunk: CellDiffModel[] = [];
67    for (let e of getSubDiffByKey(diff, 'cells') as IDiffArrayEntry[] || []) {
68      let index = e.key;
69
70      // diff is sorted on index, so take any preceding cells as unchanged:
71      for (let i=take; i < index; i++) {
72        let cell = createUnchangedCellDiffModel(base.cells[i], this.mimetype);
73        this.cells.push(cell);
74        this.chunkedCells.push([cell]);
75      }
76
77      if (index !== previousChunkIndex) {
78        currentChunk = [];
79        this.chunkedCells.push(currentChunk);
80        previousChunkIndex = index;
81      }
82
83      // Process according to diff type:
84      if (e.op === 'addrange') {
85        // One or more inserted/added cells:
86        for (let ei of e.valuelist) {
87          let cell = createAddedCellDiffModel(ei as nbformat.ICell, this.mimetype);
88          this.cells.push(cell);
89          currentChunk.push(cell);
90        }
91        skip = 0;
92      } else if (e.op === 'removerange') {
93        // One or more removed/deleted cells:
94        skip = e.length;
95        for (let i=index; i < index + skip; i++) {
96          let cell = createDeletedCellDiffModel(base.cells[i], this.mimetype);
97          this.cells.push(cell);
98          currentChunk.push(cell);
99        }
100      } else if (e.op === 'patch') {
101        // Ensure patches gets their own chunk, even if they share index:
102        if (currentChunk.length > 0) {
103          currentChunk = [];
104          this.chunkedCells.push(currentChunk);
105        }
106        // A cell has changed:
107        let cell = createPatchedCellDiffModel(base.cells[index], e.diff, this.mimetype);
108        this.cells.push(cell);
109        currentChunk.push(cell);
110        skip = 1;
111      }
112
113      // Skip the specified number of elements, but never decrement take.
114      // Note that take can pass index in diffs with repeated +/- on the
115      // same index, i.e. [op_remove(index), op_add(index, value)]
116      take = Math.max(take, index + skip);
117    }
118    // Take unchanged values at end
119    for (let i=take; i < base.cells.length; i++) {
120      let cell = createUnchangedCellDiffModel(base.cells[i], this.mimetype);
121      this.cells.push(cell);
122      this.chunkedCells.push([cell]);
123    }
124  }
125
126  /**
127   * Diff model of the notebook's root metadata field
128   */
129  metadata: IStringDiffModel | null;
130
131  /**
132   * The default MIME type according to the notebook's root metadata
133   */
134  mimetype: string;
135
136  /**
137   * List of all cell diff models, including unchanged, added/removed and
138   * changed cells, in order.
139   */
140  cells: CellDiffModel[];
141
142  /**
143   * List of chunks of cells, e.g. so that any changes that occur in the same
144   * location optionally can be shown side by side.
145   */
146  chunkedCells: CellDiffModel[][];
147}
148