api/doc/Plugin-API-tutorial-replace-with-register.md
⚠️ EXPERIMENTAL API WARNING
The 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.
The "Replace with Register" plugin (link to the original Vim plugin) demonstrates several important concepts in IdeaVim plugin development:
This tutorial will walk you through each part of the implementation, explaining the concepts and techniques used.
IdeaVim plugins using the new API are typically structured as follows:
init function that sets up mappings and functionalityLet'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 VimInitApi.init() {
// We'll add mappings and functionality here
}
The init function has a responsibility to set up our plugin using the VimInitApi, which provides a restricted set of init-safe methods (mappings, text objects, variables, operator functions).
Now, let's add mappings to our plugin. We'll define three mappings:
gr + motion: Replace the text covered by a motion with register contentsgrr: Replace the current line with register contentsgr in visual mode: Replace the selected text with register contentsAdd this code to the init function:
@VimPlugin(name = "ReplaceWithRegister")
fun VimInitApi.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:
mappings block gives us access to the MappingScopennoremap/vnoremap create non-recursive mappings from <Plug> names to actions (lambdas)nmap/vmap create recursive mappings from user-facing keys (like "gr") to <Plug> names.ideavimrc while keeping the underlying actions availableexportOperatorFunction registers a function that will be called when the operator is used with a motionNow, 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 with g@rewriteLine(): Replaces one or more lines with register contentsrewriteVisual(): Replaces the visual selection with register contentsoperatorFunction(): Implements the operator functionNotice the use of scopes:
editor { } gives us access to the editorchange { } creates a transaction for modifying textforEachCaret { } 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 registerreplaceTextAndUpdateCaret(): Handles the replacement logic for different types of selections and register contentsFor the "Replace with Register" plugin, you can test it by:
ygr followed by a motiongrgrr to replace a whole lineFor more information, check out the API Reference and the Quick Start Guide.