-
Notifications
You must be signed in to change notification settings - Fork 807
Plugin API tutorial replace with register
⚠️ EXPERIMENTAL API WARNINGThe Plugin API is currently in an experimental stage and is not yet recommended for production use.
- The API is subject to breaking changes without notice
- Features may be added, modified, or removed in future releases
- Documentation may not fully reflect the current implementation
- Use at your own risk for experimental purposes only
We welcome feedback and bug reports to help improve the API, but please be aware that stability is not guaranteed at this time.
This tutorial will guide you through the process of creating a plugin for IdeaVim using the new API. We'll implement a "Replace with Register" plugin that allows you to replace text with the contents of a register.
- Introduction
- Prerequisites
- Project Setup
- Plugin Structure
- Implementing the Plugin
- Testing Your Plugin
The "Replace with Register" plugin (link to the original Vim plugin) demonstrates several important concepts in IdeaVim plugin development:
- Creating custom mappings for different Vim modes
- Working with registers
- Manipulating text in the editor
- Handling different types of selections (character-wise, line-wise, block-wise)
- Creating operator functions
This tutorial will walk you through each part of the implementation, explaining the concepts and techniques used.
- Clone the IdeaVim repo. (Todo: update)
IdeaVim plugins using the new API are typically structured as follows:
- An
initfunction that sets up mappings and functionality - Helper functions that implement specific features
Let's look at how to implement each part of our "Replace with Register" plugin.
First, create a Kotlin file for your plugin:
@VimPlugin(name = "ReplaceWithRegister")
fun VimApi.init() {
// We'll add mappings and functionality here
}The init function has a responsibility to set up our plugin within the VimApi.
Now, let's add mappings to our plugin. We'll define three mappings:
-
gr+ motion: Replace the text covered by a motion with register contents -
grr: Replace the current line with register contents -
grin visual mode: Replace the selected text with register contents
Add this code to the init function:
@VimPlugin(name = "ReplaceWithRegister")
fun VimApi.init() {
mappings {
// Step 1: Non-recursive <Plug> → action mappings
nnoremap("<Plug>ReplaceWithRegisterOperator") {
rewriteMotion()
}
nnoremap("<Plug>ReplaceWithRegisterLine") {
rewriteLine()
}
vnoremap("<Plug>ReplaceWithRegisterVisual") {
rewriteVisual()
}
// Step 2: Recursive key → <Plug> mappings
nmap("gr", "<Plug>ReplaceWithRegisterOperator")
nmap("grr", "<Plug>ReplaceWithRegisterLine")
vmap("gr", "<Plug>ReplaceWithRegisterVisual")
}
exportOperatorFunction("ReplaceWithRegisterOperatorFunc") {
operatorFunction()
}
}Let's break down what's happening:
- The
mappingsblock gives us access to theMappingScope - We use a 2-step mapping pattern:
-
Step 1:
nnoremap/vnoremapcreate non-recursive mappings from<Plug>names to actions (lambdas) -
Step 2:
nmap/vmapcreate recursive mappings from user-facing keys (like"gr") to<Plug>names
-
Step 1:
- This pattern allows users to override the key mappings in their
.ideavimrcwhile keeping the underlying actions available -
exportOperatorFunctionregisters a function that will be called when the operator is used with a motion
Now, let's implement the functions we referenced in our mappings:
private fun VimApi.rewriteMotion() {
setOperatorFunction("ReplaceWithRegisterOperatorFunc")
normal("g@")
}
private suspend fun VimApi.rewriteLine() {
val count1 = getVariable<Int>("v:count1") ?: 1
val job: Job
editor {
job = change {
forEachCaret {
val endOffset = getLineEndOffset(line.number + count1 - 1, true)
val lineStartOffset = line.start
val registerData = prepareRegisterData() ?: return@forEachCaret
replaceText(lineStartOffset, endOffset, registerData.first)
updateCaret(offset = lineStartOffset)
}
}
}
job.join()
}
private suspend fun VimApi.rewriteVisual() {
val job: Job
editor {
job = change {
forEachCaret {
val selectionRange = selection
val registerData = prepareRegisterData() ?: return@forEachCaret
replaceTextAndUpdateCaret(this@rewriteVisual, selectionRange, registerData)
}
}
}
job.join()
mode = Mode.NORMAL()
}
private suspend fun VimApi.operatorFunction(): Boolean {
fun CaretTransaction.getSelection(): Range? {
return when (this@operatorFunction.mode) {
is Mode.NORMAL -> changeMarks
is Mode.VISUAL -> selection
else -> null
}
}
val job: Job
editor {
job = change {
forEachCaret {
val selectionRange = getSelection() ?: return@forEachCaret
val registerData = prepareRegisterData() ?: return@forEachCaret
replaceTextAndUpdateCaret(this@operatorFunction, selectionRange, registerData)
}
}
}
job.join()
return true
}Let's examine each function:
-
rewriteMotion(): Sets up an operator function and triggers it withg@ -
rewriteLine(): Replaces one or more lines with register contents -
rewriteVisual(): Replaces the visual selection with register contents -
operatorFunction(): Implements the operator function
Notice the use of scopes:
-
editor { }gives us access to the editor -
change { }creates a transaction for modifying text -
forEachCaret { }iterates over all carets (useful for multi-cursor editing)
Now, let's implement the helper functions that prepare register data and handle different types of selections:
private suspend fun CaretTransaction.prepareRegisterData(): Pair<String, TextType>? {
val lastRegisterName: Char = lastSelectedReg
var registerText: String = getReg(lastRegisterName) ?: return null
var registerType: TextType = getRegType(lastRegisterName) ?: return null
if (registerType.isLine && registerText.endsWith("\n")) {
registerText = registerText.removeSuffix("\n")
registerType = TextType.CHARACTER_WISE
}
return registerText to registerType
}
private suspend fun CaretTransaction.replaceTextAndUpdateCaret(
vimApi: VimApi,
selectionRange: Range,
registerData: Pair<String, TextType>,
) {
val (text, registerType) = registerData
if (registerType == TextType.BLOCK_WISE) {
val lines = text.lines()
if (selectionRange is Range.Simple) {
val startOffset = selectionRange.start
val endOffset = selectionRange.end
val startLine = getLine(startOffset)
val diff = startOffset - startLine.start
lines.forEachIndexed { index, lineText ->
val offset = getLineStartOffset(startLine.number + index) + diff
if (index == 0) {
replaceText(offset, endOffset, lineText)
} else {
insertText(offset, lineText)
}
}
updateCaret(offset = startOffset)
} else if (selectionRange is Range.Block) {
replaceTextBlockwise(selectionRange, lines)
}
} else {
if (selectionRange is Range.Simple) {
val textLength = this.text.length
if (textLength == 0) {
insertText(0, text)
} else {
replaceText(selectionRange.start, selectionRange.end, text)
}
} else if (selectionRange is Range.Block) {
replaceTextBlockwise(selectionRange, text)
vimApi.mode = Mode.NORMAL()
updateCaret(offset = selectionRange.start)
}
}
}These functions handle:
-
prepareRegisterData(): Gets the content and type of the last used register -
replaceTextAndUpdateCaret(): Handles the replacement logic for different types of selections and register contents
For the "Replace with Register" plugin, you can test it by:
- Yanking some text with
y - Moving to different text and using
grfollowed by a motion - Selecting text in visual mode and using
gr - Using
grrto replace a whole line
For more information, check out the API Reference and the Quick Start Guide.