Skip to content
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ These settings are specific to VSCodeVim.
| vim.highlightedyank.enable | Enable highlighting when yanking | Boolean | false |
| vim.highlightedyank.color | Set the color of yank highlights | String | rgba(250, 240, 170, 0.5) |
| vim.highlightedyank.duration | Set the duration of yank highlights | Number | 200 |
| vim.textObjects | Configure external command text objects. An array of objects with `keys` and `command` properties. | Array | [] |

### Neovim Integration

Expand Down Expand Up @@ -765,6 +766,46 @@ Usage examples:
| vim.argumentObjectClosingDelimiters | A list of closing delimiters | String list | [")", "]"] |
| vim.argumentObjectSeparators | A list of object separators | String list | [","] |

### External Command Text Objects

This feature allows you to define custom text objects using external commands. Configure them via the `vim.textObjects` setting.

Example configuration:

```json
"vim.textObjects": [
{
"keys": ["i", "f"],
"command": "myExtension.textObjectCommand"
}
]
```

- `keys`: An array of strings representing the key sequence for the text object (e.g., `["i", "f"]` for `if`).
- `command`: The VS Code command identifier that will be executed to compute the text object range.

The external command will receive arguments in the form of:

```typescript
{
position: Position;
mode: 'visual' | 'normal' | 'insert';
}
```

Where `Position` is an object with `line` and `character` properties.

The command should return a result object:

```typescript
{
start: Position;
stop: Position;
}
```

or `undefined` if the text object cannot be determined.

## 🎩 VSCodeVim tricks!

VS Code has a lot of nifty tricks and we try to preserve some of them:
Expand Down
2 changes: 2 additions & 0 deletions extensionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Logger } from './src/util/logger';
import { SpecialKeys } from './src/util/specialKeys';
import { VSCodeContext } from './src/util/vscodeContext';
import { exCommandParser } from './src/vimscript/exCommandParser';
import { registerTextObjects } from './src/textobject/externalCommandTextObject';

let extensionContext: vscode.ExtensionContext;
let previousActiveEditorUri: vscode.Uri | undefined;
Expand Down Expand Up @@ -88,6 +89,7 @@ export async function loadConfiguration() {
}
}
}
registerTextObjects(configuration.textObjects);
}

/**
Expand Down
25 changes: 25 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,31 @@
"markdownDescription": "Remapped keys in Normal mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.",
"scope": "application"
},
"vim.textObjects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"keys": {
"type": "array",
"items": {
"type": "string"
},
"description": "Key combinations to trigger the text object."
},
"command": {
"type": "string",
"description": "VSCode command ID to execute for the text object."
}
},
"required": [
"keys",
"command"
]
},
"markdownDescription": "Configure external text objects powered by VSCode commands. Each object must specify keys and command ID.",
"scope": "application"
},
"vim.normalModeKeyBindingsNonRecursive": {
"type": "array",
"markdownDescription": "Non-recursive remapped keys in Normal mode. Allows mapping to Vim commands or VS Code actions. See [README](https://github.com/VSCodeVim/Vim/#key-remapping) for details.",
Expand Down
8 changes: 8 additions & 0 deletions src/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
IKeyRemapping,
IModeSpecificStrings,
ITargetsConfiguration,
ITextObjectConfig,
} from './iconfiguration';

import { SUPPORT_VIMRC } from 'platform/constants';
Expand Down Expand Up @@ -204,6 +205,11 @@ class Configuration implements IConfiguration {
// prevent packaging if we simply called `updateLangmap(configuration.langmap);`
this.loadListeners.forEach((listener) => listener());

// Load external text objects from vim.textObjects
this.textObjects = Array.isArray(vimConfigs.textObjects)
? (vimConfigs.textObjects as ITextObjectConfig[])
: [];

return validatorResults;
}

Expand Down Expand Up @@ -491,6 +497,8 @@ class Configuration implements IConfiguration {
langmapReverseBindingsMap: Map<string, string> = new Map();
langmap = '';

textObjects: ITextObjectConfig[] = [];

get textwidth(): number {
const textwidth = this.getConfiguration('vim').get('textwidth', 80);

Expand Down
10 changes: 10 additions & 0 deletions src/configuration/iconfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export interface ITargetsConfiguration {
smartQuotes: ISmartQuotesConfiguration;
}

export interface ITextObjectConfig {
keys: string[];
command: string;
}

export interface IConfiguration {
[key: string]: any;

Expand Down Expand Up @@ -373,6 +378,11 @@ export interface IConfiguration {
*/
autoSwitchInputMethod: IAutoSwitchInputMethod;

/**
* External text objects powered by VSCode commands
*/
textObjects: ITextObjectConfig[];

/**
* Keybindings
*/
Expand Down
95 changes: 95 additions & 0 deletions src/textobject/externalCommandTextObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { TextObject } from './textobject';
import { VimState } from '../state/vimState';
import { Position } from 'vscode';
import { IMovement, failedMovement } from '../actions/baseMotion';
import { commands } from 'vscode';
import { isVisualMode, Mode } from '../mode/mode';
import type { ITextObjectConfig } from '../configuration/iconfiguration';
import { RegisterAction } from '../actions/base';

type TextObjectResult = {
start: Position;
stop: Position;
};

type TextObjectArgs = {
position: Position;
mode: 'visual' | 'normal' | 'insert';
};

export class ExternalCommandTextObject extends TextObject {
keys: string[];
command: string;
override modes: Mode[] = [Mode.Normal, Mode.Visual, Mode.VisualBlock];

constructor(keys: string[], command: string) {
super();
this.keys = keys;
this.command = command;
}

public override async execAction(position: Position, vimState: VimState): Promise<IMovement> {
try {
const mode = vimState.currentMode;
const args: TextObjectArgs = {
position,
mode: isVisualMode(mode) ? 'visual' : mode === Mode.Insert ? 'insert' : 'normal',
};
const result = await commands.executeCommand(this.command, args);
assertResult(result);
return {
start: result.start,
stop: result.stop,
};
} catch (e) {
return failedMovement(vimState);
}
}
}

function assertResult(result: unknown): asserts result is TextObjectResult {
if (typeof result !== 'object' || result === null) {
throw new Error('Invalid result');
}

if (!('start' in result) || !('stop' in result)) {
throw new Error('Invalid result');
}
const start = result.start;
const stop = result.stop;

if (typeof start !== 'object' || start === null) {
throw new Error('Invalid start');
}

if (typeof stop !== 'object' || stop === null) {
throw new Error('Invalid stop');
}

if (!('line' in start) || typeof start.line !== 'number') {
throw new Error('Invalid start line');
}

if (!('character' in start) || typeof start.character !== 'number') {
throw new Error('Invalid start character');
}

if (!('line' in stop) || typeof stop.line !== 'number') {
throw new Error('Invalid stop line');
}

if (!('character' in stop) || typeof stop.character !== 'number') {
throw new Error('Invalid stop character');
}
}

export function registerTextObjects(objects: ITextObjectConfig[]): void {
for (const obj of objects) {
class DynamicTextObject extends ExternalCommandTextObject {
constructor() {
super(obj.keys, obj.command);
}
}
RegisterAction(DynamicTextObject);
}
}
1 change: 1 addition & 0 deletions test/testConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,5 @@ export class Configuration implements IConfiguration {
'<C-d>': true,
};
langmap = '';
textObjects = [];
}