From 1990d30a95cc74751e207c2ea252ed8065187034 Mon Sep 17 00:00:00 2001 From: Ryan Stringham Date: Tue, 27 Jun 2017 12:24:18 -0600 Subject: [PATCH 1/4] Keep undo history even when file changes outside Code. --- src/vs/editor/common/editorCommon.ts | 6 + src/vs/editor/common/model/textModel.ts | 115 ++++++++++ .../common/services/modelServiceImpl.ts | 8 +- .../test/common/model/textModel.test.ts | 200 +++++++++++++++++- .../textfile/common/textFileEditorModel.ts | 1 + 5 files changed, 326 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 313e090044b1a..f2b67450260e5 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -660,6 +660,12 @@ export interface ITextModel { */ validateRange(range: IRange): Range; + /** + * Get a list of edits to change this model to match the other model. + * @internal + */ + getEdits(other: ITextSource): IIdentifiedSingleEditOperation[]; + /** * Converts the position to a zero-based offset. * diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 5eb1f1bba7e41..185b9ae7b7b0e 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -18,6 +18,7 @@ import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelS import { TextSource, ITextSource, IRawTextSource, RawTextSource } from 'vs/editor/common/model/textSource'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; +import { EditOperation } from "vs/editor/common/core/editOperation"; const USE_MIMINAL_MODEL_LINE = true; @@ -752,6 +753,120 @@ export class TextModel implements editorCommon.ITextModel { return new Range(startLineNumber, startColumn, endLineNumber, endColumn + 1); } + public getEdits(other: ITextSource): editorCommon.IIdentifiedSingleEditOperation[] { + //find the minimum number of lines to add/remove in order to get this model to look like the passed in text source + //implementation of the Wagner–Fischer algorithm + interface Operation { + type: 'insert' | 'delete' | 'replace'; + index: number; + value?: string; + before?: boolean; + } + interface DistanceEntry { + distance: number; + operation?: Operation; + previous?: DistanceEntry; + } + let l1 = this._lines.map(line => line.text); + let l2 = other.lines; + let distanceMatrix: DistanceEntry[][] = []; + for (let i = 0; i < l1.length + 1; i++) { + let row = []; + for (let j = 0; j < l2.length + 1; j++) { + row.push({ + distance: 0, + }); + } + distanceMatrix.push(row); + } + + for (let i = 0; i <= l1.length; i++) { + distanceMatrix[i][0].distance = i; + if (i > 0) { + distanceMatrix[i][0].operation = { + type: 'delete', + index: i - 1 + }; + distanceMatrix[i][0].previous = distanceMatrix[i - 1][0]; + } + } + + for (let j = 0; j <= l2.length; j++) { + distanceMatrix[0][j].distance = j; + if (j > 0) { + distanceMatrix[0][j].operation = { + type: 'insert', + index: 0, + value: l2[j - 1], + before: true, + }; + distanceMatrix[0][j].previous = distanceMatrix[0][j - 1]; + } + } + + for (let j = 1; j <= l2.length; j++) { + for (let i = 1; i <= l1.length; i++) { + if (l1[i - 1] === l2[j - 1]) { + distanceMatrix[i][j].distance = distanceMatrix[i - 1][j - 1].distance; + distanceMatrix[i][j].previous = distanceMatrix[i - 1][j - 1]; + } else { + if (distanceMatrix[i - 1][j].distance <= distanceMatrix[i][j - 1].distance && + distanceMatrix[i - 1][j].distance <= distanceMatrix[i - 1][j - 1].distance) { + // deletion + distanceMatrix[i][j].distance = distanceMatrix[i - 1][j].distance + 1; + distanceMatrix[i][j].previous = distanceMatrix[i - 1][j]; + distanceMatrix[i][j].operation = { + type: 'delete', + index: i - 1, + }; + } else if (distanceMatrix[i][j - 1].distance <= distanceMatrix[i - 1][j - 1].distance) { + //insertion + distanceMatrix[i][j].distance = distanceMatrix[i][j - 1].distance + 1; + distanceMatrix[i][j].previous = distanceMatrix[i][j - 1]; + distanceMatrix[i][j].operation = { + type: 'insert', + index: i - 1, + value: l2[j - 1], + }; + } else { + //substitution + distanceMatrix[i][j].distance = distanceMatrix[i - 1][j - 1].distance + 1; + distanceMatrix[i][j].previous = distanceMatrix[i - 1][j - 1]; + distanceMatrix[i][j].operation = { + type: 'replace', + index: i - 1, + value: l2[j - 1], + }; + } + } + } + } + + let getEdit = (o: Operation): editorCommon.IIdentifiedSingleEditOperation => { + let lineNumber = o.index + 1; + if (o.type === 'delete') { + return EditOperation.delete(new Range(lineNumber, 1, lineNumber + 1, 1)); + } else if (o.type === 'insert') { + return EditOperation.insert(new Position(lineNumber + (o.before ? 0 : 1), 1), o.value + this._EOL); + } else if (o.type === 'replace') { + return EditOperation.replace(new Range(lineNumber, 1, lineNumber, this._lines[o.index].text.length + 1), o.value); + } + throw new Error('invalid operation'); + }; + + let result: editorCommon.IIdentifiedSingleEditOperation[] = []; + + let current: DistanceEntry | undefined = distanceMatrix[l1.length][l2.length]; + while (current) { + if (current.operation) { + result.unshift(getEdit(current.operation)); + } + current = current.previous; + } + + return result; + } + public modifyPosition(rawPosition: IPosition, offset: number): Position { this._assertNotDisposed(); return this.getPositionAt(this.getOffsetAt(rawPosition) + offset); diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 24040eb3e5467..1ad4b20b2858c 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -15,6 +15,7 @@ import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IMarker, IMarkerService } from 'vs/platform/markers/common/markers'; import { Range } from 'vs/editor/common/core/range'; +import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { Model } from 'vs/editor/common/model/model'; import { IMode, LanguageIdentifier } from 'vs/editor/common/modes'; @@ -371,9 +372,12 @@ export class ModelServiceImpl implements IModelService { if (model.equals(textSource)) { return; } - // Otherwise update model - model.setValueFromTextSource(textSource); + model.pushEditOperations( + [new Selection(1, 1, 1, 1)], + model.getEdits(textSource), + (inverseEditOperations: editorCommon.IIdentifiedSingleEditOperation[]) => [new Selection(1, 1, 1, 1)] + ); } public createModel(value: string | IRawTextSource, modeOrPromise: TPromise | IMode, resource: URI): editorCommon.IModel { diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index cba2a056ebae1..05d589a19a43f 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -8,8 +8,8 @@ import * as assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { TextModel, ITextModelCreationData } from 'vs/editor/common/model/textModel'; -import { DefaultEndOfLine, TextModelResolvedOptions } from 'vs/editor/common/editorCommon'; -import { RawTextSource } from 'vs/editor/common/model/textSource'; +import { DefaultEndOfLine, TextModelResolvedOptions, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; +import { RawTextSource, TextSource } from 'vs/editor/common/model/textSource'; function testGuessIndentation(defaultInsertSpaces: boolean, defaultTabSize: number, expectedInsertSpaces: boolean, expectedTabSize: number, text: string[], msg?: string): void { var m = TextModel.createFromString( @@ -995,3 +995,199 @@ suite('TextModel.getLineIndentGuide', () => { ]); }); }); + +suite('Get edits', () => { + function getEdits(lines1: string[], lines2: string[]): IIdentifiedSingleEditOperation[] { + const model = TextModel.createFromString(lines1.join('\n')); + const textSource = TextSource.create(lines2.join('\n'), model.getOptions().defaultEOL); + return model.getEdits(textSource); + } + + test('does insertions in the middle of the document', () => { + const edits = getEdits( + [ + 'line 1', + 'line 2', + 'line 3' + ], [ + 'line 1', + 'line 2', + 'line 5', + 'line 3' + ] + ); + assert.equal(edits.length, 1); + assert.deepEqual(edits[0], { + 'identifier': null, + 'range': { + 'startLineNumber': 3, + 'startColumn': 1, + 'endLineNumber': 3, + 'endColumn': 1 + }, + 'text': 'line 5\n', + 'forceMoveMarkers': true + }); + }); + + test('does insertions at the end of the document', () => { + const edits = getEdits( + [ + 'line 1', + 'line 2', + 'line 3' + ], [ + 'line 1', + 'line 2', + 'line 3', + 'line 4' + ] + ); + assert.equal(edits.length, 1); + assert.deepEqual(edits[0], { + 'identifier': null, + 'range': { + 'startLineNumber': 4, + 'startColumn': 1, + 'endLineNumber': 4, + 'endColumn': 1 + }, + 'text': 'line 4\n', + 'forceMoveMarkers': true + }); + }); + + test('does insertions at the beginning of the document', () => { + const edits = getEdits( + [ + 'line 1', + 'line 2', + 'line 3' + ], [ + 'line 0', + 'line 1', + 'line 2', + 'line 3' + ] + ); + assert.equal(edits.length, 1); + assert.deepEqual(edits[0], { + 'identifier': null, + 'range': { + 'startLineNumber': 1, + 'startColumn': 1, + 'endLineNumber': 1, + 'endColumn': 1 + }, + 'text': 'line 0\n', + 'forceMoveMarkers': true + }); + }); + + test('does replacements', () => { + const edits = getEdits( + [ + 'line 1', + 'line 2', + 'line 3' + ], [ + 'line 1', + 'line 7', + 'line 3' + ] + ); + assert.equal(edits.length, 1); + assert.deepEqual(edits[0], { + 'identifier': null, + 'range': { + 'startLineNumber': 2, + 'startColumn': 1, + 'endLineNumber': 2, + 'endColumn': 7 + }, + 'text': 'line 7', + 'forceMoveMarkers': false + }); + }); + + test('does deletions', () => { + const edits = getEdits( + [ + 'line 1', + 'line 2', + 'line 3' + ], [ + 'line 1', + 'line 3' + ] + ); + assert.equal(edits.length, 1); + assert.deepEqual(edits[0], { + 'identifier': null, + 'range': { + 'startLineNumber': 2, + 'startColumn': 1, + 'endLineNumber': 3, + 'endColumn': 1 + }, + 'text': null, + 'forceMoveMarkers': true + }); + }); + + test('does insert, replace, and delete', () => { + const edits = getEdits( + [ + 'line 1', + 'line 2', + 'line 3', + 'line 4', + 'line 5', + ], [ + 'line 0', // insert line 0 + 'line 1', + 'replace line 2', // replace line 2 + 'line 3', + // delete line 4 + 'line 5', + ] + ); + + assert.equal(edits.length, 3); + assert.deepEqual(edits, [ + { + 'identifier': null, + 'range': { + 'startLineNumber': 1, + 'startColumn': 1, + 'endLineNumber': 1, + 'endColumn': 1 + }, + 'text': 'line 0\n', + 'forceMoveMarkers': true + }, + { + 'identifier': null, + 'range': { + 'startLineNumber': 2, + 'startColumn': 1, + 'endLineNumber': 2, + 'endColumn': 7 + }, + 'text': 'replace line 2', + 'forceMoveMarkers': false + }, + { + 'identifier': null, + 'range': { + 'startLineNumber': 4, + 'startColumn': 1, + 'endLineNumber': 5, + 'endColumn': 1 + }, + 'text': null, + 'forceMoveMarkers': true + } + ]); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 2d4b9eee76a93..7f270afb4b457 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -413,6 +413,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.blockModelContentChange = true; try { this.updateTextEditorModel(value); + this.setDirty(false); } finally { this.blockModelContentChange = false; } From 4173c4b0b3009ad01f69efa256f4527eddc2fc22 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 28 Aug 2017 21:55:01 +0200 Subject: [PATCH 2/4] Switch to using our existing diff algorithm --- src/vs/editor/common/editorCommon.ts | 6 - src/vs/editor/common/model/textModel.ts | 115 ------- .../common/services/modelServiceImpl.ts | 100 +++++- .../test/common/model/textModel.test.ts | 200 +----------- .../test/common/services/modelService.test.ts | 297 +++++++++++++++++- 5 files changed, 395 insertions(+), 323 deletions(-) diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index cfe01f6300e4f..be008c501789d 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -660,12 +660,6 @@ export interface ITextModel { */ validateRange(range: IRange): Range; - /** - * Get a list of edits to change this model to match the other model. - * @internal - */ - getEdits(other: ITextSource): IIdentifiedSingleEditOperation[]; - /** * Converts the position to a zero-based offset. * diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 518fb28ef1211..ce088944f8d39 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -18,7 +18,6 @@ import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelS import { TextSource, ITextSource, IRawTextSource, RawTextSource } from 'vs/editor/common/model/textSource'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; -import { EditOperation } from "vs/editor/common/core/editOperation"; const USE_MIMINAL_MODEL_LINE = true; @@ -753,120 +752,6 @@ export class TextModel implements editorCommon.ITextModel { return new Range(startLineNumber, startColumn, endLineNumber, endColumn + 1); } - public getEdits(other: ITextSource): editorCommon.IIdentifiedSingleEditOperation[] { - //find the minimum number of lines to add/remove in order to get this model to look like the passed in text source - //implementation of the Wagner–Fischer algorithm - interface Operation { - type: 'insert' | 'delete' | 'replace'; - index: number; - value?: string; - before?: boolean; - } - interface DistanceEntry { - distance: number; - operation?: Operation; - previous?: DistanceEntry; - } - let l1 = this._lines.map(line => line.text); - let l2 = other.lines; - let distanceMatrix: DistanceEntry[][] = []; - for (let i = 0; i < l1.length + 1; i++) { - let row = []; - for (let j = 0; j < l2.length + 1; j++) { - row.push({ - distance: 0, - }); - } - distanceMatrix.push(row); - } - - for (let i = 0; i <= l1.length; i++) { - distanceMatrix[i][0].distance = i; - if (i > 0) { - distanceMatrix[i][0].operation = { - type: 'delete', - index: i - 1 - }; - distanceMatrix[i][0].previous = distanceMatrix[i - 1][0]; - } - } - - for (let j = 0; j <= l2.length; j++) { - distanceMatrix[0][j].distance = j; - if (j > 0) { - distanceMatrix[0][j].operation = { - type: 'insert', - index: 0, - value: l2[j - 1], - before: true, - }; - distanceMatrix[0][j].previous = distanceMatrix[0][j - 1]; - } - } - - for (let j = 1; j <= l2.length; j++) { - for (let i = 1; i <= l1.length; i++) { - if (l1[i - 1] === l2[j - 1]) { - distanceMatrix[i][j].distance = distanceMatrix[i - 1][j - 1].distance; - distanceMatrix[i][j].previous = distanceMatrix[i - 1][j - 1]; - } else { - if (distanceMatrix[i - 1][j].distance <= distanceMatrix[i][j - 1].distance && - distanceMatrix[i - 1][j].distance <= distanceMatrix[i - 1][j - 1].distance) { - // deletion - distanceMatrix[i][j].distance = distanceMatrix[i - 1][j].distance + 1; - distanceMatrix[i][j].previous = distanceMatrix[i - 1][j]; - distanceMatrix[i][j].operation = { - type: 'delete', - index: i - 1, - }; - } else if (distanceMatrix[i][j - 1].distance <= distanceMatrix[i - 1][j - 1].distance) { - //insertion - distanceMatrix[i][j].distance = distanceMatrix[i][j - 1].distance + 1; - distanceMatrix[i][j].previous = distanceMatrix[i][j - 1]; - distanceMatrix[i][j].operation = { - type: 'insert', - index: i - 1, - value: l2[j - 1], - }; - } else { - //substitution - distanceMatrix[i][j].distance = distanceMatrix[i - 1][j - 1].distance + 1; - distanceMatrix[i][j].previous = distanceMatrix[i - 1][j - 1]; - distanceMatrix[i][j].operation = { - type: 'replace', - index: i - 1, - value: l2[j - 1], - }; - } - } - } - } - - let getEdit = (o: Operation): editorCommon.IIdentifiedSingleEditOperation => { - let lineNumber = o.index + 1; - if (o.type === 'delete') { - return EditOperation.delete(new Range(lineNumber, 1, lineNumber + 1, 1)); - } else if (o.type === 'insert') { - return EditOperation.insert(new Position(lineNumber + (o.before ? 0 : 1), 1), o.value + this._EOL); - } else if (o.type === 'replace') { - return EditOperation.replace(new Range(lineNumber, 1, lineNumber, this._lines[o.index].text.length + 1), o.value); - } - throw new Error('invalid operation'); - }; - - let result: editorCommon.IIdentifiedSingleEditOperation[] = []; - - let current: DistanceEntry | undefined = distanceMatrix[l1.length][l2.length]; - while (current) { - if (current.operation) { - result.unshift(getEdit(current.operation)); - } - current = current.previous; - } - - return result; - } - public modifyPosition(rawPosition: IPosition, offset: number): Position { this._assertNotDisposed(); return this.getPositionAt(this.getOffsetAt(rawPosition) + offset); diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index a051d19f7cae4..304ef213a5fb2 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -24,9 +24,11 @@ import * as platform from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { PLAINTEXT_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/modesRegistry'; -import { IRawTextSource, TextSource, RawTextSource } from 'vs/editor/common/model/textSource'; +import { IRawTextSource, TextSource, RawTextSource, ITextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ClassName } from 'vs/editor/common/model/textModelWithDecorations'; +import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -376,14 +378,106 @@ export class ModelServiceImpl implements IModelService { if (model.equals(textSource)) { return; } - // Otherwise update model + + // Otherwise find a diff between the values and update model + // TODO: update BOM, EOL model.pushEditOperations( [new Selection(1, 1, 1, 1)], - model.getEdits(textSource), + ModelServiceImpl._computeEdits(model, textSource), (inverseEditOperations: editorCommon.IIdentifiedSingleEditOperation[]) => [new Selection(1, 1, 1, 1)] ); } + /** + * Compute edits to bring `model` to the state of `textSource`. + */ + public static _computeEdits(model: editorCommon.IModel, textSource: ITextSource): editorCommon.IIdentifiedSingleEditOperation[] { + const modelLineSequence = new class implements ISequence { + public getLength(): number { + return model.getLineCount(); + } + public getElementHash(index: number): string { + return model.getLineContent(index + 1); + } + }; + const textSourceLineSequence = new class implements ISequence { + public getLength(): number { + return textSource.lines.length; + } + public getElementHash(index: number): string { + return textSource.lines[index]; + } + }; + + const diffResult = new LcsDiff(modelLineSequence, textSourceLineSequence).ComputeDiff(false); + + let edits: editorCommon.IIdentifiedSingleEditOperation[] = [], editsLen = 0; + const modelLineCount = model.getLineCount(); + for (let i = 0, len = diffResult.length; i < len; i++) { + const diff = diffResult[i]; + const originalStart = diff.originalStart; + const originalLength = diff.originalLength; + const modifiedStart = diff.modifiedStart; + const modifiedLength = diff.modifiedLength; + + let lines: string[] = []; + for (let j = 0; j < modifiedLength; j++) { + lines[j] = textSource.lines[modifiedStart + j]; + } + let text = lines.join('\n'); + + let range: Range; + if (originalLength === 0) { + // insertion + + if (originalStart === modelLineCount) { + // insert at the end + const maxLineColumn = model.getLineMaxColumn(modelLineCount); + range = new Range( + modelLineCount, maxLineColumn, + modelLineCount, maxLineColumn + ); + text = '\n' + text; + } else { + // insert + range = new Range( + originalStart + 1, 1, + originalStart + 1, 1 + ); + text = text + '\n'; + } + + } else if (modifiedLength === 0) { + // deletion + + if (originalStart + originalLength >= modelLineCount) { + // delete at the end + range = new Range( + originalStart, model.getLineMaxColumn(originalStart), + originalStart + originalLength, model.getLineMaxColumn(originalStart + originalLength) + ); + } else { + // delete + range = new Range( + originalStart + 1, 1, + originalStart + originalLength + 1, 1 + ); + } + + } else { + // modification + range = new Range( + originalStart + 1, 1, + originalStart + originalLength, model.getLineMaxColumn(originalStart + originalLength) + ); + } + + edits[editsLen++] = EditOperation.replace(range, text); + } + + return edits; + } + public createModel(value: string | IRawTextSource, modeOrPromise: TPromise | IMode, resource: URI): editorCommon.IModel { let modelData: ModelData; diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index 05d589a19a43f..cba2a056ebae1 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -8,8 +8,8 @@ import * as assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { TextModel, ITextModelCreationData } from 'vs/editor/common/model/textModel'; -import { DefaultEndOfLine, TextModelResolvedOptions, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; -import { RawTextSource, TextSource } from 'vs/editor/common/model/textSource'; +import { DefaultEndOfLine, TextModelResolvedOptions } from 'vs/editor/common/editorCommon'; +import { RawTextSource } from 'vs/editor/common/model/textSource'; function testGuessIndentation(defaultInsertSpaces: boolean, defaultTabSize: number, expectedInsertSpaces: boolean, expectedTabSize: number, text: string[], msg?: string): void { var m = TextModel.createFromString( @@ -995,199 +995,3 @@ suite('TextModel.getLineIndentGuide', () => { ]); }); }); - -suite('Get edits', () => { - function getEdits(lines1: string[], lines2: string[]): IIdentifiedSingleEditOperation[] { - const model = TextModel.createFromString(lines1.join('\n')); - const textSource = TextSource.create(lines2.join('\n'), model.getOptions().defaultEOL); - return model.getEdits(textSource); - } - - test('does insertions in the middle of the document', () => { - const edits = getEdits( - [ - 'line 1', - 'line 2', - 'line 3' - ], [ - 'line 1', - 'line 2', - 'line 5', - 'line 3' - ] - ); - assert.equal(edits.length, 1); - assert.deepEqual(edits[0], { - 'identifier': null, - 'range': { - 'startLineNumber': 3, - 'startColumn': 1, - 'endLineNumber': 3, - 'endColumn': 1 - }, - 'text': 'line 5\n', - 'forceMoveMarkers': true - }); - }); - - test('does insertions at the end of the document', () => { - const edits = getEdits( - [ - 'line 1', - 'line 2', - 'line 3' - ], [ - 'line 1', - 'line 2', - 'line 3', - 'line 4' - ] - ); - assert.equal(edits.length, 1); - assert.deepEqual(edits[0], { - 'identifier': null, - 'range': { - 'startLineNumber': 4, - 'startColumn': 1, - 'endLineNumber': 4, - 'endColumn': 1 - }, - 'text': 'line 4\n', - 'forceMoveMarkers': true - }); - }); - - test('does insertions at the beginning of the document', () => { - const edits = getEdits( - [ - 'line 1', - 'line 2', - 'line 3' - ], [ - 'line 0', - 'line 1', - 'line 2', - 'line 3' - ] - ); - assert.equal(edits.length, 1); - assert.deepEqual(edits[0], { - 'identifier': null, - 'range': { - 'startLineNumber': 1, - 'startColumn': 1, - 'endLineNumber': 1, - 'endColumn': 1 - }, - 'text': 'line 0\n', - 'forceMoveMarkers': true - }); - }); - - test('does replacements', () => { - const edits = getEdits( - [ - 'line 1', - 'line 2', - 'line 3' - ], [ - 'line 1', - 'line 7', - 'line 3' - ] - ); - assert.equal(edits.length, 1); - assert.deepEqual(edits[0], { - 'identifier': null, - 'range': { - 'startLineNumber': 2, - 'startColumn': 1, - 'endLineNumber': 2, - 'endColumn': 7 - }, - 'text': 'line 7', - 'forceMoveMarkers': false - }); - }); - - test('does deletions', () => { - const edits = getEdits( - [ - 'line 1', - 'line 2', - 'line 3' - ], [ - 'line 1', - 'line 3' - ] - ); - assert.equal(edits.length, 1); - assert.deepEqual(edits[0], { - 'identifier': null, - 'range': { - 'startLineNumber': 2, - 'startColumn': 1, - 'endLineNumber': 3, - 'endColumn': 1 - }, - 'text': null, - 'forceMoveMarkers': true - }); - }); - - test('does insert, replace, and delete', () => { - const edits = getEdits( - [ - 'line 1', - 'line 2', - 'line 3', - 'line 4', - 'line 5', - ], [ - 'line 0', // insert line 0 - 'line 1', - 'replace line 2', // replace line 2 - 'line 3', - // delete line 4 - 'line 5', - ] - ); - - assert.equal(edits.length, 3); - assert.deepEqual(edits, [ - { - 'identifier': null, - 'range': { - 'startLineNumber': 1, - 'startColumn': 1, - 'endLineNumber': 1, - 'endColumn': 1 - }, - 'text': 'line 0\n', - 'forceMoveMarkers': true - }, - { - 'identifier': null, - 'range': { - 'startLineNumber': 2, - 'startColumn': 1, - 'endLineNumber': 2, - 'endColumn': 7 - }, - 'text': 'replace line 2', - 'forceMoveMarkers': false - }, - { - 'identifier': null, - 'range': { - 'startLineNumber': 4, - 'startColumn': 1, - 'endLineNumber': 5, - 'endColumn': 1 - }, - 'text': null, - 'forceMoveMarkers': true - } - ]); - }); -}); \ No newline at end of file diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 08c1bd0fca8ac..09a020208704e 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -10,6 +10,14 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import URI from 'vs/base/common/uri'; import * as platform from 'vs/base/common/platform'; import { DefaultEndOfLine } from 'vs/editor/common/editorCommon'; +import { Model } from 'vs/editor/common/model/model'; +import { TextSource } from 'vs/editor/common/model/textSource'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { Range } from 'vs/editor/common/core/range'; +import { CharCode } from 'vs/base/common/charCode'; +import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; + +const GENERATE_TESTS = false; suite('ModelService', () => { let modelService: ModelServiceImpl; @@ -35,4 +43,291 @@ suite('ModelService', () => { assert.equal(model2.getOptions().defaultEOL, DefaultEndOfLine.CRLF); assert.equal(model3.getOptions().defaultEOL, DefaultEndOfLine.LF); }); -}); \ No newline at end of file + + test('_computeEdits first line changed', function () { + + const model = Model.createFromString( + [ + 'This is line one', //16 + 'and this is line number two', //27 + 'it is followed by #3', //20 + 'and finished with the fourth.', //29 + ].join('\n') + ); + + const textSource = TextSource.fromString( + [ + 'This is line One', //16 + 'and this is line number two', //27 + 'it is followed by #3', //20 + 'and finished with the fourth.', //29 + ].join('\n'), + DefaultEndOfLine.LF + ); + + const actual = ModelServiceImpl._computeEdits(model, textSource); + + assert.deepEqual(actual, [ + EditOperation.replace(new Range(1, 1, 1, 17), 'This is line One') + ]); + }); + + test('_computeEdits EOL changed', function () { + + const model = Model.createFromString( + [ + 'This is line one', //16 + 'and this is line number two', //27 + 'it is followed by #3', //20 + 'and finished with the fourth.', //29 + ].join('\n') + ); + + const textSource = TextSource.fromString( + [ + 'This is line one', //16 + 'and this is line number two', //27 + 'it is followed by #3', //20 + 'and finished with the fourth.', //29 + ].join('\r\n'), + DefaultEndOfLine.LF + ); + + const actual = ModelServiceImpl._computeEdits(model, textSource); + + assert.deepEqual(actual, []); + }); + + test('_computeEdits EOL and other change 1', function () { + + const model = Model.createFromString( + [ + 'This is line one', //16 + 'and this is line number two', //27 + 'it is followed by #3', //20 + 'and finished with the fourth.', //29 + ].join('\n') + ); + + const textSource = TextSource.fromString( + [ + 'This is line One', //16 + 'and this is line number two', //27 + 'It is followed by #3', //20 + 'and finished with the fourth.', //29 + ].join('\r\n'), + DefaultEndOfLine.LF + ); + + const actual = ModelServiceImpl._computeEdits(model, textSource); + + assert.deepEqual(actual, [ + EditOperation.replace(new Range(1, 1, 1, 17), 'This is line One'), + EditOperation.replace(new Range(3, 1, 3, 21), 'It is followed by #3') + ]); + }); + + test('_computeEdits EOL and other change 2', function () { + + const model = Model.createFromString( + [ + 'package main', // 1 + 'func foo() {', // 2 + '}' // 3 + ].join('\n') + ); + + const textSource = TextSource.fromString( + [ + 'package main', // 1 + 'func foo() {', // 2 + '}', // 3 + '' + ].join('\r\n'), + DefaultEndOfLine.LF + ); + + const actual = ModelServiceImpl._computeEdits(model, textSource); + + assert.deepEqual(actual, [ + EditOperation.replace(new Range(3, 2, 3, 2), '\n') + ]); + }); + + test('generated1', () => { + const file1 = ['pram', 'okctibad', 'pjuwtemued', 'knnnm', 'u', '']; + const file2 = ['tcnr', 'rxwlicro', 'vnzy', '', '', 'pjzcogzur', 'ptmxyp', 'dfyshia', 'pee', 'ygg']; + assertComputeEdits(file1, file2); + }); + + test('generated2', () => { + const file1 = ['', 'itls', 'hrilyhesv', '']; + const file2 = ['vdl', '', 'tchgz', 'bhx', 'nyl']; + assertComputeEdits(file1, file2); + }); + + test('generated3', () => { + const file1 = ['ubrbrcv', 'wv', 'xodspybszt', 's', 'wednjxm', 'fklajt', 'fyfc', 'lvejgge', 'rtpjlodmmk', 'arivtgmjdm']; + const file2 = ['s', 'qj', 'tu', 'ur', 'qerhjjhyvx', 't']; + assertComputeEdits(file1, file2); + }); + + test('generated4', () => { + const file1 = ['ig', 'kh', 'hxegci', 'smvker', 'pkdmjjdqnv', 'vgkkqqx', '', 'jrzeb']; + const file2 = ['yk', '']; + assertComputeEdits(file1, file2); + }); + + test('does insertions in the middle of the document', () => { + const file1 = [ + 'line 1', + 'line 2', + 'line 3' + ]; + const file2 = [ + 'line 1', + 'line 2', + 'line 5', + 'line 3' + ]; + assertComputeEdits(file1, file2); + }); + + test('does insertions at the end of the document', () => { + const file1 = [ + 'line 1', + 'line 2', + 'line 3' + ]; + const file2 = [ + 'line 1', + 'line 2', + 'line 3', + 'line 4' + ]; + assertComputeEdits(file1, file2); + }); + + test('does insertions at the beginning of the document', () => { + const file1 = [ + 'line 1', + 'line 2', + 'line 3' + ]; + const file2 = [ + 'line 0', + 'line 1', + 'line 2', + 'line 3' + ]; + assertComputeEdits(file1, file2); + }); + + test('does replacements', () => { + const file1 = [ + 'line 1', + 'line 2', + 'line 3' + ]; + const file2 = [ + 'line 1', + 'line 7', + 'line 3' + ]; + assertComputeEdits(file1, file2); + }); + + test('does deletions', () => { + const file1 = [ + 'line 1', + 'line 2', + 'line 3' + ]; + const file2 = [ + 'line 1', + 'line 3' + ]; + assertComputeEdits(file1, file2); + }); + + test('does insert, replace, and delete', () => { + const file1 = [ + 'line 1', + 'line 2', + 'line 3', + 'line 4', + 'line 5', + ]; + const file2 = [ + 'line 0', // insert line 0 + 'line 1', + 'replace line 2', // replace line 2 + 'line 3', + // delete line 4 + 'line 5', + ]; + assertComputeEdits(file1, file2); + }); +}); + +function assertComputeEdits(lines1: string[], lines2: string[]): void { + const model = Model.createFromString(lines1.join('\n')); + const textSource = TextSource.fromString(lines2.join('\n'), DefaultEndOfLine.LF); + + // compute required edits + // let start = Date.now(); + const edits = ModelServiceImpl._computeEdits(model, textSource); + // console.log(`took ${Date.now() - start} ms.`); + + // apply edits + model.pushEditOperations(null, edits, null); + + assert.equal(model.getValue(), lines2.join('\n')); +} + +function getRandomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function getRandomString(minLength: number, maxLength: number): string { + let length = getRandomInt(minLength, maxLength); + let t = createStringBuilder(length); + for (let i = 0; i < length; i++) { + t.appendASCII(getRandomInt(CharCode.a, CharCode.z)); + } + return t.build(); +} + +function generateFile(small: boolean): string[] { + let lineCount = getRandomInt(1, small ? 3 : 10000); + let lines: string[] = []; + for (let i = 0; i < lineCount; i++) { + lines.push(getRandomString(0, small ? 3 : 10000)); + } + return lines; +} + +if (GENERATE_TESTS) { + let number = 1; + while (true) { + + console.log('------TEST: ' + number++); + + const file1 = generateFile(true); + const file2 = generateFile(true); + + console.log('------TEST GENERATED'); + + try { + assertComputeEdits(file1, file2); + } catch (err) { + console.log(err); + console.log(` +const file1 = ${JSON.stringify(file1).replace(/"/g, '\'')}; +const file2 = ${JSON.stringify(file2).replace(/"/g, '\'')}; +assertComputeEdits(file1, file2); +`); + break; + } + } +} \ No newline at end of file From eb52beaec2f23e092afecf35b25c72308ab8dca8 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 28 Aug 2017 22:10:21 +0200 Subject: [PATCH 3/4] Remove unnecessary change --- src/vs/workbench/services/textfile/common/textFileEditorModel.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index e0074192b1e4c..1314d6dea90a5 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -413,7 +413,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.blockModelContentChange = true; try { this.updateTextEditorModel(value); - this.setDirty(false); } finally { this.blockModelContentChange = false; } From 576a13336786ec289d36c44d5d61def78ca1ca81 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Mon, 28 Aug 2017 22:13:16 +0200 Subject: [PATCH 4/4] Use the TextSource EOL when updating the model --- src/vs/editor/common/services/modelServiceImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 304ef213a5fb2..67e4748603001 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -380,7 +380,7 @@ export class ModelServiceImpl implements IModelService { } // Otherwise find a diff between the values and update model - // TODO: update BOM, EOL + model.setEOL(textSource.EOL === '\r\n' ? editorCommon.EndOfLineSequence.CRLF : editorCommon.EndOfLineSequence.LF); model.pushEditOperations( [new Selection(1, 1, 1, 1)], ModelServiceImpl._computeEdits(model, textSource),