Skip to content

Commit 1cc8ec4

Browse files
authored
Merge pull request #48023 from brentwang23/master
Implement word part move and delete for issue #46203
2 parents 1178793 + 66eb175 commit 1cc8ec4

File tree

5 files changed

+487
-4
lines changed

5 files changed

+487
-4
lines changed

src/vs/base/common/strings.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,11 @@ export function compareIgnoreCase(a: string, b: string): number {
333333
}
334334
}
335335

336-
function isLowerAsciiLetter(code: number): boolean {
336+
export function isLowerAsciiLetter(code: number): boolean {
337337
return code >= CharCode.a && code <= CharCode.z;
338338
}
339339

340-
function isUpperAsciiLetter(code: number): boolean {
340+
export function isUpperAsciiLetter(code: number): boolean {
341341
return code >= CharCode.A && code <= CharCode.Z;
342342
}
343343

src/vs/editor/common/controller/cursorWordOperations.ts

+128-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { WordCharacterClassifier, WordCharacterClass, getMapForWordSeparators }
1010
import * as strings from 'vs/base/common/strings';
1111
import { Range } from 'vs/editor/common/core/range';
1212
import { Selection } from 'vs/editor/common/core/selection';
13+
import { CharCode } from 'vs/base/common/charCode';
1314

1415
interface IFindWordResult {
1516
/**
@@ -235,7 +236,7 @@ export class WordOperations {
235236
return new Position(lineNumber, column);
236237
}
237238

238-
private static _deleteWordLeftWhitespace(model: ICursorSimpleModel, position: Position): Range {
239+
protected static _deleteWordLeftWhitespace(model: ICursorSimpleModel, position: Position): Range {
239240
const lineContent = model.getLineContent(position.lineNumber);
240241
const startIndex = position.column - 2;
241242
const lastNonWhitespace = strings.lastNonWhitespaceIndex(lineContent, startIndex);
@@ -310,7 +311,7 @@ export class WordOperations {
310311
return len;
311312
}
312313

313-
private static _deleteWordRightWhitespace(model: ICursorSimpleModel, position: Position): Range {
314+
protected static _deleteWordRightWhitespace(model: ICursorSimpleModel, position: Position): Range {
314315
const lineContent = model.getLineContent(position.lineNumber);
315316
const startIndex = position.column - 1;
316317
const firstNonWhitespace = this._findFirstNonWhitespaceChar(lineContent, startIndex);
@@ -463,3 +464,128 @@ export class WordOperations {
463464
return cursor.move(true, lineNumber, column, 0);
464465
}
465466
}
467+
468+
export function _lastWordPartEnd(str: string, startIndex: number = str.length - 1): number {
469+
for (let i = startIndex; i >= 0; i--) {
470+
let chCode = str.charCodeAt(i);
471+
if (chCode === CharCode.Space || chCode === CharCode.Tab || strings.isUpperAsciiLetter(chCode) || chCode === CharCode.Underline) {
472+
return i - 1;
473+
}
474+
}
475+
return -1;
476+
}
477+
478+
export function _nextWordPartBegin(str: string, startIndex: number = str.length - 1): number {
479+
const checkLowerCase = str.charCodeAt(startIndex - 1) === CharCode.Space; // does a lc char count as a part start?
480+
for (let i = startIndex; i < str.length; ++i) {
481+
let chCode = str.charCodeAt(i);
482+
if (chCode === CharCode.Space || chCode === CharCode.Tab || strings.isUpperAsciiLetter(chCode) || (checkLowerCase && strings.isLowerAsciiLetter(chCode))) {
483+
return i + 1;
484+
}
485+
if (chCode === CharCode.Underline) {
486+
return i + 2;
487+
}
488+
}
489+
return str.length + 1;
490+
}
491+
492+
export class WordPartOperations extends WordOperations {
493+
public static deleteWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range {
494+
if (!selection.isEmpty()) {
495+
return selection;
496+
}
497+
498+
const position = new Position(selection.positionLineNumber, selection.positionColumn);
499+
const lineNumber = position.lineNumber;
500+
const column = position.column;
501+
502+
if (lineNumber === 1 && column === 1) {
503+
// Ignore deleting at beginning of file
504+
return null;
505+
}
506+
507+
if (whitespaceHeuristics) {
508+
let r = WordOperations._deleteWordLeftWhitespace(model, position);
509+
if (r) {
510+
return r;
511+
}
512+
}
513+
514+
const wordRange = WordOperations.deleteWordLeft(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType);
515+
const lastWordPartEnd = _lastWordPartEnd(model.getLineContent(position.lineNumber), position.column - 2);
516+
const wordPartRange = new Range(lineNumber, column, lineNumber, lastWordPartEnd + 2);
517+
518+
if (wordPartRange.getStartPosition().isBeforeOrEqual(wordRange.getStartPosition())) {
519+
return wordRange;
520+
}
521+
return wordPartRange;
522+
}
523+
524+
public static deleteWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range {
525+
if (!selection.isEmpty()) {
526+
return selection;
527+
}
528+
529+
const position = new Position(selection.positionLineNumber, selection.positionColumn);
530+
const lineNumber = position.lineNumber;
531+
const column = position.column;
532+
533+
const lineCount = model.getLineCount();
534+
const maxColumn = model.getLineMaxColumn(lineNumber);
535+
if (lineNumber === lineCount && column === maxColumn) {
536+
// Ignore deleting at end of file
537+
return null;
538+
}
539+
540+
if (whitespaceHeuristics) {
541+
let r = WordOperations._deleteWordRightWhitespace(model, position);
542+
if (r) {
543+
return r;
544+
}
545+
}
546+
547+
const wordRange = WordOperations.deleteWordRight(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType);
548+
const nextWordPartBegin = _nextWordPartBegin(model.getLineContent(position.lineNumber), position.column);
549+
const wordPartRange = new Range(lineNumber, column, lineNumber, nextWordPartBegin);
550+
551+
if (wordRange.getEndPosition().isBeforeOrEqual(wordPartRange.getEndPosition())) {
552+
return wordRange;
553+
}
554+
return wordPartRange;
555+
}
556+
557+
public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position {
558+
const lineNumber = position.lineNumber;
559+
const column = position.column;
560+
if (column === 1) {
561+
return (lineNumber > 1 ? new Position(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)) : position);
562+
}
563+
564+
const wordPos = WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType);
565+
const lastWordPartEnd = _lastWordPartEnd(model.getLineContent(lineNumber), column - 2);
566+
const wordPartPos = new Position(lineNumber, lastWordPartEnd + 2);
567+
568+
if (wordPartPos.isBeforeOrEqual(wordPos)) {
569+
return wordPos;
570+
}
571+
return wordPartPos;
572+
}
573+
574+
public static moveWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position {
575+
const lineNumber = position.lineNumber;
576+
const column = position.column;
577+
const maxColumn = model.getLineMaxColumn(lineNumber);
578+
if (column === maxColumn) {
579+
return (lineNumber < model.getLineCount() ? new Position(lineNumber + 1, 1) : position);
580+
}
581+
582+
const wordPos = WordOperations.moveWordRight(wordSeparators, model, position, wordNavigationType);
583+
const nextWordPartBegin = _nextWordPartBegin(model.getLineContent(lineNumber), column);
584+
const wordPartPos = new Position(lineNumber, nextWordPartBegin);
585+
586+
if (wordPos.isBeforeOrEqual(wordPartPos)) {
587+
return wordPos;
588+
}
589+
return wordPartPos;
590+
}
591+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
import * as assert from 'assert';
8+
import { Position } from 'vs/editor/common/core/position';
9+
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
10+
import {
11+
DeleteWordPartLeft, DeleteWordPartRight,
12+
CursorWordPartLeft, CursorWordPartRight
13+
} from 'vs/editor/contrib/wordPartOperations/wordPartOperations';
14+
import { EditorCommand } from 'vs/editor/browser/editorExtensions';
15+
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
16+
17+
suite('WordPartOperations', () => {
18+
const _deleteWordPartLeft = new DeleteWordPartLeft();
19+
const _deleteWordPartRight = new DeleteWordPartRight();
20+
const _cursorWordPartLeft = new CursorWordPartLeft();
21+
const _cursorWordPartRight = new CursorWordPartRight();
22+
23+
function runEditorCommand(editor: ICodeEditor, command: EditorCommand): void {
24+
command.runEditorCommand(null, editor, null);
25+
}
26+
function moveWordPartLeft(editor: ICodeEditor, inSelectionmode: boolean = false): void {
27+
runEditorCommand(editor, inSelectionmode ? _cursorWordPartLeft : _cursorWordPartLeft);
28+
}
29+
function moveWordPartRight(editor: ICodeEditor, inSelectionmode: boolean = false): void {
30+
runEditorCommand(editor, inSelectionmode ? _cursorWordPartLeft : _cursorWordPartRight);
31+
}
32+
function deleteWordPartLeft(editor: ICodeEditor): void {
33+
runEditorCommand(editor, _deleteWordPartLeft);
34+
}
35+
function deleteWordPartRight(editor: ICodeEditor): void {
36+
runEditorCommand(editor, _deleteWordPartRight);
37+
}
38+
39+
test('move word part left basic', () => {
40+
withTestCodeEditor([
41+
'start line',
42+
'thisIsACamelCaseVar this_is_a_snake_case_var',
43+
'end line'
44+
], {}, (editor, _) => {
45+
editor.setPosition(new Position(3, 8));
46+
const expectedStops = [
47+
[3, 5],
48+
[3, 4],
49+
[3, 1],
50+
[2, 46],
51+
[2, 42],
52+
[2, 37],
53+
[2, 31],
54+
[2, 29],
55+
[2, 26],
56+
[2, 22],
57+
[2, 21],
58+
[2, 20],
59+
[2, 17],
60+
[2, 13],
61+
[2, 8],
62+
[2, 7],
63+
[2, 5],
64+
[2, 1],
65+
[1, 11],
66+
[1, 7],
67+
[1, 6],
68+
[1, 1]
69+
];
70+
71+
let actualStops: number[][] = [];
72+
for (let i = 0; i < expectedStops.length; i++) {
73+
moveWordPartLeft(editor);
74+
const pos = editor.getPosition();
75+
actualStops.push([pos.lineNumber, pos.column]);
76+
}
77+
78+
assert.deepEqual(actualStops, expectedStops);
79+
});
80+
});
81+
82+
test('move word part right basic', () => {
83+
withTestCodeEditor([
84+
'start line',
85+
'thisIsACamelCaseVar this_is_a_snake_case_var',
86+
'end line'
87+
], {}, (editor, _) => {
88+
editor.setPosition(new Position(1, 1));
89+
const expectedStops = [
90+
[1, 6],
91+
[1, 7],
92+
[1, 11],
93+
[2, 1],
94+
[2, 5],
95+
[2, 7],
96+
[2, 8],
97+
[2, 13],
98+
[2, 17],
99+
[2, 20],
100+
[2, 21],
101+
[2, 22],
102+
[2, 27],
103+
[2, 30],
104+
[2, 32],
105+
[2, 38],
106+
[2, 43],
107+
[2, 46],
108+
[3, 1],
109+
[3, 4],
110+
[3, 5],
111+
[3, 9]
112+
];
113+
114+
let actualStops: number[][] = [];
115+
for (let i = 0; i < expectedStops.length; i++) {
116+
moveWordPartRight(editor);
117+
const pos = editor.getPosition();
118+
actualStops.push([pos.lineNumber, pos.column]);
119+
}
120+
121+
assert.deepEqual(actualStops, expectedStops);
122+
});
123+
});
124+
125+
test('delete word part left basic', () => {
126+
withTestCodeEditor([
127+
' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var'
128+
], {}, (editor, _) => {
129+
const model = editor.getModel();
130+
editor.setPosition(new Position(1, 84));
131+
132+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case', '001');
133+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake', '002');
134+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a', '003');
135+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is', '004');
136+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this', '005');
137+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar ', '006');
138+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar', '007');
139+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamelCase', '008');
140+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsACamel', '009');
141+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIsA', '010');
142+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ thisIs', '011');
143+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ this', '012');
144+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */ ', '013');
145+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 */', '014');
146+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3 ', '015');
147+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-3', '015bis');
148+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5-', '016');
149+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +5', '017');
150+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 +', '018');
151+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3 ', '019');
152+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= 3', '019bis');
153+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+= ', '020');
154+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a+=', '021');
155+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text a', '022');
156+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text ', '023');
157+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some text', '024');
158+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some ', '025');
159+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just some', '026');
160+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just ', '027');
161+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* Just', '028');
162+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /* ', '029');
163+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' /*', '030');
164+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), ' ', '031');
165+
deleteWordPartLeft(editor); assert.equal(model.getLineContent(1), '', '032');
166+
});
167+
});
168+
169+
test('delete word part right basic', () => {
170+
withTestCodeEditor([
171+
' /* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var'
172+
], {}, (editor, _) => {
173+
const model = editor.getModel();
174+
editor.setPosition(new Position(1, 1));
175+
176+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '/* Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '001');
177+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '002');
178+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'Just some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '003');
179+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '004');
180+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'some text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '005');
181+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '006');
182+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'text a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '007');
183+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '008');
184+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'a+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '009');
185+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '+= 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '010');
186+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' 3 +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '011');
187+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' +5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '012');
188+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '5-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '013');
189+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '-3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '014');
190+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '3 */ thisIsACamelCaseVar this_is_a_snake_case_var', '015');
191+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' */ thisIsACamelCaseVar this_is_a_snake_case_var', '016');
192+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' thisIsACamelCaseVar this_is_a_snake_case_var', '017');
193+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'thisIsACamelCaseVar this_is_a_snake_case_var', '018');
194+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'IsACamelCaseVar this_is_a_snake_case_var', '019');
195+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'ACamelCaseVar this_is_a_snake_case_var', '020');
196+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'CamelCaseVar this_is_a_snake_case_var', '021');
197+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'CaseVar this_is_a_snake_case_var', '022');
198+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'Var this_is_a_snake_case_var', '023');
199+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), ' this_is_a_snake_case_var', '024');
200+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'this_is_a_snake_case_var', '025');
201+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'is_a_snake_case_var', '026');
202+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'a_snake_case_var', '027');
203+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'snake_case_var', '028');
204+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'case_var', '029');
205+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), 'var', '030');
206+
deleteWordPartRight(editor); assert.equal(model.getLineContent(1), '', '031');
207+
});
208+
});
209+
});

0 commit comments

Comments
 (0)