diff --git a/src/vs/editor/common/model/editableTextModel.ts b/src/vs/editor/common/model/editableTextModel.ts index 630ccd9b553a6cebd7e2a4da81b9a3407310fade..b326a78140c5170bead4628db9c529a7d81348b4 100644 --- a/src/vs/editor/common/model/editableTextModel.ts +++ b/src/vs/editor/common/model/editableTextModel.ts @@ -97,46 +97,69 @@ export class EditableTextModel extends TextModelWithDecorations implements edito }; }); - for (let i = 0, len = this._trimAutoWhitespaceLines.length; i < len; i++) { - let trimLineNumber = this._trimAutoWhitespaceLines[i]; - let maxLineColumn = this.getLineMaxColumn(trimLineNumber); - - let allowTrimLine = true; + // Sometimes, auto-formatters change ranges automatically which can cause undesired auto whitespace trimming near the cursor + // We'll use the following heuristic: if the edits occur near the cursor, then it's ok to trim auto whitespace + let editsAreNearCursors = true; + for (let i = 0, len = beforeCursorState.length; i < len; i++) { + let sel = beforeCursorState[i]; + let foundEditNearSel = false; for (let j = 0, lenJ = incomingEdits.length; j < lenJ; j++) { let editRange = incomingEdits[j].range; - let editText = incomingEdits[j].text; - - if (trimLineNumber < editRange.startLineNumber || trimLineNumber > editRange.endLineNumber) { - // `trimLine` is completely outside this edit - continue; + let selIsAbove = editRange.startLineNumber > sel.endLineNumber; + let selIsBelow = sel.startLineNumber > editRange.endLineNumber; + if (!selIsAbove && !selIsBelow) { + foundEditNearSel = true; + break; } + } + if (!foundEditNearSel) { + editsAreNearCursors = false; + break; + } + } - // At this point: - // editRange.startLineNumber <= trimLine <= editRange.endLineNumber - - if ( - trimLineNumber === editRange.startLineNumber && editRange.startColumn === maxLineColumn - && editRange.isEmpty() && editText && editText.length > 0 && editText.charAt(0) === '\n' - ) { - // This edit inserts a new line (and maybe other text) after `trimLine` - continue; + if (editsAreNearCursors) { + for (let i = 0, len = this._trimAutoWhitespaceLines.length; i < len; i++) { + let trimLineNumber = this._trimAutoWhitespaceLines[i]; + let maxLineColumn = this.getLineMaxColumn(trimLineNumber); + + let allowTrimLine = true; + for (let j = 0, lenJ = incomingEdits.length; j < lenJ; j++) { + let editRange = incomingEdits[j].range; + let editText = incomingEdits[j].text; + + if (trimLineNumber < editRange.startLineNumber || trimLineNumber > editRange.endLineNumber) { + // `trimLine` is completely outside this edit + continue; + } + + // At this point: + // editRange.startLineNumber <= trimLine <= editRange.endLineNumber + + if ( + trimLineNumber === editRange.startLineNumber && editRange.startColumn === maxLineColumn + && editRange.isEmpty() && editText && editText.length > 0 && editText.charAt(0) === '\n' + ) { + // This edit inserts a new line (and maybe other text) after `trimLine` + continue; + } + + // Looks like we can't trim this line as it would interfere with an incoming edit + allowTrimLine = false; + break; } - // Looks like we can't trim this line as it would interfere with an incoming edit - allowTrimLine = false; - break; - } + if (allowTrimLine) { + editOperations.push({ + identifier: null, + range: new Range(trimLineNumber, 1, trimLineNumber, maxLineColumn), + text: null, + forceMoveMarkers: false, + isAutoWhitespaceEdit: false + }); + } - if (allowTrimLine) { - editOperations.push({ - identifier: null, - range: new Range(trimLineNumber, 1, trimLineNumber, maxLineColumn), - text: null, - forceMoveMarkers: false, - isAutoWhitespaceEdit: false - }); } - } this._trimAutoWhitespaceLines = null; diff --git a/src/vs/editor/test/common/controller/cursor.test.ts b/src/vs/editor/test/common/controller/cursor.test.ts index dc710cbf8007f4e142183a48505f71b64e3298e4..190224e638d6d604afdae988aa322cfcc434f312 100644 --- a/src/vs/editor/test/common/controller/cursor.test.ts +++ b/src/vs/editor/test/common/controller/cursor.test.ts @@ -10,7 +10,11 @@ import {EditOperation} from 'vs/editor/common/core/editOperation'; import {Position} from 'vs/editor/common/core/position'; import {Range} from 'vs/editor/common/core/range'; import {Selection} from 'vs/editor/common/core/selection'; -import {EndOfLinePreference, EventType, Handler, IPosition, ISelection, IEditorOptions, DefaultEndOfLine, ITextModelCreationOptions} from 'vs/editor/common/editorCommon'; +import { + EndOfLinePreference, EventType, Handler, IPosition, ISelection, IEditorOptions, + DefaultEndOfLine, ITextModelCreationOptions, ICommand, + ITokenizedModel, IEditOperationBuilder, ICursorStateComputerData +} from 'vs/editor/common/editorCommon'; import {Model} from 'vs/editor/common/model/model'; import {IMode, IRichEditSupport, IndentAction} from 'vs/editor/common/modes'; import {RichEditSupport} from 'vs/editor/common/modes/supports/richEditSupport'; @@ -2184,6 +2188,49 @@ suite('Editor Controller - Cursor Configuration', () => { }); }); + test('issue #6862: Editor removes auto inserted indentation when formatting on type', () => { + usingCursor({ + text: [ + 'function foo (params: string) {}' + ], + modelOpts: { + insertSpaces: true, + tabSize: 4, + detectIndentation: false, + defaultEOL: DefaultEndOfLine.LF, + trimAutoWhitespace: true + }, + mode: new OnEnterMode(IndentAction.IndentOutdent), + }, (model, cursor) => { + + moveTo(cursor, 1, 32); + cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + assert.equal(model.getLineContent(1), 'function foo (params: string) {'); + assert.equal(model.getLineContent(2), ' '); + assert.equal(model.getLineContent(3), '}'); + + class TestCommand implements ICommand { + + private _selectionId: string = null; + + public getEditOperations(model: ITokenizedModel, builder: IEditOperationBuilder): void { + builder.addEditOperation(new Range(1, 13, 1, 14), ''); + this._selectionId = builder.trackSelection(cursor.getSelection()); + } + + public computeCursorState(model: ITokenizedModel, helper: ICursorStateComputerData): Selection { + return helper.getTrackedSelection(this._selectionId); + } + + } + + cursor.trigger('autoFormat', Handler.ExecuteCommand, new TestCommand()); + assert.equal(model.getLineContent(1), 'function foo(params: string) {'); + assert.equal(model.getLineContent(2), ' '); + assert.equal(model.getLineContent(3), '}'); + }); + }); + test('removeAutoWhitespace on: removes only whitespace the cursor added 2', () => { usingCursor({ text: [