From 616b9ad9d8f24792a804c94187ae5805a68f16b6 Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Tue, 24 Nov 2015 15:24:56 +0100 Subject: [PATCH] fixes #144 --- .../contrib/format/common/formatCommand.ts | 62 ++-- .../format/test/common/formatCommand.test.ts | 295 ++++++++++++++++++ .../common/model/editableTextModel.test.ts | 35 +++ 3 files changed, 363 insertions(+), 29 deletions(-) create mode 100644 src/vs/editor/contrib/format/test/common/formatCommand.test.ts diff --git a/src/vs/editor/contrib/format/common/formatCommand.ts b/src/vs/editor/contrib/format/common/formatCommand.ts index 65b1df24a170e..37f0edce92c5c 100644 --- a/src/vs/editor/contrib/format/common/formatCommand.ts +++ b/src/vs/editor/contrib/format/common/formatCommand.ts @@ -23,8 +23,7 @@ export class EditOperationsCommand implements EditorCommon.ICommand { public getEditOperations(model: EditorCommon.ITokenizedModel, builder: EditorCommon.IEditOperationBuilder): void { this._edits // We know that this edit.range comes from the mirror model, so it should only contain \n and no \r's - .map((edit) => this.fixLineTerminators(edit, model) ) - .map((edit) => this.trimEdit(edit, model)) + .map((edit) => EditOperationsCommand.trimEdit(edit, model)) .filter((edit) => edit !== null) // produced above in case the edit.text is identical to the existing text .forEach((edit) => builder.addEditOperation(Range.lift(edit.range), edit.text)); @@ -50,9 +49,8 @@ export class EditOperationsCommand implements EditorCommon.ICommand { return helper.getTrackedSelection(this._selectionId); } - private fixLineTerminators(edit: EditorCommon.ISingleEditOperation, model: EditorCommon.ITokenizedModel) : EditorCommon.ISingleEditOperation { + static fixLineTerminators(edit: EditorCommon.ISingleEditOperation, model: EditorCommon.ITokenizedModel): void { edit.text = edit.text.replace(/\r\n|\r|\n/g, model.getEOL()); - return edit; } /** @@ -63,42 +61,48 @@ export class EditOperationsCommand implements EditorCommon.ICommand { * bug #15108. There the cursor was jumping since the tracked selection was in the middle of the range edit * and was lost. */ - private trimEdit(edit:EditorCommon.ISingleEditOperation, model: EditorCommon.ITokenizedModel): EditorCommon.ISingleEditOperation { + static trimEdit(edit:EditorCommon.ISingleEditOperation, model: EditorCommon.ITokenizedModel): EditorCommon.ISingleEditOperation { - var currentText = model.getValueInRange(edit.range); + this.fixLineTerminators(edit, model); + + return this._trimEdit(model.validateRange(edit.range), edit.text, edit.forceMoveMarkers, model); + } + + static _trimEdit(editRange:Range, editText:string, editForceMoveMarkers:boolean, model: EditorCommon.ITokenizedModel): EditorCommon.ISingleEditOperation { + + let currentText = model.getValueInRange(editRange); // Find the equal characters in the front - var commonPrefixLength = Strings.commonPrefixLength(edit.text, currentText); + let commonPrefixLength = Strings.commonPrefixLength(editText, currentText); - // If the two strings are identical, return no edit - if (commonPrefixLength === currentText.length && commonPrefixLength === edit.text.length) { + // If the two strings are identical, return no edit (no-op) + if (commonPrefixLength === currentText.length && commonPrefixLength === editText.length) { return null; } - // Only compute a common suffix if none of the strings is already fully contained in the prefix - var commonSuffixLength = 0; - if (commonPrefixLength !== currentText.length && commonPrefixLength !== edit.text.length) { - commonSuffixLength = Strings.commonSuffixLength(edit.text, currentText); + if (commonPrefixLength > 0) { + // Apply front trimming + let newStartPosition = model.modifyPosition(editRange.getStartPosition(), commonPrefixLength); + editRange = new Range(newStartPosition.lineNumber, newStartPosition.column, editRange.endLineNumber, editRange.endColumn); + editText = editText.substring(commonPrefixLength); + currentText = currentText.substr(commonPrefixLength); } - // Adjust start position - var newStartPosition = new Position(edit.range.startLineNumber, edit.range.startColumn); - newStartPosition = model.modifyPosition(newStartPosition, commonPrefixLength); - - // Adjust end position - var newEndPosition = new Position(edit.range.endLineNumber, edit.range.endColumn); - newEndPosition = model.modifyPosition(newEndPosition, -commonSuffixLength); + // Find the equal characters in the rear + let commonSuffixLength = Strings.commonSuffixLength(editText, currentText); - //Trim the text - var newText = edit.text.slice(commonPrefixLength, edit.text.length - commonSuffixLength); + if (commonSuffixLength > 0) { + // Apply rear trimming + let newEndPosition = model.modifyPosition(editRange.getEndPosition(), -commonSuffixLength); + editRange = new Range(editRange.startLineNumber, editRange.startColumn, newEndPosition.lineNumber, newEndPosition.column); + editText = editText.substring(0, editText.length - commonSuffixLength); + currentText = currentText.substring(0, currentText.length - commonSuffixLength); + } return { - text: newText, - range: { - startLineNumber:newStartPosition.lineNumber, - startColumn:newStartPosition.column, - endLineNumber:newEndPosition.lineNumber, - endColumn: newEndPosition.column - }}; + text: editText, + range: editRange, + forceMoveMarkers: editForceMoveMarkers + }; } } diff --git a/src/vs/editor/contrib/format/test/common/formatCommand.test.ts b/src/vs/editor/contrib/format/test/common/formatCommand.test.ts new file mode 100644 index 0000000000000..897863c949cf5 --- /dev/null +++ b/src/vs/editor/contrib/format/test/common/formatCommand.test.ts @@ -0,0 +1,295 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import assert = require('assert'); +import {Range} from 'vs/editor/common/core/range'; +import TU = require('vs/editor/test/common/commands/commandTestUtils'); +import {EditOperationsCommand} from 'vs/editor/contrib/format/common/formatCommand'; +import {Selection} from 'vs/editor/common/core/selection'; +import {Model} from 'vs/editor/common/model/model'; +import EditorCommon = require('vs/editor/common/editorCommon'); + +function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, text:string[]): EditorCommon.ISingleEditOperation { + return { + range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), + text: text.join('\n'), + forceMoveMarkers: false + }; +} + +suite('FormatCommand.trimEdit', () => { + function testTrimEdit(lines: string[], edit:EditorCommon.ISingleEditOperation, expected:EditorCommon.ISingleEditOperation): void { + let model = new Model(lines.join('\n'), null); + let actual = EditOperationsCommand.trimEdit(edit, model); + assert.deepEqual(actual, expected); + model.dispose(); + } + + test('single-line no-op', () => { + testTrimEdit( + [ + 'some text', + 'some other text' + ], + editOp(1,1,1,10, [ + 'some text' + ]), + null + ) + }); + + test('multi-line no-op 1', () => { + testTrimEdit( + [ + 'some text', + 'some other text' + ], + editOp(1,1,2,16, [ + 'some text', + 'some other text' + ]), + null + ) + }); + + test('multi-line no-op 2', () => { + testTrimEdit( + [ + 'some text', + 'some other text' + ], + editOp(1,1,2,1, [ + 'some text', + '' + ]), + null + ) + }); + + test('simple prefix, no suffix', () => { + testTrimEdit( + [ + 'some text', + 'some other text' + ], + editOp(1,1,1,10, [ + 'some interesting thing' + ]), + editOp(1,6,1,10, [ + 'interesting thing' + ]) + ) + }); + + test('whole line prefix, no suffix', () => { + testTrimEdit( + [ + 'some text', + 'some other text' + ], + editOp(1,1,1,10, [ + 'some text', + 'interesting thing' + ]), + editOp(1,10,1,10, [ + '', + 'interesting thing' + ]) + ) + }); + + test('multi-line prefix, no suffix', () => { + testTrimEdit( + [ + 'some text', + 'some other text' + ], + editOp(1,1,2,16, [ + 'some text', + 'some other interesting thing' + ]), + editOp(2,12,2,16, [ + 'interesting thing' + ]) + ) + }); + + test('no prefix, simple suffix', () => { + testTrimEdit( + [ + 'some text', + 'some other text' + ], + editOp(1,1,1,10, [ + 'interesting text' + ]), + editOp(1,1,1,5, [ + 'interesting' + ]) + ) + }); + + test('no prefix, whole line suffix', () => { + testTrimEdit( + [ + 'some text', + 'some other text' + ], + editOp(1,1,1,10, [ + 'interesting thing', + 'some text' + ]), + editOp(1,1,1,1, [ + 'interesting thing', + '' + ]) + ) + }); + + test('no prefix, multi-line suffix', () => { + testTrimEdit( + [ + 'some text', + 'some other text' + ], + editOp(1,1,2,16, [ + 'interesting thing text', + 'some other text' + ]), + editOp(1,1,1,5, [ + 'interesting thing' + ]) + ) + }); + + test('no overlapping prefix & suffix', () => { + testTrimEdit( + [ + 'some cool text' + ], + editOp(1,1,1,15, [ + 'some interesting text' + ]), + editOp(1,6,1,10, [ + 'interesting' + ]) + ) + }); + + test('overlapping prefix & suffix 1', () => { + testTrimEdit( + [ + 'some cool text' + ], + editOp(1,1,1,15, [ + 'some cool cool text' + ]), + editOp(1,11,1,11, [ + 'cool ' + ]) + ) + }); + + test('overlapping prefix & suffix 2', () => { + testTrimEdit( + [ + 'some cool cool text' + ], + editOp(1,1,1,29, [ + 'some cool text' + ]), + editOp(1,11,1,16, [ + '' + ]) + ) + }); +}); + +suite('FormatCommand', () => { + function testFormatCommand(lines: string[], selection: Selection, edits:EditorCommon.ISingleEditOperation[], expectedLines: string[], expectedSelection: Selection): void { + TU.testCommand(lines, null, selection, (sel) => new EditOperationsCommand(edits, sel), expectedLines, expectedSelection); + } + + test('no-op', () => { + testFormatCommand( + [ + 'some text', + 'some other text' + ], + new Selection(2,1,2,5), + [ + editOp(1, 1, 2, 16, [ + 'some text', + 'some other text' + ]) + ], + [ + 'some text', + 'some other text' + ], + new Selection(2,1,2,5) + ); + }); + + test('trim beginning', () => { + testFormatCommand( + [ + 'some text', + 'some other text' + ], + new Selection(2,1,2,5), + [ + editOp(1, 1, 2, 16, [ + 'some text', + 'some new other text' + ]) + ], + [ + 'some text', + 'some new other text' + ], + new Selection(2,1,2,5) + ); + }); + + test('issue #144', () => { + testFormatCommand( + [ + 'package caddy', + '', + 'func main() {', + '\tfmt.Println("Hello World! :)")', + '}', + '' + ], + new Selection(1,1,1,1), + [ + editOp(1, 1, 6, 1, [ + 'package caddy', + '', + 'import "fmt"', + '', + 'func main() {', + '\tfmt.Println("Hello World! :)")', + '}', + '' + ]) + ], + [ + 'package caddy', + '', + 'import "fmt"', + '', + 'func main() {', + '\tfmt.Println("Hello World! :)")', + '}', + '' + ], + new Selection(1,1,1,1) + ); + }); + +}); \ No newline at end of file diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index eeae4109e0fc0..75c6088e86f5f 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -1139,6 +1139,41 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { ); }); + test('issue #144', () => { + testApplyEdits( + [ + 'package caddy', + '', + 'func main() {', + '\tfmt.Println("Hello World! :)")', + '}', + '' + ], + [ + editOp(1, 1, 6, 1, [ + 'package caddy', + '', + 'import "fmt"', + '', + 'func main() {', + '\tfmt.Println("Hello World! :)")', + '}', + '' + ]) + ], + [ + 'package caddy', + '', + 'import "fmt"', + '', + 'func main() {', + '\tfmt.Println("Hello World! :)")', + '}', + '' + ] + ); + }); + function assertSyncedModels(text:string, callback:(model:EditableTextModel, assertMirrorModels:()=>void)=>void, setup:(model:EditableTextModel)=>void = null): void { var model = new EditableTextModel([], TextModel.toRawText(text), null); model.setEOL(EditorCommon.EndOfLineSequence.LF);