Skip to content

Adding trimTextTrailingWhitespaceCommand #211093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
86 changes: 86 additions & 0 deletions src/vs/editor/common/commands/trimTextTrailingWhitespaceCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { TrimTrailingWhitespaceCommand } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { ITextModel } from 'vs/editor/common/model';
import { Position } from 'vs/editor/common/core/position';
import * as strings from 'vs/base/common/strings';
import { Selection } from 'vs/editor/common/core/selection';
import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';

export class TrimTextTrailingWhitespaceCommand extends TrimTrailingWhitespaceCommand {
constructor(
selection: Selection,
cursors: Position[],
trimInRegexesAndStrings: boolean
) {
super(selection, cursors, trimInRegexesAndStrings);
}
//Override base class to minimize duplicate logic
protected trimTrailingWhitespace(model: ITextModel, cursors: Position[], trimInRegexesAndStrings: boolean): ISingleEditOperation[] {
cursors.sort((a, b) => {
if (a.lineNumber === b.lineNumber) {
return a.column - b.column;
}
return a.lineNumber - b.lineNumber;
});

const operations: ISingleEditOperation[] = [];
let cursorIndex = 0;
const cursorLen = cursors.length;

for (let lineNumber = 1, lineCount = model.getLineCount(); lineNumber <= lineCount; lineNumber++) {
const lineContent = model.getLineContent(lineNumber);
const maxLineColumn = lineContent.length + 1;
let minEditColumn = 0;

if (cursorIndex < cursorLen && cursors[cursorIndex].lineNumber === lineNumber) {
minEditColumn = cursors[cursorIndex].column;
cursorIndex++;
if (minEditColumn === maxLineColumn) {
continue;
}
}

if (lineContent.length === 0) {
continue;
}

const lastNonWhitespaceIndex = strings.lastNonWhitespaceIndex(lineContent);
if (lastNonWhitespaceIndex === -1) {
// Entire line is whitespace; skip it.
continue;
}

let fromColumn = lastNonWhitespaceIndex + 2;
if (!trimInRegexesAndStrings) {
if (!model.tokenization.hasAccurateTokensForLine(lineNumber)) {
continue;
}

const lineTokens = model.tokenization.getLineTokens(lineNumber);
const fromColumnType = lineTokens.getStandardTokenType(lineTokens.findTokenIndexAtOffset(fromColumn));

if (fromColumnType === StandardTokenType.String || fromColumnType === StandardTokenType.RegEx) {
continue;
}
}

fromColumn = Math.max(minEditColumn, fromColumn);
operations.push({
range: {
startLineNumber: lineNumber,
startColumn: fromColumn,
endLineNumber: lineNumber,
endColumn: maxLineColumn
},
text: ''
});
}

return operations;
}
}
43 changes: 43 additions & 0 deletions src/vs/editor/contrib/linesOperations/browser/linesOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'
import { EditorAction, IActionOptions, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { ReplaceCommand, ReplaceCommandThatPreservesSelection, ReplaceCommandThatSelectsText } from 'vs/editor/common/commands/replaceCommand';
import { TrimTrailingWhitespaceCommand } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand';
import { TrimTextTrailingWhitespaceCommand } from 'vs/editor/common/commands/trimTextTrailingWhitespaceCommand';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations';
import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation';
Expand Down Expand Up @@ -415,6 +416,48 @@ export class TrimTrailingWhitespaceAction extends EditorAction {
}
}

export class TrimTextTrailingWhitespaceAction extends EditorAction {

public static readonly ID = 'editor.action.trimTextTrailingWhitespace';

constructor() {
super({
id: TrimTextTrailingWhitespaceAction.ID,
label: nls.localize('lines.trimTextTrailingWhitespace', "Trim Text-Trailing Whitespace"),
alias: 'Trim Text-Trailing Whitespace',
precondition: EditorContextKeys.writable,
kbOpts: {
kbExpr: EditorContextKeys.editorTextFocus,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX),
weight: KeybindingWeight.EditorContrib
}
});
}

public run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {

let cursors: Position[] = [];
if (args.reason === 'auto-save') {
cursors = (editor.getSelections() || []).map(s => new Position(s.positionLineNumber, s.positionColumn));
}

const selection = editor.getSelection();
if (selection === null) {
return;
}

const config = _accessor.get(IConfigurationService);
const model = editor.getModel();
const trimInRegexAndStrings = config.getValue<boolean>('files.trimTextTrailingWhitespaceInRegexAndStrings', { overrideIdentifier: model?.getLanguageId(), resource: model?.uri });

const command = new TrimTextTrailingWhitespaceCommand(selection, cursors, trimInRegexAndStrings);

editor.pushUndoStop();
editor.executeCommands(this.id, [command]);
editor.pushUndoStop();
}
}

// delete lines

interface IDeleteLinesOperation {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { TrimTextTrailingWhitespaceCommand } from 'vs/editor/common/commands/trimTextTrailingWhitespaceCommand';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import { Selection } from 'vs/editor/common/core/selection';
import { getEditOperation } from 'vs/editor/test/browser/testCommand';
import { withEditorModel } from 'vs/editor/test/common/testTextModel';

suite('Editor Commands - Trim Text Trailing Whitespace Command', () => {

let disposables: DisposableStore;

setup(() => {
disposables = new DisposableStore();
});

teardown(() => {
disposables.dispose();
});

ensureNoDisposablesAreLeakedInTestSuite();

test('whitespace-only lines should not get deleted', function () {
assertTrimTextTrailingWhitespaceCommand(['\t \n', ' \n', ' \t\n', ' \t \n'], []);
});

function assertTrimTextTrailingWhitespaceCommand(text: string[], expected: ISingleEditOperation[]): void {
return withEditorModel(text, (model) => {
const op = new TrimTextTrailingWhitespaceCommand(new Selection(1, 1, 1, 1), [], true);
const actual = getEditOperation(model, op);
assert.deepStrictEqual(actual, expected);
});
}
});