Skip to content

Commit a4a5b59

Browse files
committed
Reimplement word part operations (microsoft#53899)
1 parent e4a3eb8 commit a4a5b59

4 files changed

Lines changed: 158 additions & 174 deletions

File tree

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

Lines changed: 126 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,43 @@ export class WordOperations {
212212
return new Position(lineNumber, prevWordOnLine ? prevWordOnLine.end + 1 : 1);
213213
}
214214

215+
public static _moveWordPartLeft(model: ICursorSimpleModel, position: Position): Position {
216+
const lineNumber = position.lineNumber;
217+
const maxColumn = model.getLineMaxColumn(lineNumber);
218+
219+
if (position.column === 1) {
220+
return (lineNumber > 1 ? new Position(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)) : position);
221+
}
222+
223+
const lineContent = model.getLineContent(lineNumber);
224+
for (let column = position.column - 1; column > 1; column--) {
225+
const left = lineContent.charCodeAt(column - 2);
226+
const right = lineContent.charCodeAt(column - 1);
227+
228+
if (left !== CharCode.Underline && right === CharCode.Underline) {
229+
// snake_case_variables
230+
return new Position(lineNumber, column);
231+
}
232+
233+
if (strings.isLowerAsciiLetter(left) && strings.isUpperAsciiLetter(right)) {
234+
// camelCaseVariables
235+
return new Position(lineNumber, column);
236+
}
237+
238+
if (strings.isUpperAsciiLetter(left) && strings.isUpperAsciiLetter(right)) {
239+
// thisIsACamelCaseWithOneLetterWords
240+
if (column + 1 < maxColumn) {
241+
const rightRight = lineContent.charCodeAt(column);
242+
if (strings.isLowerAsciiLetter(rightRight)) {
243+
return new Position(lineNumber, column);
244+
}
245+
}
246+
}
247+
}
248+
249+
return new Position(lineNumber, 1);
250+
}
251+
215252
public static moveWordRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position {
216253
let lineNumber = position.lineNumber;
217254
let column = position.column;
@@ -251,6 +288,43 @@ export class WordOperations {
251288
return new Position(lineNumber, column);
252289
}
253290

291+
public static _moveWordPartRight(model: ICursorSimpleModel, position: Position): Position {
292+
const lineNumber = position.lineNumber;
293+
const maxColumn = model.getLineMaxColumn(lineNumber);
294+
295+
if (position.column === maxColumn) {
296+
return (lineNumber < model.getLineCount() ? new Position(lineNumber + 1, 1) : position);
297+
}
298+
299+
const lineContent = model.getLineContent(lineNumber);
300+
for (let column = position.column + 1; column < maxColumn; column++) {
301+
const left = lineContent.charCodeAt(column - 2);
302+
const right = lineContent.charCodeAt(column - 1);
303+
304+
if (left === CharCode.Underline && right !== CharCode.Underline) {
305+
// snake_case_variables
306+
return new Position(lineNumber, column);
307+
}
308+
309+
if (strings.isLowerAsciiLetter(left) && strings.isUpperAsciiLetter(right)) {
310+
// camelCaseVariables
311+
return new Position(lineNumber, column);
312+
}
313+
314+
if (strings.isUpperAsciiLetter(left) && strings.isUpperAsciiLetter(right)) {
315+
// thisIsACamelCaseWithOneLetterWords
316+
if (column + 1 < maxColumn) {
317+
const rightRight = lineContent.charCodeAt(column);
318+
if (strings.isLowerAsciiLetter(rightRight)) {
319+
return new Position(lineNumber, column);
320+
}
321+
}
322+
}
323+
}
324+
325+
return new Position(lineNumber, maxColumn);
326+
}
327+
254328
protected static _deleteWordLeftWhitespace(model: ICursorSimpleModel, position: Position): Range {
255329
const lineContent = model.getLineContent(position.lineNumber);
256330
const startIndex = position.column - 2;
@@ -315,6 +389,16 @@ export class WordOperations {
315389
return new Range(lineNumber, column, position.lineNumber, position.column);
316390
}
317391

392+
public static _deleteWordPartLeft(model: ICursorSimpleModel, selection: Selection): Range {
393+
if (!selection.isEmpty()) {
394+
return selection;
395+
}
396+
397+
const pos = selection.getPosition();
398+
const toPosition = WordOperations._moveWordPartLeft(model, pos);
399+
return new Range(pos.lineNumber, pos.column, toPosition.lineNumber, toPosition.column);
400+
}
401+
318402
private static _findFirstNonWhitespaceChar(str: string, startIndex: number): number {
319403
let len = str.length;
320404
for (let chIndex = startIndex; chIndex < len; chIndex++) {
@@ -403,6 +487,16 @@ export class WordOperations {
403487
return new Range(lineNumber, column, position.lineNumber, position.column);
404488
}
405489

490+
public static _deleteWordPartRight(model: ICursorSimpleModel, selection: Selection): Range {
491+
if (!selection.isEmpty()) {
492+
return selection;
493+
}
494+
495+
const pos = selection.getPosition();
496+
const toPosition = WordOperations._moveWordPartRight(model, pos);
497+
return new Range(pos.lineNumber, pos.column, toPosition.lineNumber, toPosition.column);
498+
}
499+
406500
public static word(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, position: Position): SingleCursorState {
407501
const wordSeparators = getMapForWordSeparators(config.wordSeparators);
408502
let prevWord = WordOperations._findPreviousWordOnLine(wordSeparators, model, position);
@@ -480,175 +574,44 @@ export class WordOperations {
480574
}
481575
}
482576

483-
export function _lastWordPartEnd(str: string, startIndex: number = str.length - 1): number {
484-
let ignoreUpperCase = !strings.isLowerAsciiLetter(str.charCodeAt(startIndex + 1));
485-
for (let i = startIndex; i >= 0; i--) {
486-
const chCode = str.charCodeAt(i);
487-
if (chCode === CharCode.Space || chCode === CharCode.Tab) {
488-
if (i === 0) {
489-
return 0;
490-
}
491-
const prevChCode = str.charCodeAt(i - 1);
492-
if (prevChCode !== CharCode.Space && prevChCode !== CharCode.Tab) {
493-
return i - 1;
494-
}
495-
}
496-
if (!ignoreUpperCase && strings.isUpperAsciiLetter(chCode)) {
497-
return i - 1;
498-
}
499-
if (ignoreUpperCase && i < startIndex && strings.isLowerAsciiLetter(chCode)) {
500-
return i;
501-
}
502-
if (chCode === CharCode.Underline) {
503-
if (i === 0) {
504-
return 0;
505-
}
506-
const prevChCode = str.charCodeAt(i - 1);
507-
if (prevChCode !== CharCode.Underline) {
508-
return i - 1;
509-
}
510-
}
511-
ignoreUpperCase = ignoreUpperCase && strings.isUpperAsciiLetter(chCode);
512-
}
513-
return -1;
514-
}
515-
516-
export function _nextWordPartBegin(str: string, startIndex: number = 0): number {
517-
let prevChCode = str.charCodeAt(startIndex - 1);
518-
let chCode = str.charCodeAt(startIndex);
519-
// handle the special case ' X' and ' x' which is different from the standard methods
520-
if ((prevChCode === CharCode.Space || prevChCode === CharCode.Tab) && (strings.isLowerAsciiLetter(chCode) || strings.isUpperAsciiLetter(chCode))) {
521-
return startIndex + 1;
522-
}
523-
let ignoreUpperCase = strings.isUpperAsciiLetter(chCode);
524-
for (let i = startIndex; i < str.length; ++i) {
525-
chCode = str.charCodeAt(i);
526-
if (chCode === CharCode.Space || chCode === CharCode.Tab) {
527-
if (i + 1 >= str.length) {
528-
return i + 1;
529-
}
530-
const nextChCode = str.charCodeAt(i + 1);
531-
if (nextChCode !== CharCode.Space && nextChCode !== CharCode.Tab) {
532-
return i + 2;
533-
}
534-
}
535-
if (!ignoreUpperCase && strings.isUpperAsciiLetter(chCode)) {
536-
return i + 1;
537-
}
538-
if (ignoreUpperCase && strings.isLowerAsciiLetter(chCode)) {
539-
return i; // multiple UPPERCase : assume an upper case word and a CamelCase word - like DSLModel
540-
}
541-
ignoreUpperCase = ignoreUpperCase && strings.isUpperAsciiLetter(chCode);
542-
if (chCode === CharCode.Underline) {
543-
if (i + 1 >= str.length) {
544-
return i + 1;
545-
}
546-
const nextChCode = str.charCodeAt(i + 1);
547-
if (nextChCode !== CharCode.Underline) {
548-
return i + 2;
549-
}
550-
}
551-
}
552-
return str.length + 1;
553-
}
554-
555577
export class WordPartOperations extends WordOperations {
556-
public static deleteWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range {
557-
if (!selection.isEmpty()) {
558-
return selection;
559-
}
560-
561-
const position = new Position(selection.positionLineNumber, selection.positionColumn);
562-
const lineNumber = position.lineNumber;
563-
const column = position.column;
564-
565-
if (lineNumber === 1 && column === 1) {
566-
// Ignore deleting at beginning of file
567-
return null;
568-
}
569-
570-
if (whitespaceHeuristics) {
571-
let r = WordOperations._deleteWordLeftWhitespace(model, position);
572-
if (r) {
573-
return r;
574-
}
575-
}
576-
577-
const wordRange = WordOperations.deleteWordLeft(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType);
578-
const lastWordPartEnd = _lastWordPartEnd(model.getLineContent(position.lineNumber), position.column - 2);
579-
const wordPartRange = new Range(lineNumber, column, lineNumber, lastWordPartEnd + 2);
580-
581-
if (wordPartRange.getStartPosition().isBeforeOrEqual(wordRange.getStartPosition())) {
582-
return wordRange;
583-
}
584-
return wordPartRange;
578+
public static deleteWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean): Range {
579+
const candidates = [
580+
WordOperations.deleteWordLeft(wordSeparators, model, selection, whitespaceHeuristics, WordNavigationType.WordStart),
581+
WordOperations.deleteWordLeft(wordSeparators, model, selection, whitespaceHeuristics, WordNavigationType.WordEnd),
582+
WordOperations._deleteWordPartLeft(model, selection)
583+
];
584+
candidates.sort(Range.compareRangesUsingEnds);
585+
return candidates[2];
585586
}
586587

587-
public static deleteWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean, wordNavigationType: WordNavigationType): Range {
588-
if (!selection.isEmpty()) {
589-
return selection;
590-
}
591-
592-
const position = new Position(selection.positionLineNumber, selection.positionColumn);
593-
const lineNumber = position.lineNumber;
594-
const column = position.column;
595-
596-
const lineCount = model.getLineCount();
597-
const maxColumn = model.getLineMaxColumn(lineNumber);
598-
if (lineNumber === lineCount && column === maxColumn) {
599-
// Ignore deleting at end of file
600-
return null;
601-
}
602-
603-
if (whitespaceHeuristics) {
604-
let r = WordOperations._deleteWordRightWhitespace(model, position);
605-
if (r) {
606-
return r;
607-
}
608-
}
609-
610-
const wordRange = WordOperations.deleteWordRight(wordSeparators, model, selection, whitespaceHeuristics, wordNavigationType);
611-
const nextWordPartBegin = _nextWordPartBegin(model.getLineContent(position.lineNumber), position.column);
612-
const wordPartRange = new Range(lineNumber, column, lineNumber, nextWordPartBegin);
613-
614-
if (wordRange.getEndPosition().isBeforeOrEqual(wordPartRange.getEndPosition())) {
615-
return wordRange;
616-
}
617-
return wordPartRange;
588+
public static deleteWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, selection: Selection, whitespaceHeuristics: boolean): Range {
589+
const candidates = [
590+
WordOperations.deleteWordRight(wordSeparators, model, selection, whitespaceHeuristics, WordNavigationType.WordStart),
591+
WordOperations.deleteWordRight(wordSeparators, model, selection, whitespaceHeuristics, WordNavigationType.WordEnd),
592+
WordOperations._deleteWordPartRight(model, selection)
593+
];
594+
candidates.sort(Range.compareRangesUsingStarts);
595+
return candidates[0];
618596
}
619597

620-
public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position {
621-
const lineNumber = position.lineNumber;
622-
const column = position.column;
623-
if (column === 1) {
624-
return (lineNumber > 1 ? new Position(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)) : position);
625-
}
626-
627-
const wordPos = WordOperations.moveWordLeft(wordSeparators, model, position, wordNavigationType);
628-
const lastWordPartEnd = _lastWordPartEnd(model.getLineContent(lineNumber), column - 2);
629-
const wordPartPos = new Position(lineNumber, lastWordPartEnd + 2);
630-
631-
if (wordPartPos.isBeforeOrEqual(wordPos)) {
632-
return wordPos;
633-
}
634-
return wordPartPos;
598+
public static moveWordPartLeft(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): Position {
599+
const candidates = [
600+
WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordStart),
601+
WordOperations.moveWordLeft(wordSeparators, model, position, WordNavigationType.WordEnd),
602+
WordOperations._moveWordPartLeft(model, position)
603+
];
604+
candidates.sort(Position.compare);
605+
return candidates[2];
635606
}
636607

637-
public static moveWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, wordNavigationType: WordNavigationType): Position {
638-
const lineNumber = position.lineNumber;
639-
const column = position.column;
640-
const maxColumn = model.getLineMaxColumn(lineNumber);
641-
if (column === maxColumn) {
642-
return (lineNumber < model.getLineCount() ? new Position(lineNumber + 1, 1) : position);
643-
}
644-
645-
const wordPos = WordOperations.moveWordRight(wordSeparators, model, position, wordNavigationType);
646-
const nextWordPartBegin = _nextWordPartBegin(model.getLineContent(lineNumber), column);
647-
const wordPartPos = new Position(lineNumber, nextWordPartBegin);
648-
649-
if (wordPos.isBeforeOrEqual(wordPartPos)) {
650-
return wordPos;
651-
}
652-
return wordPartPos;
608+
public static moveWordPartRight(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): Position {
609+
const candidates = [
610+
WordOperations.moveWordRight(wordSeparators, model, position, WordNavigationType.WordStart),
611+
WordOperations.moveWordRight(wordSeparators, model, position, WordNavigationType.WordEnd),
612+
WordOperations._moveWordPartRight(model, position)
613+
];
614+
candidates.sort(Position.compare);
615+
return candidates[0];
653616
}
654617
}

src/vs/editor/contrib/wordOperations/test/wordTestUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function testRepeatedActionAndExtractPositions(text: string, initialPosit
7171
}
7272

7373
if (actualStops.length > 1000) {
74-
throw new Error(`Endless loop detected!`);
74+
throw new Error(`Endless loop detected involving position ${editor.getPosition()}!`);
7575
}
7676
}
7777
});

0 commit comments

Comments
 (0)