From 22d9406e584f6b730191041bb31fac7c8ce0b74e Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 4 Mar 2025 21:12:53 +1100 Subject: [PATCH] Support moving notebook cells during chat edits (#242534) * Support moving a nb cell whilst in chat edits * Fix tests * Fixes to insert/delete of cells * More fixes --- .../chatEditingModifiedNotebookEntry.ts | 267 ++++- .../chatEditingNotebookEditorIntegration.ts | 8 + .../chatEditingModifiedNotebookEntry.test.ts | 1037 +++++++++++++++++ 3 files changed, 1259 insertions(+), 53 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index 21069ff486f..ad28059c6a1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -48,7 +48,7 @@ import { CellEditState, getNotebookEditorFromEditorPane } from '../../../noteboo import { INotebookEditorService } from '../../../notebook/browser/services/notebookEditorService.js'; import { NotebookCellTextModel } from '../../../notebook/common/model/notebookCellTextModel.js'; import { NotebookTextModel } from '../../../notebook/common/model/notebookTextModel.js'; -import { CellEditType, ICellDto2, ICellEditOperation, ICellReplaceEdit, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookTextModelChangedEvent, TransientOptions } from '../../../notebook/common/notebookCommon.js'; +import { CellEditType, ICell, ICellDto2, ICellEditOperation, ICellReplaceEdit, IResolvedNotebookEditorModel, NotebookCellsChangeType, NotebookCellsModelMoveEvent, NotebookCellTextModelSplice, NotebookTextModelChangedEvent, TransientOptions } from '../../../notebook/common/notebookCommon.js'; import { computeDiff } from '../../../notebook/common/notebookDiff.js'; import { INotebookEditorModelResolverService } from '../../../notebook/common/notebookEditorModelResolverService.js'; import { INotebookLoggingService } from '../../../notebook/common/notebookLoggingService.js'; @@ -243,14 +243,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie } mirrorNotebookEdits(e: NotebookTextModelChangedEvent) { - /** - * TODO@DonJayamanne - * If user deletes cells, invoke this.disposeDeletedCellEntries(); - * If user makes any changes, invoke this.computeStateAfterAcceptingRejectingChanges(true); - * If user deletes/inserts cells manually, do we need to apply those to the original snapshot? Unlikely, double check. - */ if (this._isEditFromUs || Array.from(this.cellEntryMap.values()).some(entry => entry.isEditFromUs)) { - // TODO@DonJayamanne Apply this same edit to the original notebook. return; } @@ -288,44 +281,16 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie break; } case NotebookCellsChangeType.ModelChange: { + let cellDiffs = sortCellChanges(this._cellsDiffInfo.get()).slice(); event.changes.forEach(change => { - const cells = change[2].map(cell => { - return { - cellKind: cell.cellKind, - language: cell.language, - metadata: cell.metadata, - outputs: cell.outputs, - source: cell.getValue(), - mime: undefined, - internalMetadata: cell.internalMetadata - } satisfies ICellDto2; - }); - const cellDiffs = sortCellChanges(this._cellsDiffInfo.get()).slice(); - const wasInsertedAsFirstCell = change[0] === 0; - const wasInsertedAsLastCell = change[0] === this.modifiedModel.cells.length - 1; - const diffEntryIndex = wasInsertedAsFirstCell ? 0 : (wasInsertedAsLastCell ? this.originalModel.cells.length : (cellDiffs.findIndex(d => d.modifiedCellIndex === change[0]))); - const indexToInsertInOriginalModel = (wasInsertedAsFirstCell || diffEntryIndex === -1) ? 0 : (wasInsertedAsLastCell ? this.originalModel.cells.length : (((cellDiffs.slice(0, diffEntryIndex).reverse().find(c => typeof c.originalCellIndex === 'number')?.originalCellIndex ?? -1) + 1))); - const edit: ICellEditOperation = { - editType: CellEditType.Replace, - cells, - index: indexToInsertInOriginalModel, - count: change[1] - }; - this.originalModel.applyEdits([edit], true, undefined, () => undefined, undefined, true); - // If cells were deleted we handled that with this.disposeDeletedCellEntries(); - if (diffEntryIndex >= 0) { - cellDiffs.splice(diffEntryIndex, change[1]); - } - // For inserted cells, we need to ensure that we create a corresponding CellEntry. - // So that any edits to the inserted cell is handled and mirrored over to the corresponding cell in original model. - cells.forEach((_, i) => { - const originalCellIndex = i + indexToInsertInOriginalModel; - const modifiedCellIndex = change[0] + i; - const unchangedCell = this.createModifiedCellDiffInfo(modifiedCellIndex, originalCellIndex); - cellDiffs.splice(diffEntryIndex === -1 ? 0 : diffEntryIndex, 0, unchangedCell); - this.updateCellDiffInfo(cellDiffs, undefined); - }); + cellDiffs = adjustCellDiffAndOriginalModelBasedOnCellAddDelete(change, + cellDiffs, + this.modifiedModel.cells.length, + this.originalModel.cells.length, + this.originalModel.applyEdits.bind(this.originalModel), + this.createModifiedCellDiffInfo.bind(this)); }); + this.updateCellDiffInfo(cellDiffs, undefined); this.disposeDeletedCellEntries(); break; } @@ -379,15 +344,11 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie break; } case NotebookCellsChangeType.Move: { - const edit: ICellEditOperation = { - editType: CellEditType.Move, - index: event.index, - length: event.length, - newIdx: event.newIdx - }; - this.originalModel.applyEdits([edit], true, undefined, () => undefined, undefined, true); - // TODO@DonJayamanne - // We need to update the entries in _cellDiffInfo. + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements(event, this._cellsDiffInfo.get().slice()); + if (result) { + this.originalModel.applyEdits(result[1], true, undefined, () => undefined, undefined, true); + this._cellsDiffInfo.set(result[0], undefined); + } break; } default: { @@ -1280,3 +1241,203 @@ class ChatEditingNotebookCellEntry extends ObservableDisposable { } } } + +export function adjustCellDiffAndOriginalModelBasedOnCellAddDelete(change: NotebookCellTextModelSplice, + cellDiffInfo: ICellDiffInfo[], + modifiedModelCellCount: number, + originalModelCellCount: number, + applyEdits: typeof NotebookTextModel.prototype.applyEdits, + createModifiedCellDiffInfo: (modifiedCellIndex: number, originalCellIndex: number) => ICellDiffInfo, +): ICellDiffInfo[] { + cellDiffInfo = sortCellChanges(cellDiffInfo).slice(); + const numberOfCellsInserted = change[2].length; + const numberOfCellsDeleted = change[1]; + const cells = change[2].map(cell => { + return { + cellKind: cell.cellKind, + language: cell.language, + metadata: cell.metadata, + outputs: cell.outputs, + source: cell.getValue(), + mime: undefined, + internalMetadata: cell.internalMetadata + } satisfies ICellDto2; + }); + const wasInsertedAsFirstCell = change[0] === 0; + const wasInsertedAsLastCell = change[0] === modifiedModelCellCount - 1; + const diffEntryIndex = wasInsertedAsFirstCell ? 0 : (wasInsertedAsLastCell ? cellDiffInfo.length - 1 : (cellDiffInfo.findIndex(d => d.modifiedCellIndex === change[0]))); + const indexToInsertInOriginalModel = (wasInsertedAsFirstCell || diffEntryIndex === -1) ? 0 : (wasInsertedAsLastCell ? originalModelCellCount : (((cellDiffInfo.slice(0, diffEntryIndex).reverse().find(c => typeof c.originalCellIndex === 'number')?.originalCellIndex ?? -1) + 1))); + if (cells.length) { + const edit: ICellEditOperation = { + editType: CellEditType.Replace, + cells, + index: indexToInsertInOriginalModel, + count: change[1] + }; + applyEdits([edit], true, undefined, () => undefined, undefined, true); + } + // If cells were deleted we handled that with this.disposeDeletedCellEntries(); + if (numberOfCellsDeleted) { + // Adjust the indexes. + let numberOfOriginalCellsRemovedSoFar = 0; + let numberOfModifiedCellsRemovedSoFar = 0; + const modifiedIndexesToRemove = new Set(); + for (let i = 0; i < numberOfCellsDeleted; i++) { + modifiedIndexesToRemove.add(change[0] + i); + } + const itemsToRemove = new Set(); + for (let i = 0; i < cellDiffInfo.length; i++) { + const diff = cellDiffInfo[i]; + if (i < diffEntryIndex) { + continue; + } + + let changed = false; + if (typeof diff.modifiedCellIndex === 'number' && modifiedIndexesToRemove.has(diff.modifiedCellIndex)) { + // This will be removed. + numberOfModifiedCellsRemovedSoFar++; + if (typeof diff.originalCellIndex === 'number') { + numberOfOriginalCellsRemovedSoFar++; + } + itemsToRemove.add(diff); + continue; + } + if (typeof diff.modifiedCellIndex === 'number' && numberOfModifiedCellsRemovedSoFar) { + diff.modifiedCellIndex -= numberOfModifiedCellsRemovedSoFar; + changed = true; + } + if (typeof diff.originalCellIndex === 'number' && numberOfOriginalCellsRemovedSoFar) { + diff.originalCellIndex -= numberOfOriginalCellsRemovedSoFar; + changed = true; + } + if (changed) { + cellDiffInfo[i] = { ...diff }; + } + } + cellDiffInfo = cellDiffInfo.filter(d => !itemsToRemove.has(d)); + } + + if (numberOfCellsInserted) { + for (let i = 0; i < cellDiffInfo.length; i++) { + const diff = cellDiffInfo[i]; + if (i < diffEntryIndex) { + continue; + } + let changed = false; + if (typeof diff.modifiedCellIndex === 'number') { + diff.modifiedCellIndex += numberOfCellsInserted; + changed = true; + } + if (typeof diff.originalCellIndex === 'number') { + diff.originalCellIndex += numberOfCellsInserted; + changed = true; + } + if (changed) { + cellDiffInfo[i] = { ...diff }; + } + } + } + + // For inserted cells, we need to ensure that we create a corresponding CellEntry. + // So that any edits to the inserted cell is handled and mirrored over to the corresponding cell in original model. + cells.forEach((_, i) => { + const originalCellIndex = i + indexToInsertInOriginalModel; + const modifiedCellIndex = change[0] + i; + const unchangedCell = createModifiedCellDiffInfo(modifiedCellIndex, originalCellIndex); + cellDiffInfo.splice((diffEntryIndex === -1 ? 0 : diffEntryIndex) + i, 0, unchangedCell); + }); + return cellDiffInfo; +} +/** + * Given the movements of cells in modified notebook, adjust the ICellDiffInfo[] array + * and generate edits for the old notebook (if required). + * TODO@DonJayamanne Handle bulk moves (movements of more than 1 cell). + */ +export function adjustCellDiffAndOriginalModelBasedOnCellMovements(event: NotebookCellsModelMoveEvent, cellDiffInfo: ICellDiffInfo[]): [ICellDiffInfo[], ICellEditOperation[]] | undefined { + const minimumIndex = Math.min(event.index, event.newIdx); + const maximumIndex = Math.max(event.index, event.newIdx); + const cellDiffs = cellDiffInfo.slice(); + const indexOfEntry = cellDiffs.findIndex(d => d.modifiedCellIndex === event.index); + const indexOfEntryToPlaceBelow = cellDiffs.findIndex(d => d.modifiedCellIndex === event.newIdx); + if (indexOfEntry === -1 || indexOfEntryToPlaceBelow === -1) { + return undefined; + } + // Create a new object so that the observable value is triggered. + // Besides we'll be updating the values of this object in place. + const entryToBeMoved = { ...cellDiffs[indexOfEntry] }; + const moveDirection = event.newIdx > event.index ? 'down' : 'up'; + + + const startIndex = cellDiffs.findIndex(d => d.modifiedCellIndex === minimumIndex); + const endIndex = cellDiffs.findIndex(d => d.modifiedCellIndex === maximumIndex); + const movingExistingCell = typeof entryToBeMoved.originalCellIndex === 'number'; + let originalCellsWereEffected = false; + for (let i = 0; i < cellDiffs.length; i++) { + const diff = cellDiffs[i]; + let changed = false; + if (moveDirection === 'down') { + if (i > startIndex && i <= endIndex) { + if (typeof diff.modifiedCellIndex === 'number') { + changed = true; + diff.modifiedCellIndex = diff.modifiedCellIndex - 1; + } + if (typeof diff.originalCellIndex === 'number' && movingExistingCell) { + diff.originalCellIndex = diff.originalCellIndex - 1; + originalCellsWereEffected = true; + changed = true; + } + } + } else { + if (i >= startIndex && i < endIndex) { + if (typeof diff.modifiedCellIndex === 'number') { + changed = true; + diff.modifiedCellIndex = diff.modifiedCellIndex + 1; + } + if (typeof diff.originalCellIndex === 'number' && movingExistingCell) { + diff.originalCellIndex = diff.originalCellIndex + 1; + originalCellsWereEffected = true; + changed = true; + } + } + } + // Create a new object so that the observable value is triggered. + // Do only if there's a change. + if (changed) { + cellDiffs[i] = { ...diff }; + } + } + entryToBeMoved.modifiedCellIndex = event.newIdx; + const originalCellIndex = entryToBeMoved.originalCellIndex; + if (moveDirection === 'down') { + cellDiffs.splice(endIndex + 1, 0, entryToBeMoved); + cellDiffs.splice(startIndex, 1); + // If we're moving a new cell up/down, then we need just adjust just the modified indexes of the cells in between. + // If we're moving an existing up/down, then we need to adjust the original indexes as well. + if (typeof entryToBeMoved.originalCellIndex === 'number') { + entryToBeMoved.originalCellIndex = cellDiffs.slice(0, endIndex).reduce((lastOriginalIndex, diff) => typeof diff.originalCellIndex === 'number' ? Math.max(lastOriginalIndex, diff.originalCellIndex) : lastOriginalIndex, -1) + 1; + } + } else { + cellDiffs.splice(endIndex, 1); + cellDiffs.splice(startIndex, 0, entryToBeMoved); + // If we're moving a new cell up/down, then we need just adjust just the modified indexes of the cells in between. + // If we're moving an existing up/down, then we need to adjust the original indexes as well. + if (typeof entryToBeMoved.originalCellIndex === 'number') { + entryToBeMoved.originalCellIndex = cellDiffs.slice(0, startIndex).reduce((lastOriginalIndex, diff) => typeof diff.originalCellIndex === 'number' ? Math.max(lastOriginalIndex, diff.originalCellIndex) : lastOriginalIndex, -1) + 1; + } + } + + // If this is a new cell that we're moving, and there are no existing cells in between, then we can just move the new cell. + // I.e. no need to update the original notebook model. + if (typeof entryToBeMoved.originalCellIndex === 'number' && originalCellsWereEffected && typeof originalCellIndex === 'number' && entryToBeMoved.originalCellIndex !== originalCellIndex) { + const edit: ICellEditOperation = { + editType: CellEditType.Move, + index: originalCellIndex, + length: event.length, + newIdx: entryToBeMoved.originalCellIndex + }; + + return [cellDiffs, [edit]]; + } + + return [cellDiffs, []]; +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingNotebookEditorIntegration.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingNotebookEditorIntegration.ts index 774012a01bf..459490d5369 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingNotebookEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingNotebookEditorIntegration.ts @@ -595,9 +595,17 @@ export function sortCellChanges(changes: ICellDiffInfo[]): ICellDiffInfo[] { return a.modifiedCellIndex - b.modifiedCellIndex; } + if (a.type === 'delete' && b.type === 'insert') { + return -1; + } + if (a.type === 'insert' && b.type === 'delete') { + return 1; + } + if ((a.type === 'delete' && b.type !== 'insert') || (a.type !== 'insert' && b.type === 'delete')) { return a.originalCellIndex - b.originalCellIndex; } + // Mixed types: compare based on available indices const aIndex = a.type === 'delete' ? a.originalCellIndex : (a.type === 'insert' ? a.modifiedCellIndex : a.modifiedCellIndex); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts new file mode 100644 index 00000000000..17201568d18 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts @@ -0,0 +1,1037 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { adjustCellDiffAndOriginalModelBasedOnCellAddDelete, adjustCellDiffAndOriginalModelBasedOnCellMovements } from '../../browser/chatEditing/chatEditingModifiedNotebookEntry.js'; +import { ICellDiffInfo } from '../../browser/chatEditing/chatEditingNotebookEditorIntegration.js'; +import { nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; +import { ObservablePromise, observableValue } from '../../../../../base/common/observable.js'; +import { CellEditType, CellKind, ICell, ICellEditOperation, NotebookCellsChangeType } from '../../../notebook/common/notebookCommon.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { hash } from '../../../../../base/common/hash.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; + +suite('ChatEditingModifiedNotebookEntry Cell Addition', function () { + + const keep = () => Promise.resolve(true); + const undo = () => Promise.resolve(true); + const diff = observableValue('cell1', nullDocumentDiff); + const appliedEdits: ICellEditOperation[] = []; + setup(() => { + appliedEdits.length = 0; + }); + ensureNoDisposablesAreLeakedInTestSuite(); + function createModifiedModel(id: string): ObservablePromise { + return `Modified:${id}` as any; + + } + function createOriginalModel(id: string): ObservablePromise { + return `Original:${id}` as any; + + } + function applyEdits(edits: ICellEditOperation[]): boolean { + appliedEdits.push(...edits); + return true; + } + + function createICell(cellKind: CellKind, source: string): ICell { + const handle = hash(generateUuid()); + return { + uri: URI.parse(`file:///path/${handle}`), + handle, + cellKind, + language: cellKind === CellKind.Markup ? 'markdown' : 'python', + outputs: [], + metadata: {}, + getHashValue: () => { + return hash(`${handle}=>${cellKind}=>${source}`); + }, + getValue: () => { + return source; + }, + internalMetadata: {}, + } as any; + } + function createModifiedCellDiffInfo(modifiedCellIndex: number, originalCellIndex: number): ICellDiffInfo { + return { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:${originalCellIndex}`), originalCellIndex, + modifiedCellIndex, modifiedModel: createModifiedModel(`InsertedModified:${modifiedCellIndex}`), + }; + } + test('Insert a new cell into an unchanged notebook', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([0, 0, [createICell(CellKind.Code, 'print("Hello World")')]], + cellsDiffInfo, 2, 2, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:0`), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel(`InsertedModified:0`), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 2, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Insert a new cell into an notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([2, 0, [createICell(CellKind.Code, 'print("Hello World")')]], + cellsDiffInfo, 4, 6, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:4`), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel(`InsertedModified:2`), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 6, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Insert 2 new cells into an notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([2, 0, [createICell(CellKind.Code, 'print("Hello World")'), createICell(CellKind.Code, 'print("Foo Bar")')]], + cellsDiffInfo, 4, 6, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:4`), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel(`InsertedModified:2`), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel(`InsertedOriginal:5`), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel(`InsertedModified:3`), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 6, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 7, + modifiedCellIndex: 5, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Delete a cell from an unchanged notebook', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([0, 1, []], + cellsDiffInfo, 2, 2, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Delete last cell from an unchanged notebook', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([1, 1, []], + cellsDiffInfo, 2, 2, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Delete a new cell from a notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([1, 1, [ + // createICell(CellKind.Code, 'print("Hello World")') + ]], + cellsDiffInfo, 4, 6, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Delete 2 cells from a notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([1, 2, [ + ]], + cellsDiffInfo, 4, 6, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Delete 3 cells from a notebook with 3 cells deleted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'modified', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('6'), originalCellIndex: 6, + modifiedCellIndex: 4, modifiedModel: createModifiedModel('6'), + }, + ]; + + const result = adjustCellDiffAndOriginalModelBasedOnCellAddDelete([1, 3, [ + ]], + cellsDiffInfo, 5, 7, applyEdits, createModifiedCellDiffInfo); + + assert.deepStrictEqual(result, [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('6'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('6'), + }, + ]); + }); +}); + +suite('ChatEditingModifiedNotebookEntry Cell Movements', function () { + + const keep = () => Promise.resolve(true); + const undo = () => Promise.resolve(true); + const diff = observableValue('cell1', nullDocumentDiff); + + ensureNoDisposablesAreLeakedInTestSuite(); + function createModifiedModel(id: string): ObservablePromise { + return `Modified:${id}` as any; + + } + function createOriginalModel(id: string): ObservablePromise { + return `Original:${id}` as any; + + } + test('Swap first two inserted cells in a previously empty notebook', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 0, length: 1, newIdx: 1 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Swap first two inserted cells in a notebook that had 2 cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 0, length: 1, newIdx: 1 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]); + }); + test('Move first inserted cell to the very bottom of notebook that had 2 cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 0, length: 1, newIdx: 3 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('3'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Move last cell to top of notebook after 2 cells were inserted', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 3, length: 1, newIdx: 0 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 1, + length: 1, + newIdx: 0 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('3'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('2'), + }, + ]); + }); + + test('Move second inserted cell to the very bottom of notebook that had 2 cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 1, length: 1, newIdx: 3 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('3'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Move second inserted cell to the second last position of notebook that had 2 cells', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 1, length: 1, newIdx: 2 + }, cellsDiffInfo); + + assert.ok(result); + assert.strictEqual(result[1].length, 0); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('3'), + } + ]); + }); + test('Move first cell to the last position of notebook that had 3 cells deleted from the middle', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 0, length: 1, newIdx: 2 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 0, + length: 1, + newIdx: 5 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('0'), + }, + ]); + }); + test('Move second cell to the last position of notebook that had 3 cells deleted from the middle', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('2'), + }, + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 1, length: 1, newIdx: 2 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 1, + length: 1, + newIdx: 5 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('2'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + + test('Move second cell to the last position of notebook that had 3 cells deleted from middle and 1 inserted in the middle', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('5'), + }, + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 1, length: 1, newIdx: 3 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 1, + length: 1, + newIdx: 5 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 1, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 4, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('1'), + }, + ]); + }); + test('Move last cell to the second position of notebook that had 3 cells deleted from middle and 1 inserted in the middle', async function () { + const cellsDiffInfo: ICellDiffInfo[] = [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 2, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('New1'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 5, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('5'), + }, + ]; + const result = adjustCellDiffAndOriginalModelBasedOnCellMovements({ + cells: [], kind: NotebookCellsChangeType.Move, + index: 3, length: 1, newIdx: 1 + }, cellsDiffInfo); + + assert.ok(result); + assert.deepStrictEqual(result[1], [ + { + editType: CellEditType.Move, + index: 5, + length: 1, + newIdx: 1 + } + ]); + assert.deepStrictEqual(result[0], [ + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('0'), originalCellIndex: 0, + modifiedCellIndex: 0, modifiedModel: createModifiedModel('0'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('5'), originalCellIndex: 1, + modifiedCellIndex: 1, modifiedModel: createModifiedModel('5'), + }, + { + diff, keep, undo, type: 'unchanged', originalModel: createOriginalModel('1'), originalCellIndex: 2, + modifiedCellIndex: 2, modifiedModel: createModifiedModel('1'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('2'), originalCellIndex: 3, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('3'), originalCellIndex: 4, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'delete', originalModel: createOriginalModel('4'), originalCellIndex: 5, + modifiedCellIndex: undefined, modifiedModel: createModifiedModel('null'), + }, + { + diff, keep, undo, type: 'insert', originalModel: createOriginalModel('null'), originalCellIndex: undefined, + modifiedCellIndex: 3, modifiedModel: createModifiedModel('New1'), + }, + ]); + }); +});