diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index a3b7e37958e..b61d0ada8be 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -303,8 +303,8 @@ export function getLeadingWhitespace(str: string): string { * Returns last index of the string that is not whitespace. * If string is empty or contains only whitespaces, returns -1 */ -export function lastNonWhitespaceIndex(str: string): number { - for (let i = str.length - 1; i >= 0; i--) { +export function lastNonWhitespaceIndex(str: string, startIndex: number = str.length - 1): number { + for (let i = startIndex; i >= 0; i--) { if (str.charAt(i) !== ' ' && str.charAt(i) !== '\t') { return i; } diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index 08d3d66cad0..cbe7c027b1f 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -157,4 +157,15 @@ suite('Strings', () => { assert.strictEqual(strings.repeat(' ', 0), ''); assert.strictEqual(strings.repeat('abc', 2), 'abcabc'); }); + + test('lastNonWhitespaceIndex', () => { + assert.strictEqual(strings.lastNonWhitespaceIndex('abc \t \t '), 2); + assert.strictEqual(strings.lastNonWhitespaceIndex('abc'), 2); + assert.strictEqual(strings.lastNonWhitespaceIndex('abc\t'), 2); + assert.strictEqual(strings.lastNonWhitespaceIndex('abc '), 2); + assert.strictEqual(strings.lastNonWhitespaceIndex('abc \t \t '), 2); + assert.strictEqual(strings.lastNonWhitespaceIndex('abc \t \t abc \t \t '), 11); + assert.strictEqual(strings.lastNonWhitespaceIndex('abc \t \t abc \t \t ', 8), 2); + assert.strictEqual(strings.lastNonWhitespaceIndex(' \t \t '), -1); + }); }); \ No newline at end of file diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 08b45f8198d..48d1bc50cb0 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -107,6 +107,8 @@ export class InternalEditorOptions implements editorCommon.IInternalEditorOption folding: boolean; renderWhitespace: boolean; indentGuides: boolean; + useTabStops: boolean; + trimAutoWhitespace: boolean; layoutInfo: editorCommon.IEditorLayoutInfo; fontInfo: editorCommon.FontInfo; editorClassName: string; @@ -173,6 +175,8 @@ export class InternalEditorOptions implements editorCommon.IInternalEditorOption this.folding = Boolean(input.folding); this.renderWhitespace = Boolean(input.renderWhitespace); this.indentGuides = Boolean(input.indentGuides); + this.useTabStops = Boolean(input.useTabStops); + this.trimAutoWhitespace = Boolean(input.trimAutoWhitespace); this.layoutInfo = { width: Number(input.layoutInfo.width)|0, height: Number(input.layoutInfo.height)|0, @@ -359,6 +363,8 @@ class InternalEditorOptionsHelper { folding: toBoolean(opts.folding), renderWhitespace: toBoolean(opts.renderWhitespace), indentGuides: toBoolean(opts.indentGuides), + useTabStops: toBoolean(opts.useTabStops), + trimAutoWhitespace: toBoolean(opts.trimAutoWhitespace), layoutInfo: layoutInfo, fontInfo: fontInfo, @@ -443,9 +449,11 @@ class InternalEditorOptionsHelper { folding: (prevOpts.folding !== newOpts.folding), renderWhitespace: (prevOpts.renderWhitespace !== newOpts.renderWhitespace), indentGuides: (prevOpts.indentGuides !== newOpts.indentGuides), + useTabStops: (prevOpts.useTabStops !== newOpts.useTabStops), + trimAutoWhitespace: (prevOpts.trimAutoWhitespace !== newOpts.trimAutoWhitespace), layoutInfo: (!EditorLayoutProvider.layoutEqual(prevOpts.layoutInfo, newOpts.layoutInfo)), - fontInfo: (!prevOpts.fontInfo.equals(newOpts.fontInfo)), + fontInfo: (!prevOpts.fontInfo.equals(newOpts.fontInfo)), editorClassName: (prevOpts.editorClassName !== newOpts.editorClassName), wrappingInfo: (!this._wrappingInfoEqual(prevOpts.wrappingInfo, newOpts.wrappingInfo)), observedOuterWidth: (prevOpts.observedOuterWidth !== newOpts.observedOuterWidth), @@ -926,6 +934,16 @@ let editorConfiguration:IConfigurationNode = { 'default': DefaultConfig.editor.folding, 'description': nls.localize('folding', "Controls whether the editor has code folding enabled") }, + 'editor.useTabStops' : { + 'type': 'boolean', + 'default': DefaultConfig.editor.useTabStops, + 'description': nls.localize('useTabStops', "Inserting and deleting whitespace follows tab stops") + }, + 'editor.trimAutoWhitespace' : { + 'type': 'boolean', + 'default': DefaultConfig.editor.trimAutoWhitespace, + 'description': nls.localize('trimAutoWhitespace', "Remove trailing auto inserted whitespace") + }, 'diffEditor.renderSideBySide' : { 'type': 'boolean', 'default': true, diff --git a/src/vs/editor/common/config/defaultConfig.ts b/src/vs/editor/common/config/defaultConfig.ts index 5655df23b49..462a09b0e80 100644 --- a/src/vs/editor/common/config/defaultConfig.ts +++ b/src/vs/editor/common/config/defaultConfig.ts @@ -90,6 +90,8 @@ class ConfigClass implements IConfiguration { folding: true, renderWhitespace: false, indentGuides: false, + useTabStops: true, + trimAutoWhitespace: true, fontFamily: ( platform.isMacintosh ? DEFAULT_MAC_FONT_FAMILY : (platform.isLinux ? DEFAULT_LINUX_FONT_FAMILY : DEFAULT_WINDOWS_FONT_FAMILY) diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 4bff679cfa8..c3476462fee 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -40,6 +40,7 @@ interface IMultipleCursorOperationContext { hasExecutedCommands: boolean; isCursorUndo: boolean; executeCommands: editorCommon.ICommand[]; + isAutoWhitespaceCommand: boolean[]; postOperationRunnables: IPostOperationRunnable[]; requestScrollDeltaLines: number; setColumnSelectToLineNumber: number; @@ -313,6 +314,7 @@ export class Cursor extends EventEmitter { eventSource: eventSource, eventData: eventData, executeCommands: [], + isAutoWhitespaceCommand: [], hasExecutedCommands: false, isCursorUndo: false, postOperationRunnables: [], @@ -440,7 +442,7 @@ export class Cursor extends EventEmitter { this._columnSelectToLineNumber = ctx.setColumnSelectToLineNumber; this._columnSelectToVisualColumn = ctx.setColumnSelectToVisualColumn; - ctx.hasExecutedCommands = this._internalExecuteCommands(ctx.executeCommands, ctx.postOperationRunnables) || ctx.hasExecutedCommands; + ctx.hasExecutedCommands = this._internalExecuteCommands(ctx.executeCommands, ctx.isAutoWhitespaceCommand, ctx.postOperationRunnables) || ctx.hasExecutedCommands; ctx.executeCommands = []; if (ctx.shouldPushStackElementAfter) { @@ -480,7 +482,7 @@ export class Cursor extends EventEmitter { return true; } - private _getEditOperationsFromCommand(ctx: IExecContext, majorIdentifier: number, command: editorCommon.ICommand): ICommandData { + private _getEditOperationsFromCommand(ctx: IExecContext, majorIdentifier: number, command: editorCommon.ICommand, isAutoWhitespaceCommand:boolean): ICommandData { // This method acts as a transaction, if the command fails // everything it has done is ignored var operations: editorCommon.IIdentifiedSingleEditOperation[] = [], @@ -498,7 +500,8 @@ export class Cursor extends EventEmitter { }, range: selection, text: text, - forceMoveMarkers: false + forceMoveMarkers: false, + isAutoWhitespaceEdit: isAutoWhitespaceCommand }); }; @@ -560,7 +563,7 @@ export class Cursor extends EventEmitter { }; } - private _getEditOperations(ctx: IExecContext, commands: editorCommon.ICommand[]): ICommandsData { + private _getEditOperations(ctx: IExecContext, commands: editorCommon.ICommand[], isAutoWhitespaceCommand:boolean[]): ICommandsData { var oneResult: ICommandData; var operations: editorCommon.IIdentifiedSingleEditOperation[] = []; var hadTrackedRanges: boolean[] = []; @@ -568,7 +571,7 @@ export class Cursor extends EventEmitter { for (var i = 0; i < commands.length; i++) { if (commands[i]) { - oneResult = this._getEditOperationsFromCommand(ctx, i, commands[i]); + oneResult = this._getEditOperationsFromCommand(ctx, i, commands[i], isAutoWhitespaceCommand[i]); operations = operations.concat(oneResult.operations); hadTrackedRanges[i] = oneResult.hadTrackedRange; anyoneHadTrackedRange = anyoneHadTrackedRange || hadTrackedRanges[i]; @@ -634,7 +637,7 @@ export class Cursor extends EventEmitter { return loserCursorsMap; } - private _collapseDeleteCommands(rawCmds: editorCommon.ICommand[], postOperationRunnables: IPostOperationRunnable[]): boolean { + private _collapseDeleteCommands(rawCmds: editorCommon.ICommand[], isAutoWhitespaceCommand:boolean[], postOperationRunnables: IPostOperationRunnable[]): boolean { if (rawCmds.length === 1) { return ; } @@ -690,15 +693,15 @@ export class Cursor extends EventEmitter { } } - private _internalExecuteCommands(commands: editorCommon.ICommand[], postOperationRunnables: IPostOperationRunnable[]): boolean { + private _internalExecuteCommands(commands: editorCommon.ICommand[], isAutoWhitespaceCommand: boolean[], postOperationRunnables: IPostOperationRunnable[]): boolean { var ctx:IExecContext = { selectionStartMarkers: [], positionMarkers: [] }; - this._collapseDeleteCommands(commands, postOperationRunnables); + this._collapseDeleteCommands(commands, isAutoWhitespaceCommand, postOperationRunnables); - var r = this._innerExecuteCommands(ctx, commands, postOperationRunnables); + var r = this._innerExecuteCommands(ctx, commands, isAutoWhitespaceCommand, postOperationRunnables); for (var i = 0; i < ctx.selectionStartMarkers.length; i++) { this.model._removeMarker(ctx.selectionStartMarkers[i]); this.model._removeMarker(ctx.positionMarkers[i]); @@ -719,7 +722,7 @@ export class Cursor extends EventEmitter { return true; } - private _innerExecuteCommands(ctx: IExecContext, commands: editorCommon.ICommand[], postOperationRunnables: IPostOperationRunnable[]): boolean { + private _innerExecuteCommands(ctx: IExecContext, commands: editorCommon.ICommand[], isAutoWhitespaceCommand: boolean[], postOperationRunnables: IPostOperationRunnable[]): boolean { if (this.configuration.editor.readOnly) { return false; @@ -731,7 +734,7 @@ export class Cursor extends EventEmitter { var selectionsBefore = this.cursors.getSelections(); - var commandsData = this._getEditOperations(ctx, commands); + var commandsData = this._getEditOperations(ctx, commands, isAutoWhitespaceCommand); if (commandsData.operations.length === 0 && !commandsData.anyoneHadTrackedRange) { return false; } @@ -771,6 +774,10 @@ export class Cursor extends EventEmitter { } for (var i = 0; i < inverseEditOperations.length; i++) { var op = inverseEditOperations[i]; + if (!op.identifier) { + // perhaps auto whitespace trim edits + continue; + } groupedInverseEditOperations[op.identifier.major].push(op); } var minorBasedSorter = (a:editorCommon.IIdentifiedSingleEditOperation, b:editorCommon.IIdentifiedSingleEditOperation) => { @@ -1080,6 +1087,7 @@ export class Cursor extends EventEmitter { shouldRevealVerticalInCenter: false, shouldRevealHorizontal: true, executeCommand: null, + isAutoWhitespaceCommand: false, postOperationRunnable: null, shouldPushStackElementBefore: false, shouldPushStackElementAfter: false, @@ -1100,6 +1108,7 @@ export class Cursor extends EventEmitter { ctx.shouldPushStackElementAfter = ctx.shouldPushStackElementAfter || context.shouldPushStackElementAfter; ctx.executeCommands[i] = context.executeCommand; + ctx.isAutoWhitespaceCommand[i] = context.isAutoWhitespaceCommand; ctx.postOperationRunnables[i] = context.postOperationRunnable; } diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index 323a769782a..5b293bf9427 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -29,6 +29,7 @@ export interface IOneCursorOperationContext { shouldPushStackElementBefore: boolean; shouldPushStackElementAfter: boolean; executeCommand: editorCommon.ICommand; + isAutoWhitespaceCommand: boolean; postOperationRunnable: IPostOperationRunnable; requestScrollDeltaLines: number; } @@ -145,7 +146,13 @@ export class OneCursor { private _selEndMarker: string; private _selDirection: editorCommon.SelectionDirection; - constructor(editorId: number, model: editorCommon.IModel, configuration: editorCommon.IConfiguration, modeConfiguration: IModeConfiguration, viewModelHelper:IViewModelHelper) { + constructor( + editorId: number, + model: editorCommon.IModel, + configuration: editorCommon.IConfiguration, + modeConfiguration: IModeConfiguration, + viewModelHelper:IViewModelHelper + ) { this.editorId = editorId; this.model = model; this.configuration = configuration; @@ -527,6 +534,9 @@ export class OneCursor { public getVisibleColumnFromColumn(lineNumber:number, column:number): number { return this.helper.visibleColumnFromColumn(this.model, lineNumber, column); } + public getColumnFromVisibleColumn(lineNumber:number, column:number): number { + return this.helper.columnFromVisibleColumn(this.model, lineNumber, column); + } public getViewVisibleColumnFromColumn(viewLineNumber:number, viewColumn:number): number { return this.helper.visibleColumnFromColumn(this.viewModelHelper.viewModel, viewLineNumber, viewColumn); } @@ -1185,6 +1195,7 @@ export class OneCursorOp { var enterAction = r.enterAction; var indentation = r.indentation; + ctx.isAutoWhitespaceCommand = true; if (enterAction.indentAction === IndentAction.None) { // Nothing special this.actualType(cursor, '\n' + cursor.model.normalizeIndentation(indentation + enterAction.appendText), keepPosition, ctx, range); @@ -1500,6 +1511,8 @@ export class OneCursorOp { if (selection.isEmpty()) { + ctx.isAutoWhitespaceCommand = true; + let typeText = ''; if (cursor.model.getLineMaxColumn(selection.startLineNumber) === 1) { @@ -1626,13 +1639,34 @@ export class OneCursorOp { if (deleteSelection.isEmpty()) { var position = cursor.getPosition(); - var leftOfPosition = cursor.getLeftOfPosition(position.lineNumber, position.column); - deleteSelection = new Range( - leftOfPosition.lineNumber, - leftOfPosition.column, - position.lineNumber, - position.column - ); + + if (cursor.configuration.editor.useTabStops && position.column > 1) { + let lineContent = cursor.getLineContent(position.lineNumber); + + let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); + let lastIndentationColumn = ( + firstNonWhitespaceIndex === -1 + ? /* entire string is whitespace */lineContent.length + 1 + : firstNonWhitespaceIndex + 1 + ); + + if (position.column <= lastIndentationColumn) { + let fromVisibleColumn = cursor.getVisibleColumnFromColumn(position.lineNumber, position.column); + let toVisibleColumn = CursorMoveHelper.prevTabColumn(fromVisibleColumn, cursor.model.getOptions().tabSize); + let toColumn = cursor.getColumnFromVisibleColumn(position.lineNumber, toVisibleColumn); + deleteSelection = new Range(position.lineNumber, toColumn, position.lineNumber, position.column); + } else { + deleteSelection = new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column); + } + } else { + var leftOfPosition = cursor.getLeftOfPosition(position.lineNumber, position.column); + deleteSelection = new Range( + leftOfPosition.lineNumber, + leftOfPosition.column, + position.lineNumber, + position.column + ); + } } if (deleteSelection.isEmpty()) { @@ -1648,21 +1682,11 @@ export class OneCursorOp { return true; } - private static _findLastNonWhitespaceChar(str:string, startIndex:number): number { - for (let chIndex = startIndex; chIndex >= 0; chIndex--) { - let ch = str.charAt(chIndex); - if (ch !== ' ' && ch !== '\t') { - return chIndex; - } - } - return -1; - } - private static deleteWordLeftWhitespace(cursor:OneCursor, ctx: IOneCursorOperationContext): boolean { let position = cursor.getPosition(); let lineContent = cursor.getLineContent(position.lineNumber); let startIndex = position.column - 2; - let lastNonWhitespace = this._findLastNonWhitespaceChar(lineContent, startIndex); + let lastNonWhitespace = strings.lastNonWhitespaceIndex(lineContent, startIndex); if (lastNonWhitespace + 1 < startIndex) { // bingo ctx.executeCommand = new ReplaceCommand(new Range(position.lineNumber, lastNonWhitespace + 2, position.lineNumber, position.column), ''); @@ -1982,6 +2006,10 @@ class CursorHelper { return this.moveHelper.visibleColumnFromColumn(model, lineNumber, column); } + public columnFromVisibleColumn(model:ICursorMoveHelperModel, lineNumber:number, column:number): number { + return this.moveHelper.columnFromVisibleColumn(model, lineNumber, column); + } + private _createWord(lineContent: string, wordType:WordType, start: number, end: number): IFindWordResult { // console.log('WORD ==> ' + start + ' => ' + end + ':::: <<<' + lineContent.substring(start, end) + '>>>'); return { start: start, end: end, wordType: wordType }; diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index d9247bb8298..c58a28dfafa 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -513,6 +513,14 @@ export interface IEditorOptions { * Defaults to true. */ indentGuides?: boolean; + /** + * Inserting and deleting whitespace follows tab stops. + */ + useTabStops?: boolean; + /** + * Remove trailing auto inserted whitespace. + */ + trimAutoWhitespace?: boolean; /** * The font family */ @@ -624,6 +632,8 @@ export interface IInternalEditorOptions { folding: boolean; renderWhitespace: boolean; indentGuides: boolean; + useTabStops: boolean; + trimAutoWhitespace: boolean; // ---- Options that are computed @@ -702,6 +712,8 @@ export interface IConfigurationChangedEvent { folding: boolean; renderWhitespace: boolean; indentGuides: boolean; + useTabStops: boolean; + trimAutoWhitespace: boolean; // ---- Options that are computed layoutInfo: boolean; @@ -1121,6 +1133,11 @@ export interface IIdentifiedSingleEditOperation { * i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved. */ forceMoveMarkers: boolean; + /** + * This indicates that this operation is inserting automatic whitespace + * that can be removed on next model edit operation if `config.trimAutoWhitespace` is true. + */ + isAutoWhitespaceEdit?: boolean; } /** @@ -1168,23 +1185,27 @@ export interface ITextModelResolvedOptions { tabSize: number; insertSpaces: boolean; defaultEOL: DefaultEndOfLine; + trimAutoWhitespace: boolean; } export interface ITextModelCreationOptions { tabSize: number; insertSpaces: boolean; detectIndentation: boolean; + trimAutoWhitespace: boolean; defaultEOL: DefaultEndOfLine; } export interface ITextModelUpdateOptions { tabSize?: number; insertSpaces?: boolean; + trimAutoWhitespace?: boolean; } export interface IModelOptionsChangedEvent { tabSize: boolean; insertSpaces: boolean; + trimAutoWhitespace: boolean; } /** diff --git a/src/vs/editor/common/model/editableTextModel.ts b/src/vs/editor/common/model/editableTextModel.ts index 96717faef7d..0c8d582a9a6 100644 --- a/src/vs/editor/common/model/editableTextModel.ts +++ b/src/vs/editor/common/model/editableTextModel.ts @@ -11,6 +11,7 @@ import {EditStack} from 'vs/editor/common/model/editStack'; import {ILineEdit, ILineMarker, ModelLine} from 'vs/editor/common/model/modelLine'; import {DeferredEventsBuilder, TextModelWithDecorations} from 'vs/editor/common/model/textModelWithDecorations'; import {IMode} from 'vs/editor/common/modes'; +import * as strings from 'vs/base/common/strings'; export interface IValidatedEditOperation { sortIndex: number; @@ -19,6 +20,7 @@ export interface IValidatedEditOperation { rangeLength: number; lines: string[]; forceMoveMarkers: boolean; + isAutoWhitespaceEdit: boolean; } interface IIdentifiedLineEdit extends ILineEdit{ @@ -37,6 +39,8 @@ export class EditableTextModel extends TextModelWithDecorations implements edito private _hasEditableRange:boolean; private _editableRangeId:string; + private _trimAutoWhitespaceLines: number[]; + constructor(allowedEventTypes:string[], rawText:editorCommon.IRawText, modeOrPromise:IMode|TPromise) { allowedEventTypes.push(editorCommon.EventType.ModelContentChanged); allowedEventTypes.push(editorCommon.EventType.ModelContentChanged2); @@ -49,6 +53,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito this._hasEditableRange = false; this._editableRangeId = null; + this._trimAutoWhitespaceLines = null; } public dispose(): void { @@ -63,6 +68,7 @@ export class EditableTextModel extends TextModelWithDecorations implements edito this._commandManager = new EditStack(this); this._hasEditableRange = false; this._editableRangeId = null; + this._trimAutoWhitespaceLines = null; } public pushStackElement(): void { @@ -71,6 +77,61 @@ export class EditableTextModel extends TextModelWithDecorations implements edito public pushEditOperations(beforeCursorState:editorCommon.IEditorSelection[], editOperations:editorCommon.IIdentifiedSingleEditOperation[], cursorStateComputer:editorCommon.ICursorStateComputer): editorCommon.IEditorSelection[] { return this.deferredEmit(() => { + if (this._options.trimAutoWhitespace && this._trimAutoWhitespaceLines) { + // Go through each saved line number and insert a trim whitespace edit + // if it is safe to do so (no conflicts with other edits). + + let incomingEdits = editOperations.map((op) => { + return { + range: this.validateRange(op.range), + text: op.text + }; + }); + + 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[i].range; + let editText = incomingEdits[i].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; + } + + if (allowTrimLine) { + editOperations.push({ + identifier: null, + range: new Range(trimLineNumber, 1, trimLineNumber, maxLineColumn), + text: null, + forceMoveMarkers: false, + isAutoWhitespaceEdit: false + }); + } + + } + + this._trimAutoWhitespaceLines = null; + } return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer); }); } @@ -145,7 +206,8 @@ export class EditableTextModel extends TextModelWithDecorations implements edito range: entireEditRange, rangeLength: this.getValueLengthInRange(entireEditRange), lines: result.join('').split('\n'), - forceMoveMarkers: forceMoveMarkers + forceMoveMarkers: forceMoveMarkers, + isAutoWhitespaceEdit: false }; } @@ -180,7 +242,8 @@ export class EditableTextModel extends TextModelWithDecorations implements edito range: validatedRange, rangeLength: this.getValueLengthInRange(validatedRange), lines: op.text ? op.text.split(/\r\n|\r|\n/) : null, - forceMoveMarkers: op.forceMoveMarkers + forceMoveMarkers: op.forceMoveMarkers, + isAutoWhitespaceEdit: op.isAutoWhitespaceEdit || false }; } @@ -212,17 +275,61 @@ export class EditableTextModel extends TextModelWithDecorations implements edito // Delta encode operations let reverseRanges = EditableTextModel._getInverseEditRanges(operations); let reverseOperations: editorCommon.IIdentifiedSingleEditOperation[] = []; + + let newTrimAutoWhitespaceCandidates: { lineNumber:number,oldContent:string }[] = []; + for (let i = 0; i < operations.length; i++) { + let op = operations[i]; + let reverseRange = reverseRanges[i]; + reverseOperations[i] = { - identifier: operations[i].identifier, - range: reverseRanges[i], - text: this.getValueInRange(operations[i].range), - forceMoveMarkers: operations[i].forceMoveMarkers + identifier: op.identifier, + range: reverseRange, + text: this.getValueInRange(op.range), + forceMoveMarkers: op.forceMoveMarkers }; + + if (this._options.trimAutoWhitespace && op.isAutoWhitespaceEdit && op.range.isEmpty()) { + // Record already the future line numbers that might be auto whitespace removal candidates on next edit + for (let lineNumber = reverseRange.startLineNumber; lineNumber <= reverseRange.endLineNumber; lineNumber++) { + let currentLineContent = ''; + if (lineNumber === reverseRange.startLineNumber) { + currentLineContent = this.getLineContent(op.range.startLineNumber); + if (strings.firstNonWhitespaceIndex(currentLineContent) !== -1) { + continue; + } + } + newTrimAutoWhitespaceCandidates.push({ lineNumber:lineNumber, oldContent:currentLineContent }); + } + } } this._applyEdits(operations); + this._trimAutoWhitespaceLines = null; + if (this._options.trimAutoWhitespace && newTrimAutoWhitespaceCandidates.length > 0) { + // sort line numbers auto whitespace removal candidates for next edit descending + newTrimAutoWhitespaceCandidates.sort((a,b) => b.lineNumber - a.lineNumber); + + this._trimAutoWhitespaceLines = []; + for (let i = 0, len = newTrimAutoWhitespaceCandidates.length; i < len; i++) { + let lineNumber = newTrimAutoWhitespaceCandidates[i].lineNumber; + if (i > 0 && newTrimAutoWhitespaceCandidates[i - 1].lineNumber === lineNumber) { + // Do not have the same line number twice + continue; + } + + let prevContent = newTrimAutoWhitespaceCandidates[i].oldContent; + let lineContent = this.getLineContent(lineNumber); + + if (lineContent.length === 0 || lineContent === prevContent || strings.firstNonWhitespaceIndex(lineContent) !== -1) { + continue; + } + + this._trimAutoWhitespaceLines.push(lineNumber); + } + } + return reverseOperations; } @@ -311,7 +418,8 @@ export class EditableTextModel extends TextModelWithDecorations implements edito lineEditsQueue.reverse(); // `lineEditsQueue` now contains edits from smaller (line number,column) to larger (line number,column) - let currentLineNumber = lineEditsQueue[0].lineNumber, currentLineNumberStart = 0; + let currentLineNumber = lineEditsQueue[0].lineNumber; + let currentLineNumberStart = 0; for (let i = 1, len = lineEditsQueue.length; i < len; i++) { let lineNumber = lineEditsQueue[i].lineNumber; diff --git a/src/vs/editor/common/model/mirrorModel.ts b/src/vs/editor/common/model/mirrorModel.ts index 8685a2c3d4b..d35b5b30bab 100644 --- a/src/vs/editor/common/model/mirrorModel.ts +++ b/src/vs/editor/common/model/mirrorModel.ts @@ -222,7 +222,8 @@ export class MirrorModelEmbedded extends AbstractMirrorModel implements editorCo tabSize: actualModelOptions.tabSize, insertSpaces: actualModelOptions.insertSpaces, detectIndentation: false, - defaultEOL: actualModelOptions.defaultEOL + defaultEOL: actualModelOptions.defaultEOL, + trimAutoWhitespace: actualModelOptions.trimAutoWhitespace }); } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 44af80dd2b4..ffe1b0431ea 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -11,7 +11,7 @@ import {Range} from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import {ModelLine} from 'vs/editor/common/model/modelLine'; import {guessIndentation} from 'vs/editor/common/model/indentationGuesser'; -import {DEFAULT_INDENTATION} from 'vs/editor/common/config/defaultConfig'; +import {DEFAULT_INDENTATION, DefaultConfig} from 'vs/editor/common/config/defaultConfig'; var LIMIT_FIND_COUNT = 999; @@ -21,6 +21,7 @@ export class TextModel extends OrderGuaranteeEventEmitter implements editorCommo tabSize: DEFAULT_INDENTATION.tabSize, insertSpaces: DEFAULT_INDENTATION.insertSpaces, detectIndentation: false, + trimAutoWhitespace: DefaultConfig.editor.trimAutoWhitespace, defaultEOL: editorCommon.DefaultEndOfLine.LF }; @@ -56,7 +57,8 @@ export class TextModel extends OrderGuaranteeEventEmitter implements editorCommo let somethingChanged = false; let changed:editorCommon.IModelOptionsChangedEvent = { tabSize: false, - insertSpaces: false + insertSpaces: false, + trimAutoWhitespace: false }; if (typeof newOpts.insertSpaces !== 'undefined') { @@ -73,6 +75,13 @@ export class TextModel extends OrderGuaranteeEventEmitter implements editorCommo this._options.tabSize = newOpts.tabSize; } } + if (typeof newOpts.trimAutoWhitespace !== 'undefined') { + if (this._options.trimAutoWhitespace !== newOpts.trimAutoWhitespace) { + somethingChanged = true; + changed.trimAutoWhitespace = true; + this._options.trimAutoWhitespace = newOpts.trimAutoWhitespace; + } + } if (somethingChanged) { this.emit(editorCommon.EventType.ModelOptionsChanged, changed); @@ -244,6 +253,7 @@ export class TextModel extends OrderGuaranteeEventEmitter implements editorCommo rawText = TextModel.toRawText(value, { tabSize: this._options.tabSize, insertSpaces: this._options.insertSpaces, + trimAutoWhitespace: this._options.trimAutoWhitespace, detectIndentation: false, defaultEOL: this._options.defaultEOL }); @@ -624,12 +634,14 @@ export class TextModel extends OrderGuaranteeEventEmitter implements editorCommo resolvedOpts = { tabSize: guessedIndentation.tabSize, insertSpaces: guessedIndentation.insertSpaces, + trimAutoWhitespace: opts.trimAutoWhitespace, defaultEOL: opts.defaultEOL }; } else { resolvedOpts = { tabSize: opts.tabSize, insertSpaces: opts.insertSpaces, + trimAutoWhitespace: opts.trimAutoWhitespace, defaultEOL: opts.defaultEOL }; } @@ -832,6 +844,7 @@ export class RawText { return TextModel.toRawText(rawText, { tabSize: opts.tabSize, insertSpaces: opts.insertSpaces, + trimAutoWhitespace: opts.trimAutoWhitespace, detectIndentation: false, defaultEOL: opts.defaultEOL }); diff --git a/src/vs/editor/common/model/textModelWithTokens.ts b/src/vs/editor/common/model/textModelWithTokens.ts index 1c173141585..a443fe81d6d 100644 --- a/src/vs/editor/common/model/textModelWithTokens.ts +++ b/src/vs/editor/common/model/textModelWithTokens.ts @@ -443,7 +443,8 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke tabSize: this._options.tabSize, insertSpaces: this._options.insertSpaces, detectIndentation: false, - defaultEOL: this._options.defaultEOL + defaultEOL: this._options.defaultEOL, + trimAutoWhitespace: this._options.trimAutoWhitespace }); } this.setValueFromRawText(rawText, newModeOrPromise); diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 5d8c79b979b..ffee163b5fa 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -26,7 +26,7 @@ import {IModelService} from 'vs/editor/common/services/modelService'; import {IResourceService} from 'vs/editor/common/services/resourceService'; import * as platform from 'vs/base/common/platform'; import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; -import {DEFAULT_INDENTATION} from 'vs/editor/common/config/defaultConfig'; +import {DEFAULT_INDENTATION, DefaultConfig} from 'vs/editor/common/config/defaultConfig'; import {IMessageService} from 'vs/platform/message/common/message'; export interface IRawModelData { @@ -180,6 +180,7 @@ interface IRawConfig { tabSize?: any; insertSpaces?: any; detectIndentation?: any; + trimAutoWhitespace?: any; }; } @@ -219,7 +220,8 @@ export class ModelServiceImpl implements IModelService { tabSize: DEFAULT_INDENTATION.tabSize, insertSpaces: DEFAULT_INDENTATION.insertSpaces, detectIndentation: DEFAULT_INDENTATION.detectIndentation, - defaultEOL: (platform.isLinux || platform.isMacintosh) ? editorCommon.DefaultEndOfLine.LF : editorCommon.DefaultEndOfLine.CRLF + defaultEOL: (platform.isLinux || platform.isMacintosh) ? editorCommon.DefaultEndOfLine.LF : editorCommon.DefaultEndOfLine.CRLF, + trimAutoWhitespace: DefaultConfig.editor.trimAutoWhitespace }; this._threadService = threadService; this._markerService = markerService; @@ -240,7 +242,6 @@ export class ModelServiceImpl implements IModelService { } let readConfig = (config: IRawConfig) => { - const eol = config.files && config.files.eol; let shouldShowMigrationMessage = false; @@ -260,12 +261,18 @@ export class ModelServiceImpl implements IModelService { } let newDefaultEOL = this._modelCreationOptions.defaultEOL; + const eol = config.files && config.files.eol; if (eol === '\r\n') { newDefaultEOL = editorCommon.DefaultEndOfLine.CRLF; } else if (eol === '\n') { newDefaultEOL = editorCommon.DefaultEndOfLine.LF; } + let trimAutoWhitespace = this._modelCreationOptions.trimAutoWhitespace; + if (config.editor && typeof config.editor.trimAutoWhitespace !== 'undefined') { + trimAutoWhitespace = (config.editor.trimAutoWhitespace === 'false' ? false : Boolean(config.editor.trimAutoWhitespace)); + } + let detectIndentation = DEFAULT_INDENTATION.detectIndentation; if (config.editor && typeof config.editor.detectIndentation !== 'undefined') { detectIndentation = (config.editor.detectIndentation === 'false' ? false : Boolean(config.editor.detectIndentation)); @@ -275,7 +282,8 @@ export class ModelServiceImpl implements IModelService { tabSize: tabSize, insertSpaces: insertSpaces, detectIndentation: detectIndentation, - defaultEOL: newDefaultEOL + defaultEOL: newDefaultEOL, + trimAutoWhitespace: trimAutoWhitespace }); @@ -300,6 +308,7 @@ export class ModelServiceImpl implements IModelService { (this._modelCreationOptions.detectIndentation === newOpts.detectIndentation) && (this._modelCreationOptions.insertSpaces === newOpts.insertSpaces) && (this._modelCreationOptions.tabSize === newOpts.tabSize) + && (this._modelCreationOptions.trimAutoWhitespace === newOpts.trimAutoWhitespace) ) { // Same indent opts, no need to touch created models this._modelCreationOptions = newOpts; @@ -315,10 +324,14 @@ export class ModelServiceImpl implements IModelService { if (this._modelCreationOptions.detectIndentation) { modelData.model.detectIndentation(this._modelCreationOptions.insertSpaces, this._modelCreationOptions.tabSize); + modelData.model.updateOptions({ + trimAutoWhitespace: this._modelCreationOptions.trimAutoWhitespace + }); } else { modelData.model.updateOptions({ insertSpaces: this._modelCreationOptions.insertSpaces, - tabSize: this._modelCreationOptions.tabSize + tabSize: this._modelCreationOptions.tabSize, + trimAutoWhitespace: this._modelCreationOptions.trimAutoWhitespace }); } } diff --git a/src/vs/editor/test/common/controller/cursor.test.ts b/src/vs/editor/test/common/controller/cursor.test.ts index 64ee25a6749..895777ba96b 100644 --- a/src/vs/editor/test/common/controller/cursor.test.ts +++ b/src/vs/editor/test/common/controller/cursor.test.ts @@ -1111,7 +1111,8 @@ suite('Editor Controller - Regression tests', () => { defaultEOL: DefaultEndOfLine.LF, detectIndentation: false, insertSpaces: false, - tabSize: 4 + tabSize: 4, + trimAutoWhitespace: false } }, (model, cursor) => { cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); @@ -1198,7 +1199,8 @@ suite('Editor Controller - Regression tests', () => { defaultEOL: DefaultEndOfLine.LF, detectIndentation: false, insertSpaces: false, - tabSize: 4 + tabSize: 4, + trimAutoWhitespace: true }, mode: new OnEnterMode(IndentAction.Indent), }, (model, cursor) => { @@ -1219,7 +1221,8 @@ suite('Editor Controller - Regression tests', () => { defaultEOL: DefaultEndOfLine.LF, detectIndentation: false, insertSpaces: false, - tabSize: 4 + tabSize: 4, + trimAutoWhitespace: true }, }, (model, cursor) => { moveTo(cursor, 1, 2, false); @@ -1240,7 +1243,7 @@ suite('Editor Controller - Regression tests', () => { ' function baz() {' ], mode: new OnEnterMode(IndentAction.IndentOutdent), - modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 1, 6, false); cursorEqual(cursor, 1, 6, 1, 6); @@ -1256,7 +1259,7 @@ suite('Editor Controller - Regression tests', () => { text: [ ' ' ], - modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 1, 7, false); cursorEqual(cursor, 1, 7, 1, 7); @@ -1282,7 +1285,8 @@ suite('Editor Controller - Regression tests', () => { defaultEOL: DefaultEndOfLine.LF, detectIndentation: false, insertSpaces: false, - tabSize: 4 + tabSize: 4, + trimAutoWhitespace: true }, }, (model, cursor) => { moveTo(cursor, 7, 1, false); @@ -1342,7 +1346,7 @@ suite('Editor Controller - Regression tests', () => { 'hello' ], mode: new SurroundingMode(), - modelOpts: { tabSize: 4, insertSpaces: true, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { tabSize: 4, insertSpaces: true, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 1, 3, false); moveTo(cursor, 1, 5, true); @@ -1364,7 +1368,7 @@ suite('Editor Controller - Regression tests', () => { '};' ], mode: new SurroundingMode(), - modelOpts: { tabSize: 4, insertSpaces: true, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { tabSize: 4, insertSpaces: true, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 3, 2, false); moveTo(cursor, 1, 14, true); @@ -1413,7 +1417,7 @@ suite('Editor Controller - Regression tests', () => { 'just some text', ], mode: null, - modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 1, 1, false); moveTo(cursor, 3, 4, true); @@ -1456,7 +1460,7 @@ suite('Editor Controller - Regression tests', () => { '\t};', '}', ], - modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 3, 2, false); cursorCommand(cursor, H.Tab); @@ -1871,7 +1875,7 @@ suite('Editor Controller - Cursor Configuration', () => { '', '1' ], - modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { cursorCommand(cursor, H.MoveTo, { position: new Position(1, 21) }, null, 'keyboard'); cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); @@ -1889,7 +1893,7 @@ suite('Editor Controller - Cursor Configuration', () => { '', '1' ], - modelOpts: { insertSpaces: true, tabSize: 13, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { insertSpaces: true, tabSize: 13, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { // Tab on column 1 cursorCommand(cursor, H.MoveTo, { position: new Position(2, 1) }, null, 'keyboard'); @@ -1953,7 +1957,7 @@ suite('Editor Controller - Cursor Configuration', () => { '\thello' ], mode: new OnEnterMode(IndentAction.Indent), - modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 1, 7, false); cursorEqual(cursor, 1, 7, 1, 7); @@ -1969,7 +1973,7 @@ suite('Editor Controller - Cursor Configuration', () => { '\thello' ], mode: new OnEnterMode(IndentAction.None), - modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 1, 7, false); cursorEqual(cursor, 1, 7, 1, 7); @@ -1985,7 +1989,7 @@ suite('Editor Controller - Cursor Configuration', () => { '\thell()' ], mode: new OnEnterMode(IndentAction.IndentOutdent), - modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF } + modelOpts: { insertSpaces: true, tabSize: 4, detectIndentation: false, defaultEOL: DefaultEndOfLine.LF, trimAutoWhitespace: true } }, (model, cursor) => { moveTo(cursor, 1, 7, false); cursorEqual(cursor, 1, 7, 1, 7); @@ -2078,6 +2082,324 @@ suite('Editor Controller - Cursor Configuration', () => { assert.equal(model.getLineContent(4), ''); }); }); + + test('removeAutoWhitespace off', () => { + usingCursor({ + text: [ + ' some line abc ' + ], + modelOpts: { + insertSpaces: true, + tabSize: 4, + detectIndentation: false, + defaultEOL: DefaultEndOfLine.LF, + trimAutoWhitespace: false + } + }, (model, cursor) => { + + // Move cursor to the end, verify that we do not trim whitespaces if line has values + moveTo(cursor, 1, model.getLineContent(1).length + 1); + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' some line abc '); + assert.equal(model.getLineContent(2), ' '); + + // Try to enter again, we should trimmed previous line + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' some line abc '); + assert.equal(model.getLineContent(2), ' '); + assert.equal(model.getLineContent(3), ' '); + }); + }); + + test('removeAutoWhitespace on: removes only whitespace the cursor added 1', () => { + usingCursor({ + text: [ + ' ' + ], + modelOpts: { + insertSpaces: true, + tabSize: 4, + detectIndentation: false, + defaultEOL: DefaultEndOfLine.LF, + trimAutoWhitespace: true + } + }, (model, cursor) => { + moveTo(cursor, 1, model.getLineContent(1).length + 1); + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' '); + assert.equal(model.getLineContent(2), ' '); + + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' '); + assert.equal(model.getLineContent(2), ''); + assert.equal(model.getLineContent(3), ' '); + }); + }); + + test('removeAutoWhitespace on: removes only whitespace the cursor added 2', () => { + usingCursor({ + text: [ + ' if (a) {', + ' ', + '', + '', + ' }' + ], + modelOpts: { + insertSpaces: true, + tabSize: 4, + detectIndentation: false, + defaultEOL: DefaultEndOfLine.LF, + trimAutoWhitespace: true + } + }, (model, cursor) => { + + moveTo(cursor, 3, 1); + cursorCommand(cursor, H.Tab, null, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' if (a) {'); + assert.equal(model.getLineContent(2), ' '); + assert.equal(model.getLineContent(3), ' '); + assert.equal(model.getLineContent(4), ''); + assert.equal(model.getLineContent(5), ' }'); + + moveTo(cursor, 4, 1); + cursorCommand(cursor, H.Tab, null, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' if (a) {'); + assert.equal(model.getLineContent(2), ' '); + assert.equal(model.getLineContent(3), ''); + assert.equal(model.getLineContent(4), ' '); + assert.equal(model.getLineContent(5), ' }'); + + moveTo(cursor, 5, model.getLineMaxColumn(5)); + cursorCommand(cursor, H.Type, { text: 'something' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' if (a) {'); + assert.equal(model.getLineContent(2), ' '); + assert.equal(model.getLineContent(3), ''); + assert.equal(model.getLineContent(4), ''); + assert.equal(model.getLineContent(5), ' }something'); + }); + }); + + test('removeAutoWhitespace on: test 1', () => { + usingCursor({ + text: [ + ' some line abc ' + ], + modelOpts: { + insertSpaces: true, + tabSize: 4, + detectIndentation: false, + defaultEOL: DefaultEndOfLine.LF, + trimAutoWhitespace: true + } + }, (model, cursor) => { + + // Move cursor to the end, verify that we do not trim whitespaces if line has values + moveTo(cursor, 1, model.getLineContent(1).length + 1); + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' some line abc '); + assert.equal(model.getLineContent(2), ' '); + + // Try to enter again, we should trimmed previous line + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' some line abc '); + assert.equal(model.getLineContent(2), ''); + assert.equal(model.getLineContent(3), ' '); + + // More whitespaces + cursorCommand(cursor, H.Tab, null, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' some line abc '); + assert.equal(model.getLineContent(2), ''); + assert.equal(model.getLineContent(3), ' '); + + // Enter and verify that trimmed again + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' some line abc '); + assert.equal(model.getLineContent(2), ''); + assert.equal(model.getLineContent(3), ''); + assert.equal(model.getLineContent(4), ' '); + + // Trimmed if we will keep only text + moveTo(cursor, 1, 5); + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' '); + assert.equal(model.getLineContent(2), ' some line abc '); + assert.equal(model.getLineContent(3), ''); + assert.equal(model.getLineContent(4), ''); + assert.equal(model.getLineContent(5), ''); + + // Trimmed if we will keep only text by selection + moveTo(cursor, 2, 5); + moveTo(cursor, 3, 1, true); + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getLineContent(1), ' '); + assert.equal(model.getLineContent(2), ' '); + assert.equal(model.getLineContent(3), ''); + assert.equal(model.getLineContent(4), ''); + assert.equal(model.getLineContent(5), ''); + }); + }); + + test('UseTabStops is off', () => { + usingCursor({ + text: [ + ' x', + ' a ', + ' ' + ], + modelOpts: { + insertSpaces: true, + tabSize: 4, + detectIndentation: false, + defaultEOL: DefaultEndOfLine.LF, + trimAutoWhitespace: true + } + }, (model, cursor) => { + cursor.configuration.editor.useTabStops = false; + + // DeleteLeft removes just one whitespace + moveTo(cursor, 2, 9); + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(2), ' a '); + }); + }); + + test('Backspace removes whitespaces with tab size', () => { + usingCursor({ + text: [ + ' \t \t x', + ' a ', + ' ' + ], + modelOpts: { + insertSpaces: true, + tabSize: 4, + detectIndentation: false, + defaultEOL: DefaultEndOfLine.LF, + trimAutoWhitespace: true + } + }, (model, cursor) => { + cursor.configuration.editor.useTabStops = true; + + // DeleteLeft does not remove tab size, because some text exists before + moveTo(cursor, 2, model.getLineContent(2).length + 1); + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(2), ' a '); + + // DeleteLeft removes tab size = 4 + moveTo(cursor, 2, 9); + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(2), ' a '); + + // DeleteLeft removes tab size = 4 + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(2), 'a '); + + // Undo DeleteLeft - get us back to original indentation + cursorCommand(cursor, H.Undo, {}); + assert.equal(model.getLineContent(2), ' a '); + + // Nothing is broken when cursor is in (1,1) + moveTo(cursor, 1, 1); + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(1), ' \t \t x'); + + // DeleteLeft stops at tab stops even in mixed whitespace case + moveTo(cursor, 1, 10); + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(1), ' \t \t x'); + + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(1), ' \t \tx'); + + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(1), ' \tx'); + + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(1), 'x'); + + // DeleteLeft on last line + moveTo(cursor, 3, model.getLineContent(3).length + 1); + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(3), ''); + + // DeleteLeft with removing new line symbol + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), 'x\n a '); + + // In case of selection DeleteLeft only deletes selected text + moveTo(cursor, 2, 3); + moveTo(cursor, 2, 4, true); + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getLineContent(2), ' a '); + }); + }); + + test('PR #5423: Auto indent + undo + redo is funky', () => { + usingCursor({ + text: [ + '' + ], + modelOpts: { + defaultEOL: DefaultEndOfLine.LF, + detectIndentation: false, + insertSpaces: false, + tabSize: 4, + trimAutoWhitespace: true + } + }, (model, cursor) => { + cursorCommand(cursor, H.Type, { text: '\n' }, null, 'keyboard'); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); + + cursorCommand(cursor, H.Tab, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); + + cursorCommand(cursor, H.Type, { text: 'y'}, null, 'keyboard'); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty', 'assert2'); + + cursorCommand(cursor, H.Type, { text: '\n'}, null, 'keyboard'); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\t', 'assert3'); + + cursorCommand(cursor, H.Type, { text: 'x' }); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert4'); + + cursorCommand(cursor, H.CursorLeft, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert5'); + + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert6'); + + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tyx', 'assert7'); + + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\tx', 'assert8'); + + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert9'); + + cursorCommand(cursor, H.DeleteLeft, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert10'); + + cursorCommand(cursor, H.Undo, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert11'); + + cursorCommand(cursor, H.Undo, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert12'); + + cursorCommand(cursor, H.Undo, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert13'); + + cursorCommand(cursor, H.Redo, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\nx', 'assert14'); + + cursorCommand(cursor, H.Redo, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), '\nx', 'assert15'); + + cursorCommand(cursor, H.Redo, {}); + assert.equal(model.getValue(EndOfLinePreference.LF), 'x', 'assert16'); + }); + }); }); interface ICursorOpts { diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index 03e3cfd4860..6cbdafa02b7 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -21,7 +21,8 @@ suite('EditorModel - EditableTextModel._getInverseEdits', () => { range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), rangeLength: rangeLength, lines: text, - forceMoveMarkers: false + forceMoveMarkers: false, + isAutoWhitespaceEdit: false }; } @@ -270,7 +271,8 @@ suite('EditorModel - EditableTextModel._toSingleEditOperation', () => { range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), rangeLength: rangeLength, lines: text, - forceMoveMarkers: false + forceMoveMarkers: false, + isAutoWhitespaceEdit: false }; } diff --git a/src/vs/editor/test/common/model/mirrorModel.test.ts b/src/vs/editor/test/common/model/mirrorModel.test.ts index 94f81fc4af3..ce0c0ff243b 100644 --- a/src/vs/editor/test/common/model/mirrorModel.test.ts +++ b/src/vs/editor/test/common/model/mirrorModel.test.ts @@ -106,6 +106,7 @@ suite('Editor Model - MirrorModel', () => { options: { tabSize: 4, insertSpaces: true, + trimAutoWhitespace: true, defaultEOL: editorCommon.DefaultEndOfLine.LF } })])); @@ -336,6 +337,7 @@ suite('Editor Model - MirrorModel Eventing', () => { options: { tabSize: 4, insertSpaces: true, + trimAutoWhitespace: true, defaultEOL: editorCommon.DefaultEndOfLine.LF } })])); diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index 67d7f40eb08..c1ce80ed2a0 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -15,7 +15,8 @@ function testGuessIndentation(defaultInsertSpaces:boolean, defaultTabSize:number tabSize: defaultTabSize, insertSpaces: defaultInsertSpaces, detectIndentation: true, - defaultEOL: DefaultEndOfLine.LF + defaultEOL: DefaultEndOfLine.LF, + trimAutoWhitespace: true })); var r = m.getOptions(); m.dispose(); @@ -473,6 +474,7 @@ suite('Editor Model - TextModel', () => { options: { tabSize: 4, insertSpaces: false, + trimAutoWhitespace: true, defaultEOL: DefaultEndOfLine.LF } }); @@ -511,6 +513,7 @@ suite('Editor Model - TextModel', () => { options: { tabSize: 4, insertSpaces: true, + trimAutoWhitespace: true, defaultEOL: DefaultEndOfLine.LF } }); diff --git a/src/vs/workbench/test/node/api/extHostApiCommands.test.ts b/src/vs/workbench/test/node/api/extHostApiCommands.test.ts index 0bf70db68be..b05f2eb12f0 100644 --- a/src/vs/workbench/test/node/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/node/api/extHostApiCommands.test.ts @@ -89,6 +89,7 @@ suite('ExtHostLanguageFeatureCommands', function() { options: { tabSize: 4, insertSpaces: true, + trimAutoWhitespace: true, defaultEOL: EditorCommon.DefaultEndOfLine.LF } }, diff --git a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts index 955d681341f..d61463a27f7 100644 --- a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts @@ -77,6 +77,7 @@ suite('ExtHostLanguageFeatures', function() { options: { tabSize: 4, insertSpaces: true, + trimAutoWhitespace: true, defaultEOL: EditorCommon.DefaultEndOfLine.LF } },