Skip to content
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

Implement word part move and delete for issue #46203 #48023

Merged
merged 6 commits into from
Jun 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/vs/base/common/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,11 @@ export function compareIgnoreCase(a: string, b: string): number {
}
}

function isLowerAsciiLetter(code: number): boolean {
export function isLowerAsciiLetter(code: number): boolean {
return code >= CharCode.a && code <= CharCode.z;
}

function isUpperAsciiLetter(code: number): boolean {
export function isUpperAsciiLetter(code: number): boolean {
return code >= CharCode.A && code <= CharCode.Z;
}

Expand Down
130 changes: 128 additions & 2 deletions src/vs/editor/common/controller/cursorWordOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { WordCharacterClassifier, WordCharacterClass, getMapForWordSeparators }
import * as strings from 'vs/base/common/strings';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { CharCode } from 'vs/base/common/charCode';

interface IFindWordResult {
/**
Expand Down Expand Up @@ -235,7 +236,7 @@ export class WordOperations {
return new Position(lineNumber, column);
}

private static _deleteWordLeftWhitespace(model: ICursorSimpleModel, position: Position): Range {
protected static _deleteWordLeftWhitespace(model: ICursorSimpleModel, position: Position): Range {
const lineContent = model.getLineContent(position.lineNumber);
const startIndex = position.column - 2;
const lastNonWhitespace = strings.lastNonWhitespaceIndex(lineContent, startIndex);
Expand Down Expand Up @@ -310,7 +311,7 @@ export class WordOperations {
return len;
}

private static _deleteWordRightWhitespace(model: ICursorSimpleModel, position: Position): Range {
protected static _deleteWordRightWhitespace(model: ICursorSimpleModel, position: Position): Range {
const lineContent = model.getLineContent(position.lineNumber);
const startIndex = position.column - 1;
const firstNonWhitespace = this._findFirstNonWhitespaceChar(lineContent, startIndex);
Expand Down Expand Up @@ -463,3 +464,128 @@ export class WordOperations {
return cursor.move(true, lineNumber, column, 0);
}
}

export function _lastWordPartEnd(str: string, startIndex: number = str.length - 1): number {
for (let i = startIndex; i >= 0; i--) {
let chCode = str.charCodeAt(i);
if (chCode === CharCode.Space || chCode === CharCode.Tab || strings.isUpperAsciiLetter(chCode) || chCode === CharCode.Underline) {
return i - 1;
}
}
return -1;
}

export function _nextWordPartBegin(str: string, startIndex: number = str.length - 1): number {
const checkLowerCase = str.charCodeAt(startIndex - 1) === CharCode.Space; // does a lc char count as a part start?
for (let i = startIndex; i < str.length; ++i) {
let chCode = str.charCodeAt(i);
if (chCode === CharCode.Space || chCode === CharCode.Tab || strings.isUpperAsciiLetter(chCode) || (checkLowerCase && strings.isLowerAsciiLetter(chCode))) {
return i + 1;
}
if (chCode === CharCode.Underline) {
return i + 2;
}
}
return str.length + 1;
}

export class WordPartOperations extends WordOperations {
public static deleteWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range {
if (!selection.isEmpty()) {
return selection;
}

const position = new Position(selection.positionLineNumber, selection.positionColumn);
const lineNumber = position.lineNumber;
const column = position.column;

if (lineNumber === 1 && column === 1) {
// Ignore deleting at beginning of file
return null;
}

if (whitespaceHeuristics) {
let r = WordOperations._deleteWordLeftWhitespace(model, position);
if (r) {
return r;
}
}

const wordRange = WordOperations.deleteWordLeft(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType);
const lastWordPartEnd = _lastWordPartEnd(model.getLineContent(position.lineNumber), position.column - 2);
const wordPartRange = new Range(lineNumber, column, lineNumber, lastWordPartEnd + 2);

if (wordPartRange.getStartPosition().isBeforeOrEqual(wordRange.getStartPosition())) {
return wordRange;
}
return wordPartRange;
}

public static deleteWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range {
if (!selection.isEmpty()) {
return selection;
}

const position = new Position(selection.positionLineNumber, selection.positionColumn);
const lineNumber = position.lineNumber;
const column = position.column;

const lineCount = model.getLineCount();
const maxColumn = model.getLineMaxColumn(lineNumber);
if (lineNumber === lineCount && column === maxColumn) {
// Ignore deleting at end of file
return null;
}

if (whitespaceHeuristics) {
let r = WordOperations._deleteWordRightWhitespace(model, position);
if (r) {
return r;
}
}

const wordRange = WordOperations.deleteWordRight(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType);
const nextWordPartBegin = _nextWordPartBegin(model.getLineContent(position.lineNumber), position.column);
const wordPartRange = new Range(lineNumber, column, lineNumber, nextWordPartBegin);

if (wordRange.getEndPosition().isBeforeOrEqual(wordPartRange.getEndPosition())) {
return wordRange;
}
return wordPartRange;
}

public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position {
const lineNumber = position.lineNumber;
const column = position.column;
if (column === 1) {
return (lineNumber > 1 ? new Position(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)) : position);
}

const wordPos = WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType);
const lastWordPartEnd = _lastWordPartEnd(model.getLineContent(lineNumber), column - 2);
const wordPartPos = new Position(lineNumber, lastWordPartEnd + 2);

if (wordPartPos.isBeforeOrEqual(wordPos)) {
return wordPos;
}
return wordPartPos;
}

public static moveWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position {
const lineNumber = position.lineNumber;
const column = position.column;
const maxColumn = model.getLineMaxColumn(lineNumber);
if (column === maxColumn) {
return (lineNumber < model.getLineCount() ? new Position(lineNumber + 1, 1) : position);
}

const wordPos = WordOperations.moveWordRight(wordSeparators, model, position, wordNavigationType);
const nextWordPartBegin = _nextWordPartBegin(model.getLineContent(lineNumber), column);
const wordPartPos = new Position(lineNumber, nextWordPartBegin);

if (wordPos.isBeforeOrEqual(wordPartPos)) {
return wordPos;
}
return wordPartPos;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*---------------------------------------------------------------------------------------------
* 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 * as assert from 'assert';
import { Position } from 'vs/editor/common/core/position';
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import {
DeleteWordPartLeft, DeleteWordPartRight,
CursorWordPartLeft, CursorWordPartRight
} from 'vs/editor/contrib/wordPartOperations/wordPartOperations';
import { EditorCommand } from 'vs/editor/browser/editorExtensions';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';

suite('WordPartOperations', () => {
const _deleteWordPartLeft = new DeleteWordPartLeft();
const _deleteWordPartRight = new DeleteWordPartRight();
const _cursorWordPartLeft = new CursorWordPartLeft();
const _cursorWordPartRight = new CursorWordPartRight();

function runEditorCommand(editor: ICodeEditor, command: EditorCommand): void {
command.runEditorCommand(null, editor, null);
}
function moveWordPartLeft(editor: ICodeEditor, inSelectionmode: boolean = false): void {
runEditorCommand(editor, inSelectionmode ? _cursorWordPartLeft : _cursorWordPartLeft);
}
function moveWordPartRight(editor: ICodeEditor, inSelectionmode: boolean = false): void {
runEditorCommand(editor, inSelectionmode ? _cursorWordPartLeft : _cursorWordPartRight);
}
function deleteWordPartLeft(editor: ICodeEditor): void {
runEditorCommand(editor, _deleteWordPartLeft);
}
function deleteWordPartRight(editor: ICodeEditor): void {
runEditorCommand(editor, _deleteWordPartRight);
}

test('move word part left basic', () => {
withTestCodeEditor([
'start line',
'thisIsACamelCaseVar this_is_a_snake_case_var',
'end line'
], {}, (editor, _) => {
editor.setPosition(new Position(3, 8));
const expectedStops = [
[3, 5],
[3, 4],
[3, 1],
[2, 46],
[2, 42],
[2, 37],
[2, 31],
[2, 29],
[2, 26],
[2, 22],
[2, 21],
[2, 20],
[2, 17],
[2, 13],
[2, 8],
[2, 7],
[2, 5],
[2, 1],
[1, 11],
[1, 7],
[1, 6],
[1, 1]
];

let actualStops: number[][] = [];
for (let i = 0; i < expectedStops.length; i++) {
moveWordPartLeft(editor);
const pos = editor.getPosition();
actualStops.push([pos.lineNumber, pos.column]);
}

assert.deepEqual(actualStops, expectedStops);
});
});

test('move word part right basic', () => {
withTestCodeEditor([
'start line',
'thisIsACamelCaseVar this_is_a_snake_case_var',
'end line'
], {}, (editor, _) => {
editor.setPosition(new Position(1, 1));
const expectedStops = [
[1, 6],
[1, 7],
[1, 11],
[2, 1],
[2, 5],
[2, 7],
[2, 8],
[2, 13],
[2, 17],
[2, 20],
[2, 21],
[2, 22],
[2, 27],
[2, 30],
[2, 32],
[2, 38],
[2, 43],
[2, 46],
[3, 1],
[3, 4],
[3, 5],
[3, 9]
];

let actualStops: number[][] = [];
for (let i = 0; i < expectedStops.length; i++) {
moveWordPartRight(editor);
const pos = editor.getPosition();
actualStops.push([pos.lineNumber, pos.column]);
}

assert.deepEqual(actualStops, expectedStops);
});
});

test('delete word part left basic', () => {
withTestCodeEditor([
' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var'
], {}, (editor, _) => {
const model = editor.getModel();
editor.setPosition(new Position(1, 84));

deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case', '001');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake', '002');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a', '003');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is', '004');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this', '005');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar ', '006');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar', '007');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCase', '008');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamel', '009');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsA', '010');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIs', '011');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ this', '012');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ ', '013');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */', '014');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 ', '015');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3', '015bis');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-', '016');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5', '017');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +', '018');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 ', '019');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3', '019bis');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= ', '020');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+=', '021');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a', '022');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text ', '023');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text', '024');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some ', '025');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some', '026');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just ', '027');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just', '028');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* ', '029');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /*', '030');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' ', '031');
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), '', '032');
});
});

test('delete word part right basic', () => {
withTestCodeEditor([
' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var'
], {}, (editor, _) => {
const model = editor.getModel();
editor.setPosition(new Position(1, 1));

deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '/* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '001');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '002');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '003');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '004');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '005');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '006');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '007');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '008');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '009');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '010');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '011');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '012');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '013');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '014');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '015');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' */ thisIsACamelCaseVar this_is_a_snake_case_var', '016');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' thisIsACamelCaseVar this_is_a_snake_case_var', '017');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'thisIsACamelCaseVar this_is_a_snake_case_var', '018');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'IsACamelCaseVar this_is_a_snake_case_var', '019');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'ACamelCaseVar this_is_a_snake_case_var', '020');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'CamelCaseVar this_is_a_snake_case_var', '021');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'CaseVar this_is_a_snake_case_var', '022');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'Var this_is_a_snake_case_var', '023');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' this_is_a_snake_case_var', '024');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'this_is_a_snake_case_var', '025');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'is_a_snake_case_var', '026');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'a_snake_case_var', '027');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'snake_case_var', '028');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'case_var', '029');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'var', '030');
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '', '031');
});
});
});
Loading