home / skills / jetbrains / ideavim / extensions-api-migration

extensions-api-migration skill

/.claude/skills/extensions-api-migration

This skill assists migrating IdeaVim extensions from VimExtensionFacade to the @VimPlugin API, guiding registration, API access, and helper refactoring.

npx playbooks add skill jetbrains/ideavim --skill extensions-api-migration

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.7 KB
---
name: extensions-api-migration
description: Migrates IdeaVim extensions from the old VimExtensionFacade API to the new @VimPlugin annotation-based API. Use when converting existing extensions to use the new API patterns.
---

# Extensions API Migration

You are an IdeaVim extensions migration specialist. Your job is to help migrate existing IdeaVim extensions from the old API (VimExtensionFacade) to the new API (@VimPlugin annotation).

## Key Locations

- **New API module**: `api/` folder - contains the new plugin API
- Old API: `VimExtensionFacade` in vim-engine
- Extensions location: `src/main/java/com/maddyhome/idea/vim/extension/`

## How to Use the New API

### Getting Access to the API

To get access to the new API, call the `api()` function from `com.maddyhome.idea.vim.extension.api`:

```kotlin
val api = api()
```

Obtain the API at the start of the `init()` method - this is the entry point for all further work.

### Registering Text Objects

Use `api.textObjects { }` to register text objects:

```kotlin
// From VimIndentObject.kt
override fun init() {
  val api = api()
  api.textObjects {
    register("ai") { _ -> findIndentRange(includeAbove = true, includeBelow = false) }
    register("aI") { _ -> findIndentRange(includeAbove = true, includeBelow = true) }
    register("ii") { _ -> findIndentRange(includeAbove = false, includeBelow = false) }
  }
}
```

### Registering Mappings

Use `api.mappings { }` to register mappings:

```kotlin
// From ParagraphMotion.kt
override fun init() {
  val api = api()

  api.mappings {
    nmapPluginAction("}", "<Plug>(ParagraphNextMotion)", keepDefaultMapping = true) {
      moveParagraph(1)
    }
    nmapPluginAction("{", "<Plug>(ParagraphPrevMotion)", keepDefaultMapping = true) {
      moveParagraph(-1)
    }
    xmapPluginAction("}", "<Plug>(ParagraphNextMotion)", keepDefaultMapping = true) {
      moveParagraph(1)
    }
    // ... operator-pending mode mappings with omapPluginAction
  }
}
```

### Defining Helper Functions

The lambdas in text object and mapping registrations typically call helper functions. Define these functions with `VimApi` as a receiver - this makes the API available inside:

```kotlin
// From VimIndentObject.kt
private fun VimApi.findIndentRange(includeAbove: Boolean, includeBelow: Boolean): TextObjectRange? {
  val charSequence = editor { read { text } }
  val caretOffset = editor { read { withPrimaryCaret { offset } } }
  // ... implementation using API
}

// From ParagraphMotion.kt
internal fun VimApi.moveParagraph(direction: Int) {
  val count = getVariable<Int>("v:count1") ?: 1
  editor {
    change {
      forEachCaret {
        val newOffset = getNextParagraphBoundOffset(actualCount, includeWhitespaceLines = true)
        if (newOffset != null) {
          updateCaret(offset = newOffset)
        }
      }
    }
  }
}
```

### API Features

<!-- Fill in additional API features here -->

## How to Migrate Existing Extensions

### What Stays the Same

- The extension **still inherits VimExtensionFacade** - this does not change
- The extension **still registers in the XML file** - this does not change

### Migration Steps

#### Step 1: Ensure Test Coverage

Before starting migration, make sure tests exist for the extension:
- Tests should work and have good coverage
- If there aren't enough tests, create more tests first
- Verify tests pass on the existing version of the plugin

#### Step 2: Migrate in Small Steps

- Don't try to handle everything in one run
- Run tests on the plugin (just the single test class to speed up things) after making smaller changes
- This ensures consistency and makes it easier to identify issues
- **Do a separate commit for each small sensible change or migration** unless explicitly told not to

#### Step 3: Migrate Handlers One by One

If the extension has multiple handlers, migrate them one at a time rather than all at once.

#### Step 4: Handler Migration Process

For each handler, follow this approach:

1. **Inject the API**: Add `val api = api()` as the first line inside the `execute` function

2. **Extract to extension function**: Extract the content of the execute function into a separate function outside the `ExtensionHandler` class. The new function should:
   - Have `VimApi` as a receiver
   - Use the api that was obtained before
   - Keep the extraction as-is (no changes to logic yet)

3. **Verify tests pass**: Run tests to ensure the extraction didn't break anything

4. **Migrate function content**: Now start migrating the content of the extracted function to use the new API

5. **Verify tests pass again**: Run tests after each significant change

6. **Update registration**: Finally, change the registration of shortcuts from the existing approach to `api.mappings { }` where you call the newly created function

#### Example Migration Flow

```kotlin
// BEFORE: Old style handler
class MyHandler : ExtensionHandler {
  override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
    // ... implementation
  }
}

// STEP 1: Inject API
class MyHandler : ExtensionHandler {
  override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
    val api = api()
    // ... implementation
  }
}

// STEP 2: Extract to extension function (as-is)
class MyHandler : ExtensionHandler {
  override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
    val api = api()
    api.doMyAction(/* pass needed params */)
  }
}

private fun VimApi.doMyAction(/* params */) {
  // ... same implementation, moved here
}

// STEP 3-5: Migrate content to new API inside doMyAction()

// STEP 6: Update registration to use api.mappings { }
override fun init() {
  val api = api()
  api.mappings {
    nmapPluginAction("key", "<Plug>(MyAction)") {
      doMyAction()
    }
  }
}
// Now MyHandler class can be removed
```

#### Handling Complicated Plugins

For more complicated plugins, additional steps may be required.

For example, there might be a separate large class that performs calculations. However, this class may not be usable as-is because it takes a `Document` - a class that is no longer directly available through the new API.

In this case, perform a **pre-refactoring step**: update this class to remove the `Document` dependency before starting the main migration. For instance, change it to accept `CharSequence` instead, which is available via the new API.

#### Final Verification: Check for Old API Usage

After migration, verify that no old API is used by checking imports for `com.maddyhome`.

**Allowed imports** (these are still required):
- `com.maddyhome.idea.vim.extension.VimExtension`
- `com.maddyhome.idea.vim.extension.api`

Any other `com.maddyhome` imports indicate incomplete migration.

Overview

This skill migrates IdeaVim extensions from the legacy VimExtensionFacade API to the new @VimPlugin annotation-based API. It provides a step-by-step migration pattern, practical code patterns for registering text objects and mappings, and verification checks to ensure no old API remnants remain. Use it to convert existing extensions safely, keeping behavior and tests intact.

How this skill works

The skill shows how to obtain the new API via api() and then register text objects and mappings with api.textObjects { } and api.mappings { }. It prescribes extracting existing handler logic into VimApi extension functions, migrating those functions incrementally, and updating registration points to use the new mapping/text object builders. It also includes verification guidance to detect leftover old-API imports.

When to use it

  • Converting an existing IdeaVim extension from VimExtensionFacade to @VimPlugin
  • When you want to adopt the new api()/VimApi patterns for text objects and mappings
  • If tests exist and you need a safe, incremental migration process
  • When an extension uses Document or other old engine types that must be refactored first

Best practices

  • Start by ensuring solid test coverage; add tests before changing code
  • Migrate in small, reviewed commits—one handler or concern per commit
  • First extract logic unchanged into VimApi extension functions, then migrate internals
  • Run focused tests (single test class) after each change to quickly catch regressions
  • Search imports for com.maddyhome to confirm no legacy API usage remains

Example use cases

  • Register text objects using api.textObjects { register("ai") { ... } } to replace old object registration
  • Convert handler execute(editor, context, args) into a private VimApi extension function and call it from mapping lambdas
  • Refactor classes that depended on Document to accept CharSequence before migrating them to the new API
  • Replace XML-registered shortcuts with api.mappings { nmapPluginAction(...) { ... } } while keeping XML registration as required for plugin discovery

FAQ

Do I need to change the extension class inheritance?

No. The extension still inherits VimExtensionFacade and remains registered in XML; migration focuses on handler logic and registration methods.

Where should I call api()?

Call val api = api() at the start of init() for registration code, and at the start of execute() when injecting the API before extracting logic to VimApi extension functions.

How do I verify migration is complete?

Run full tests and search for com.maddyhome imports. Only com.maddyhome.idea.vim.extension.VimExtension and com.maddyhome.idea.vim.extension.api are allowed; any other com.maddyhome imports indicate leftover old API usage.