diff --git a/package.json b/package.json index 8718bfaa62b..2d3920aabc3 100644 --- a/package.json +++ b/package.json @@ -697,6 +697,11 @@ "markdownDescription": "Enable last/next movements for bracket objects.", "default": true }, + "vim.targets.separatorObjects.enable": { + "type": "boolean", + "markdownDescription": "Enable separator text objects.", + "default": true + }, "vim.targets.smartQuotes.enable": { "type": "boolean", "markdownDescription": "Enable the smart quotes movements from [targets.vim](https://github.com/wellle/targets.vim#quote-text-objects).", diff --git a/src/actions/plugins/targets/separators.ts b/src/actions/plugins/targets/separators.ts new file mode 100644 index 00000000000..149a7c22a6e --- /dev/null +++ b/src/actions/plugins/targets/separators.ts @@ -0,0 +1,335 @@ +import { TextObject } from '../../../textobject/textobject'; +import { RegisterAction } from '../../base'; +import { VimState } from '../../../state/vimState'; +import { failedMovement, IMovement } from '../../baseMotion'; +import { Position } from 'vscode'; +import { isVisualMode } from '../../../mode/mode'; +import { separatorObjectsEnabled } from './targetsConfig'; + +abstract class SeparatorTextObjectMovement extends TextObject { + protected abstract readonly separator: string; + protected abstract includeLeadingSeparator: boolean; + + public override doesActionApply(vimState: VimState, keysPressed: string[]) { + return super.doesActionApply(vimState, keysPressed) && separatorObjectsEnabled(); + } + + public override couldActionApply(vimState: VimState, keysPressed: string[]) { + return super.couldActionApply(vimState, keysPressed) && separatorObjectsEnabled(); + } + + public async execAction(position: Position, vimState: VimState): Promise { + const res = this.matchSeparators(position, vimState); + if (res === undefined) { + return failedMovement(vimState); + } + + let { start, stop } = res; + + stop = stop.translate({ characterDelta: -1 }); + if (!this.includeLeadingSeparator) { + start = start.getRightThroughLineBreaks(false); + } + + if (!isVisualMode(vimState.currentMode) && position.isBefore(start)) { + vimState.recordedState.operatorPositionDiff = start.subtract(position); + } else if (!isVisualMode(vimState.currentMode) && position.isAfter(stop)) { + if (position.line === stop.line) { + vimState.recordedState.operatorPositionDiff = stop.getRight().subtract(position); + } else { + vimState.recordedState.operatorPositionDiff = start.subtract(position); + } + } + + vimState.cursorStartPosition = start; + return { + start, + stop, + }; + } + + private matchSeparators( + position: Position, + vimState: VimState, + ): { start: Position; stop: Position } | undefined { + let start = this.getPrevTarget(position, vimState); + let stop = this.getNextTarget(position, vimState); + + if (start === undefined && stop !== undefined) { + start = stop; + stop = this.getNextTarget(start, vimState); + } else if (start !== undefined && stop === undefined) { + stop = start; + start = this.getPrevTarget(stop, vimState); + } + if (start === undefined || stop === undefined) { + return undefined; + } + return { + start, + stop, + }; + } + + private getPrevTarget(position: Position, vimState: VimState): Position | undefined { + let lineText = vimState.document.lineAt(position).text; + for (let i = position.character - 1; i >= 0; i--) { + if (lineText[i] === this.separator) { + return position.with({ character: i }); + } + } + + // If opening character not found, search backwards across lines + for (let line = position.line - 1; line >= 0; line--) { + lineText = vimState.document.lineAt(line).text; + const matchIndex = lineText.lastIndexOf(this.separator); + if (matchIndex !== -1) { + return position.with({ line, character: matchIndex }); + } + } + return undefined; + } + + private getNextTarget(position: Position, vimState: VimState): Position | undefined { + let lineText = vimState.document.lineAt(position).text; + for (let i = position.character + 1; i < lineText.length; i++) { + if (lineText[i] === this.separator) { + return position.with({ character: i }); + } + } + + // If closing character not found, search forwards across lines + for (let line = position.line + 1; line < vimState.document.lineCount; line++) { + lineText = vimState.document.lineAt(line).text; + const matchIndex = lineText.indexOf(this.separator); + if (matchIndex !== -1) { + return position.with({ line, character: matchIndex }); + } + } + return undefined; + } +} + +@RegisterAction +class SelectInsideComma extends SeparatorTextObjectMovement { + keys = ['i', ',']; + readonly separator = ','; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundComma extends SeparatorTextObjectMovement { + keys = ['a', ',']; + readonly separator = ','; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsidePeriod extends SeparatorTextObjectMovement { + keys = ['i', '.']; + readonly separator = '.'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundPeriod extends SeparatorTextObjectMovement { + keys = ['a', '.']; + readonly separator = '.'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideSemicolon extends SeparatorTextObjectMovement { + keys = ['i', ';']; + readonly separator = ';'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundSemicolon extends SeparatorTextObjectMovement { + keys = ['a', ';']; + readonly separator = ';'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideColon extends SeparatorTextObjectMovement { + keys = ['i', ':']; + readonly separator = ':'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundColon extends SeparatorTextObjectMovement { + keys = ['a', ':']; + readonly separator = ':'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsidePlus extends SeparatorTextObjectMovement { + keys = ['i', '+']; + readonly separator = '+'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundPlus extends SeparatorTextObjectMovement { + keys = ['a', '+']; + readonly separator = '+'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideMinus extends SeparatorTextObjectMovement { + keys = ['i', '-']; + readonly separator = '-'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundMinus extends SeparatorTextObjectMovement { + keys = ['a', '-']; + readonly separator = '-'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideEquals extends SeparatorTextObjectMovement { + keys = ['i', '=']; + readonly separator = '='; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundEquals extends SeparatorTextObjectMovement { + keys = ['a', '=']; + readonly separator = '='; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideTilde extends SeparatorTextObjectMovement { + keys = ['i', '~']; + readonly separator = '~'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundTilde extends SeparatorTextObjectMovement { + keys = ['a', '~']; + readonly separator = '~'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideUnderscore extends SeparatorTextObjectMovement { + keys = ['i', '_']; + readonly separator = '_'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundUnderscore extends SeparatorTextObjectMovement { + keys = ['a', '_']; + readonly separator = '_'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideAsterisk extends SeparatorTextObjectMovement { + keys = ['i', '*']; + readonly separator = '*'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundAsterisk extends SeparatorTextObjectMovement { + keys = ['a', '*']; + readonly separator = '*'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideHash extends SeparatorTextObjectMovement { + keys = ['i', '#']; + readonly separator = '#'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundHash extends SeparatorTextObjectMovement { + keys = ['a', '#']; + readonly separator = '#'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideSlash extends SeparatorTextObjectMovement { + keys = ['i', '/']; + readonly separator = '/'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundSlash extends SeparatorTextObjectMovement { + keys = ['a', '/']; + readonly separator = '/'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsidePipe extends SeparatorTextObjectMovement { + keys = ['i', '|']; + readonly separator = '|'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundPipe extends SeparatorTextObjectMovement { + keys = ['a', '|']; + readonly separator = '|'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideBackslash extends SeparatorTextObjectMovement { + keys = ['i', '\\']; + readonly separator = '\\'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundBackslash extends SeparatorTextObjectMovement { + keys = ['a', '\\']; + readonly separator = '\\'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideAmpersand extends SeparatorTextObjectMovement { + keys = ['i', '&']; + readonly separator = '&'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundAmpersand extends SeparatorTextObjectMovement { + keys = ['a', '&']; + readonly separator = '&'; + readonly includeLeadingSeparator = true; +} + +@RegisterAction +class SelectInsideDollar extends SeparatorTextObjectMovement { + keys = ['i', '$']; + readonly separator = '$'; + readonly includeLeadingSeparator = false; +} + +@RegisterAction +class SelectAroundDollar extends SeparatorTextObjectMovement { + keys = ['a', '$']; + readonly separator = '$'; + readonly includeLeadingSeparator = true; +} diff --git a/src/actions/plugins/targets/targets.ts b/src/actions/plugins/targets/targets.ts index b3a859c99e2..6bb96a2bfdb 100644 --- a/src/actions/plugins/targets/targets.ts +++ b/src/actions/plugins/targets/targets.ts @@ -1,3 +1,4 @@ // targets sub-plugins import './smartQuotes'; import './lastNextObjects'; +import './separators'; diff --git a/src/actions/plugins/targets/targetsConfig.ts b/src/actions/plugins/targets/targetsConfig.ts index 4e5041100dd..92eb52fa0fd 100644 --- a/src/actions/plugins/targets/targetsConfig.ts +++ b/src/actions/plugins/targets/targetsConfig.ts @@ -16,3 +16,12 @@ export function bracketObjectsEnabled(): boolean { configuration.targets.bracketObjects.enable === true) ); } + +export function separatorObjectsEnabled(): boolean { + return ( + (configuration.targets.enable === true && + configuration.targets.separatorObjects.enable !== false) || + (configuration.targets.enable === undefined && + configuration.targets.separatorObjects.enable === true) + ); +} diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 0f0f53ac158..fd9bdfc402b 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -285,6 +285,10 @@ class Configuration implements IConfiguration { enable: true, }, + separatorObjects: { + enable: true, + }, + smartQuotes: { enable: false, breakThroughLines: false, diff --git a/src/configuration/iconfiguration.ts b/src/configuration/iconfiguration.ts index f84e9bfc628..b70534f67ff 100644 --- a/src/configuration/iconfiguration.ts +++ b/src/configuration/iconfiguration.ts @@ -83,6 +83,7 @@ export interface ITargetsConfiguration { */ enable: boolean; bracketObjects: { enable: boolean }; + separatorObjects: { enable: boolean }; smartQuotes: ISmartQuotesConfiguration; } diff --git a/test/plugins/separators.test.ts b/test/plugins/separators.test.ts new file mode 100644 index 00000000000..cb3603a5876 --- /dev/null +++ b/test/plugins/separators.test.ts @@ -0,0 +1,305 @@ +import { newTest } from '../testSimplifier'; +import { cleanUpWorkspace, setupWorkspace } from '../testUtils'; + +suite('separators plugin', () => { + suiteSetup(async () => { + await setupWorkspace({ + fileExtension: '.js', + }); + }); + suiteTeardown(cleanUpWorkspace); + // test quotes types + + newTest({ + title: 'inside commas', + start: ['a, |b, c, d'], + keysPressed: 'di,', + end: ['a,|, c, d'], + }); + newTest({ + title: 'around commas', + start: ['a, |b, c, d'], + keysPressed: 'da,', + end: ['a|, c, d'], + }); + newTest({ + title: 'inside commas from before', + start: ['|a, b, c, d'], + keysPressed: 'di,', + end: ['a,|, c, d'], + }); + newTest({ + title: 'around commas from before', + start: ['|a, b, c, d'], + keysPressed: 'da,', + end: ['a|, c, d'], + }); + newTest({ + title: 'inside commas from after', + start: ['a, b, c, |d'], + keysPressed: 'di,', + end: ['a, b,|, d'], + }); + newTest({ + title: 'around commas from after', + start: ['a, b, c, |d'], + keysPressed: 'da,', + end: ['a, b|, d'], + }); + newTest({ + title: 'inside commas across lines', + start: [ + 'hi there,', // + '|how is it going,', // + 'what are you up to', + ], + keysPressed: 'di,', + end: [ + 'hi there,', // + '|,', // + 'what are you up to', + ], + }); + newTest({ + title: 'around commas across lines', + start: [ + 'hi there,', // + '|how is it going,', // + 'what are you up to', + ], + keysPressed: 'da,', + end: [ + 'hi there|,', // + 'what are you up to', + ], + }); + newTest({ + title: 'inside commas across lines from before', + start: [ + '|hi there,', // + 'how is it going,', // + 'what are you up to', + ], + keysPressed: 'di,', + end: [ + 'hi there,', // + '|,', // + 'what are you up to', + ], + }); + newTest({ + title: 'inside commas across lines from after', + start: [ + 'hi there,', // + 'how is it going,', // + 'what are you up to|', + ], + keysPressed: 'di,', + end: [ + 'hi there,', // + '|,', // + 'what are you up to', + ], + }); + newTest({ + title: 'around commas across lines from before', + start: [ + '|hi there,', // + 'how is it going,', // + 'what are you up to', + ], + keysPressed: 'da,', + end: [ + 'hi there|,', // + 'what are you up to', + ], + }); + newTest({ + title: 'around commas across lines from after', + start: [ + 'hi there,', // + 'how is it going,', // + 'what are you up to|', + ], + keysPressed: 'da,', + end: [ + 'hi there|,', // + 'what are you up to', + ], + }); + newTest({ + title: 'no separators', + start: ['|aaabcd'], + keysPressed: 'di,', + end: ['|aaabcd'], + }); + + newTest({ + title: 'inside periods', + start: ['a. |b. c. d'], + keysPressed: 'di.', + end: ['a.|. c. d'], + }); + newTest({ + title: 'around periods', + start: ['a. |b. c. d'], + keysPressed: 'da.', + end: ['a|. c. d'], + }); + newTest({ + title: 'inside semicolons', + start: ['a; |b; c; d'], + keysPressed: 'di;', + end: ['a;|; c; d'], + }); + newTest({ + title: 'around semicolons', + start: ['a; |b; c; d'], + keysPressed: 'da;', + end: ['a|; c; d'], + }); + newTest({ + title: 'inside colons', + start: ['a: |b: c: d'], + keysPressed: 'di:', + end: ['a:|: c: d'], + }); + newTest({ + title: 'around colons', + start: ['a: |b: c: d'], + keysPressed: 'da:', + end: ['a|: c: d'], + }); + newTest({ + title: 'inside pluses', + start: ['a+ |b+ c+ d'], + keysPressed: 'di+', + end: ['a+|+ c+ d'], + }); + newTest({ + title: 'around pluses', + start: ['a+ |b+ c+ d'], + keysPressed: 'da+', + end: ['a|+ c+ d'], + }); + newTest({ + title: 'inside minuses', + start: ['a- |b- c- d'], + keysPressed: 'di-', + end: ['a-|- c- d'], + }); + newTest({ + title: 'around minuses', + start: ['a- |b- c- d'], + keysPressed: 'da-', + end: ['a|- c- d'], + }); + newTest({ + title: 'inside equals', + start: ['a= |b= c= d'], + keysPressed: 'di=', + end: ['a=|= c= d'], + }); + newTest({ + title: 'around equals', + start: ['a= |b= c= d'], + keysPressed: 'da=', + end: ['a|= c= d'], + }); + newTest({ + title: 'inside tildes', + start: ['a~ |b~ c~ d'], + keysPressed: 'di~', + end: ['a~|~ c~ d'], + }); + newTest({ + title: 'around tildes', + start: ['a~ |b~ c~ d'], + keysPressed: 'da~', + end: ['a|~ c~ d'], + }); + newTest({ + title: 'inside underscores', + start: ['a_ |b_ c_ d'], + keysPressed: 'di_', + end: ['a_|_ c_ d'], + }); + newTest({ + title: 'around underscores', + start: ['a_ |b_ c_ d'], + keysPressed: 'da_', + end: ['a|_ c_ d'], + }); + newTest({ + title: 'inside asterisks', + start: ['a* |b* c* d'], + keysPressed: 'di*', + end: ['a*|* c* d'], + }); + newTest({ + title: 'around asterisks', + start: ['a* |b* c* d'], + keysPressed: 'da*', + end: ['a|* c* d'], + }); + newTest({ + title: 'inside hashes', + start: ['a# |b# c# d'], + keysPressed: 'di#', + end: ['a#|# c# d'], + }); + newTest({ + title: 'around hashes', + start: ['a# |b# c# d'], + keysPressed: 'da#', + end: ['a|# c# d'], + }); + newTest({ + title: 'inside slashes', + start: ['a/ |b/ c/ d'], + keysPressed: 'di/', + end: ['a/|/ c/ d'], + }); + newTest({ + title: 'around slashes', + start: ['a/ |b/ c/ d'], + keysPressed: 'da/', + end: ['a|/ c/ d'], + }); + newTest({ + title: 'inside backslashes', + start: ['a\\ |b\\ c\\ d'], + keysPressed: 'di\\', + end: ['a\\|\\ c\\ d'], + }); + newTest({ + title: 'around backslashes', + start: ['a\\ |b\\ c\\ d'], + keysPressed: 'da\\', + end: ['a|\\ c\\ d'], + }); + newTest({ + title: 'inside ampersands', + start: ['a& |b& c& d'], + keysPressed: 'di&', + end: ['a&|& c& d'], + }); + newTest({ + title: 'around ampersands', + start: ['a& |b& c& d'], + keysPressed: 'da&', + end: ['a|& c& d'], + }); + newTest({ + title: 'inside dollars', + start: ['a$ |b$ c$ d'], + keysPressed: 'di$', + end: ['a$|$ c$ d'], + }); + newTest({ + title: 'around dollars', + start: ['a$ |b$ c$ d'], + keysPressed: 'da$', + end: ['a|$ c$ d'], + }); +}); diff --git a/test/testConfiguration.ts b/test/testConfiguration.ts index 614976d110e..9876722b9d6 100644 --- a/test/testConfiguration.ts +++ b/test/testConfiguration.ts @@ -47,6 +47,9 @@ export class Configuration implements IConfiguration { bracketObjects: { enable: true, }, + separatorObjects: { + enable: true, + }, smartQuotes: { enable: false, breakThroughLines: true,