diff --git a/src/vs/editor/common/controller/cursorWordOperations.ts b/src/vs/editor/common/controller/cursorWordOperations.ts index 75c25ccc0219c..73820f9af309e 100644 --- a/src/vs/editor/common/controller/cursorWordOperations.ts +++ b/src/vs/editor/common/controller/cursorWordOperations.ts @@ -438,6 +438,122 @@ export class WordOperations { return new Range(lineNumber, column, position.lineNumber, position.column); } + public static deleteInsideWord(wordSeparators: WordCharacterClassifier, model: ITextModel, selection: Selection): Range { + if (!selection.isEmpty()) { + return selection; + } + + const position = new Position(selection.positionLineNumber, selection.positionColumn); + + let r = this._deleteInsideWordWhitespace(model, position); + if (r) { + return r; + } + + return this._deleteInsideWordDetermineDeleteRange(wordSeparators, model, position); + } + + private static _charAtIsWhitespace(str: string, index: number): boolean { + const charCode = str.charCodeAt(index); + return (charCode === CharCode.Space || charCode === CharCode.Tab); + } + + private static _deleteInsideWordWhitespace(model: ICursorSimpleModel, position: Position): Range | null { + const lineContent = model.getLineContent(position.lineNumber); + const lineContentLength = lineContent.length; + + if (lineContentLength === 0) { + // empty line + return null; + } + + let leftIndex = Math.max(position.column - 2, 0); + if (!this._charAtIsWhitespace(lineContent, leftIndex)) { + // touches a non-whitespace character to the left + return null; + } + + let rightIndex = Math.min(position.column - 1, lineContentLength - 1); + if (!this._charAtIsWhitespace(lineContent, rightIndex)) { + // touches a non-whitespace character to the right + return null; + } + + // walk over whitespace to the left + while (leftIndex > 0 && this._charAtIsWhitespace(lineContent, leftIndex - 1)) { + leftIndex--; + } + + // walk over whitespace to the right + while (rightIndex + 1 < lineContentLength && this._charAtIsWhitespace(lineContent, rightIndex + 1)) { + rightIndex++; + } + + return new Range(position.lineNumber, leftIndex + 1, position.lineNumber, rightIndex + 2); + } + + private static _deleteInsideWordDetermineDeleteRange(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): Range { + const lineContent = model.getLineContent(position.lineNumber); + const lineLength = lineContent.length; + if (lineLength === 0) { + // empty line + if (position.lineNumber > 1) { + return new Range(position.lineNumber - 1, model.getLineMaxColumn(position.lineNumber - 1), position.lineNumber, 1); + } else { + if (position.lineNumber < model.getLineCount()) { + return new Range(position.lineNumber, 1, position.lineNumber + 1, 1); + } else { + // empty model + return new Range(position.lineNumber, 1, position.lineNumber, 1); + } + } + } + + const touchesWord = (word: IFindWordResult) => { + return (word.start + 1 <= position.column && position.column <= word.end + 1); + }; + const createRangeWithPosition = (startColumn: number, endColumn: number) => { + startColumn = Math.min(startColumn, position.column); + endColumn = Math.max(endColumn, position.column); + return new Range(position.lineNumber, startColumn, position.lineNumber, endColumn); + }; + const deleteWordAndAdjacentWhitespace = (word: IFindWordResult) => { + let startColumn = word.start + 1; + let endColumn = word.end + 1; + let expandedToTheRight = false; + while (endColumn - 1 < lineLength && this._charAtIsWhitespace(lineContent, endColumn - 1)) { + expandedToTheRight = true; + endColumn++; + } + if (!expandedToTheRight) { + while (startColumn > 1 && this._charAtIsWhitespace(lineContent, startColumn - 2)) { + startColumn--; + } + } + return createRangeWithPosition(startColumn, endColumn); + }; + + const prevWordOnLine = WordOperations._findPreviousWordOnLine(wordSeparators, model, position); + if (prevWordOnLine && touchesWord(prevWordOnLine)) { + return deleteWordAndAdjacentWhitespace(prevWordOnLine); + } + const nextWordOnLine = WordOperations._findNextWordOnLine(wordSeparators, model, position); + if (nextWordOnLine && touchesWord(nextWordOnLine)) { + return deleteWordAndAdjacentWhitespace(nextWordOnLine); + } + if (prevWordOnLine && nextWordOnLine) { + return createRangeWithPosition(prevWordOnLine.end + 1, nextWordOnLine.start + 1); + } + if (prevWordOnLine) { + return createRangeWithPosition(prevWordOnLine.start + 1, prevWordOnLine.end + 1); + } + if (nextWordOnLine) { + return createRangeWithPosition(nextWordOnLine.start + 1, nextWordOnLine.end + 1); + } + + return createRangeWithPosition(1, lineLength + 1); + } + public static _deleteWordPartLeft(model: ICursorSimpleModel, selection: Selection): Range { if (!selection.isEmpty()) { return selection; diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index ad2afd196a8eb..292390454bb32 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -9,7 +9,7 @@ import { EditorCommand } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { deserializePipePositions, serializePipePositions, testRepeatedActionAndExtractPositions } from 'vs/editor/contrib/wordOperations/test/wordTestUtils'; -import { CursorWordEndLeft, CursorWordEndLeftSelect, CursorWordEndRight, CursorWordEndRightSelect, CursorWordLeft, CursorWordLeftSelect, CursorWordRight, CursorWordRightSelect, CursorWordStartLeft, CursorWordStartLeftSelect, CursorWordStartRight, CursorWordStartRightSelect, DeleteWordEndLeft, DeleteWordEndRight, DeleteWordLeft, DeleteWordRight, DeleteWordStartLeft, DeleteWordStartRight, CursorWordAccessibilityLeft, CursorWordAccessibilityLeftSelect, CursorWordAccessibilityRight, CursorWordAccessibilityRightSelect } from 'vs/editor/contrib/wordOperations/wordOperations'; +import { CursorWordEndLeft, CursorWordEndLeftSelect, CursorWordEndRight, CursorWordEndRightSelect, CursorWordLeft, CursorWordLeftSelect, CursorWordRight, CursorWordRightSelect, CursorWordStartLeft, CursorWordStartLeftSelect, CursorWordStartRight, CursorWordStartRightSelect, DeleteWordEndLeft, DeleteWordEndRight, DeleteWordLeft, DeleteWordRight, DeleteWordStartLeft, DeleteWordStartRight, CursorWordAccessibilityLeft, CursorWordAccessibilityLeftSelect, CursorWordAccessibilityRight, CursorWordAccessibilityRightSelect, DeleteInsideWord } from 'vs/editor/contrib/wordOperations/wordOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; @@ -42,6 +42,7 @@ suite('WordOperations', () => { const _deleteWordRight = new DeleteWordRight(); const _deleteWordStartRight = new DeleteWordStartRight(); const _deleteWordEndRight = new DeleteWordEndRight(); + const _deleteInsideWord = new DeleteInsideWord(); function runEditorCommand(editor: ICodeEditor, command: EditorCommand): void { command.runEditorCommand(null, editor, null); @@ -88,6 +89,9 @@ suite('WordOperations', () => { function deleteWordEndRight(editor: ICodeEditor): void { runEditorCommand(editor, _deleteWordEndRight); } + function deleteInsideWord(editor: ICodeEditor): void { + runEditorCommand(editor, _deleteInsideWord); + } test('cursorWordLeft - simple', () => { const EXPECTED = [ @@ -750,4 +754,119 @@ suite('WordOperations', () => { model.dispose(); mode.dispose(); }); + + test('deleteInsideWord - empty line', () => { + withTestCodeEditor([ + 'Line1', + '', + 'Line2' + ], {}, (editor, _) => { + const model = editor.getModel()!; + editor.setPosition(new Position(2, 1)); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'Line1\nLine2'); + }); + }); + + test('deleteInsideWord - in whitespace 1', () => { + withTestCodeEditor([ + 'Just some text.' + ], {}, (editor, _) => { + const model = editor.getModel()!; + editor.setPosition(new Position(1, 6)); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'Justsome text.'); + }); + }); + + test('deleteInsideWord - in whitespace 2', () => { + withTestCodeEditor([ + 'Just some text.' + ], {}, (editor, _) => { + const model = editor.getModel()!; + editor.setPosition(new Position(1, 6)); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'Justsome text.'); + }); + }); + + test('deleteInsideWord - in whitespace 3', () => { + withTestCodeEditor([ + 'Just "some text.' + ], {}, (editor, _) => { + const model = editor.getModel()!; + editor.setPosition(new Position(1, 6)); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'Just"some text.'); + deleteInsideWord(editor); + assert.equal(model.getValue(), '"some text.'); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'some text.'); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'text.'); + deleteInsideWord(editor); + assert.equal(model.getValue(), '.'); + deleteInsideWord(editor); + assert.equal(model.getValue(), ''); + deleteInsideWord(editor); + assert.equal(model.getValue(), ''); + }); + }); + + test('deleteInsideWord - in non-words', () => { + withTestCodeEditor([ + 'x=3+4+5+6' + ], {}, (editor, _) => { + const model = editor.getModel()!; + editor.setPosition(new Position(1, 7)); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'x=3+45+6'); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'x=3++6'); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'x=36'); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'x='); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'x'); + deleteInsideWord(editor); + assert.equal(model.getValue(), ''); + deleteInsideWord(editor); + assert.equal(model.getValue(), ''); + }); + }); + + test('deleteInsideWord - in words 1', () => { + withTestCodeEditor([ + 'This is interesting' + ], {}, (editor, _) => { + const model = editor.getModel()!; + editor.setPosition(new Position(1, 7)); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'This interesting'); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'This'); + deleteInsideWord(editor); + assert.equal(model.getValue(), ''); + deleteInsideWord(editor); + assert.equal(model.getValue(), ''); + }); + }); + + test('deleteInsideWord - in words 2', () => { + withTestCodeEditor([ + 'This is interesting' + ], {}, (editor, _) => { + const model = editor.getModel()!; + editor.setPosition(new Position(1, 7)); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'This interesting'); + deleteInsideWord(editor); + assert.equal(model.getValue(), 'This'); + deleteInsideWord(editor); + assert.equal(model.getValue(), ''); + deleteInsideWord(editor); + assert.equal(model.getValue(), ''); + }); + }); }); diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index c7db9649e0f68..6b0cedb1dcd61 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -480,6 +480,34 @@ export class DeleteWordRight extends DeleteWordRightCommand { } } +export class DeleteInsideWord extends EditorCommand { + + constructor() { + super({ + id: 'deleteInsideWord', + precondition: EditorContextKeys.writable + }); + } + + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + if (!editor.hasModel()) { + return; + } + const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators)); + const model = editor.getModel(); + const selections = editor.getSelections(); + + const commands = selections.map((sel) => { + const deleteRange = WordOperations.deleteInsideWord(wordSeparators, model, sel); + return new ReplaceCommand(deleteRange, ''); + }); + + editor.pushUndoStop(); + editor.executeCommands(this.id, commands); + editor.pushUndoStop(); + } +} + registerEditorCommand(new CursorWordStartLeft()); registerEditorCommand(new CursorWordEndLeft()); registerEditorCommand(new CursorWordLeft()); @@ -502,3 +530,4 @@ registerEditorCommand(new DeleteWordLeft()); registerEditorCommand(new DeleteWordStartRight()); registerEditorCommand(new DeleteWordEndRight()); registerEditorCommand(new DeleteWordRight()); +registerEditorCommand(new DeleteInsideWord());