1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-03-06 00:32:52 +01:00

Add VimExchange extension

This commit is contained in:
Alexey Gerasimov 2020-03-11 21:52:57 +05:00
parent 5762ec0518
commit 9ea08da133

View File

@ -0,0 +1,336 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2020 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.maddyhome.idea.vim.extension.exchange
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.util.Key
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.command.CommandState
import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.command.SelectionType
import com.maddyhome.idea.vim.common.Mark
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimMark
import com.maddyhome.idea.vim.extension.VimExtension
import com.maddyhome.idea.vim.extension.VimExtensionFacade.executeNormal
import com.maddyhome.idea.vim.extension.VimExtensionFacade.getRegister
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping
import com.maddyhome.idea.vim.extension.VimExtensionFacade.setOperatorFunction
import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegister
import com.maddyhome.idea.vim.extension.VimExtensionHandler
import com.maddyhome.idea.vim.group.MarkGroup
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.StringHelper.parseKeys
import com.maddyhome.idea.vim.helper.StringHelper.stringToKeys
import com.maddyhome.idea.vim.helper.subMode
import com.maddyhome.idea.vim.key.OperatorFunction
class VimExchangeExtension: VimExtension {
override fun getName() = "exchange"
override fun init() {
putExtensionHandlerMapping(MappingMode.N, parseKeys("<Plug>Exchange"), owner, ExchangeHandler(false), false)
putExtensionHandlerMapping(MappingMode.X, parseKeys("<Plug>Exchange"), owner, VExchangeHandler(), false)
putExtensionHandlerMapping(MappingMode.N, parseKeys("<Plug>ExchangeClear"), owner, ExchangeClearHandler(), false)
putExtensionHandlerMapping(MappingMode.N, parseKeys("<Plug>ExchangeLine"), owner, ExchangeHandler(true), false)
putKeyMapping(MappingMode.N, parseKeys("cx"), getOwner(), parseKeys("<Plug>Exchange"), true)
putKeyMapping(MappingMode.X, parseKeys("X"), getOwner(), parseKeys("<Plug>Exchange"), true)
putKeyMapping(MappingMode.N, parseKeys("cxc"), getOwner(), parseKeys("<Plug>ExchangeClear"), true)
putKeyMapping(MappingMode.N, parseKeys("cxx"), getOwner(), parseKeys("<Plug>ExchangeLine"), true)
}
private companion object {
val EXCHANGE_KEY = Key<Exchange>("exchange");
class Exchange(val type: CommandState.SubMode, val start: Mark, val end: Mark, val text: String)
fun clearExchange(editor: Editor) {
editor.putUserData(EXCHANGE_KEY, null)
}
}
private class ExchangeHandler(private val isLine: Boolean): VimExtensionHandler {
override fun execute(editor: Editor, context: DataContext) {
setOperatorFunction(Operator(false))
executeNormal(parseKeys(if(isLine) "g@_" else "g@"), editor)
}
}
private class ExchangeClearHandler: VimExtensionHandler {
override fun execute(editor: Editor, context: DataContext) {
clearExchange(editor)
}
}
private class VExchangeHandler: VimExtensionHandler {
override fun execute(editor: Editor, context: DataContext) {
runWriteAction {
// Leave visual mode to create selection marks
executeNormal(parseKeys("<Esc>"), editor)
Operator(true).apply(editor, context, SelectionType.fromSubMode(editor.subMode))
}
}
}
private class Operator(private val isVisual: Boolean): OperatorFunction {
fun Editor.getMarkOffset(mark: Mark) = EditorHelper.getOffset(this, mark.logicalLine, mark.col)
// fun Exchange.getRange(editor: Editor) = TextRange(editor.getMarkOffset(this.start), editor.getMarkOffset(this.end))
fun CommandState.SubMode.getString() = when(this) {
CommandState.SubMode.VISUAL_CHARACTER -> "v"
CommandState.SubMode.VISUAL_LINE -> "V"
CommandState.SubMode.VISUAL_BLOCK -> "\\<C-V>"
else -> throw Error("Invalid SubMode: $this")
}
override fun apply(editor: Editor, context: DataContext, selectionType: SelectionType): Boolean {
fun highlightExchange(ex: Exchange) {
val attributes = editor.colorsScheme.getAttributes(EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES)
val hlArea = when(ex.type) {
CommandState.SubMode.VISUAL_LINE -> HighlighterTargetArea.EXACT_RANGE
// TODO: handle other modes
else -> HighlighterTargetArea.EXACT_RANGE
}
editor.markupModel.addRangeHighlighter(
editor.getMarkOffset(ex.start),
// normalize end offset to remain in line on linewise mode
EditorHelper.normalizeOffset(editor, ex.end.logicalLine, editor.getMarkOffset(ex.end) + 1, false),
HighlighterLayer.SELECTION - 1,
attributes,
hlArea
)
}
val currentExchange = getExchange(editor, isVisual, selectionType)
val exchange1 = editor.getUserData(EXCHANGE_KEY)
if (exchange1 == null) {
editor.putUserData(EXCHANGE_KEY, currentExchange)
highlightExchange(currentExchange)
return true
} else {
val cmp = compareExchanges(exchange1, currentExchange)
var reverse = false
var expand = false
val (ex1, ex2) = when(cmp) {
ExchangeCompareResult.OVERLAP -> return false
ExchangeCompareResult.OUTER -> {
reverse = true
expand = true
Pair(currentExchange, exchange1)
}
ExchangeCompareResult.INNER -> {
expand = true
Pair(exchange1, currentExchange)
}
ExchangeCompareResult.GT -> {
reverse = true
Pair(currentExchange, exchange1)
}
ExchangeCompareResult.LT -> {
Pair(exchange1, currentExchange)
}
}
exchange(editor, ex1, ex2, reverse, expand)
clearExchange(editor)
return true
}
}
private fun exchange(editor: Editor, ex1: Exchange, ex2: Exchange, reverse: Boolean, expand: Boolean) {
fun pasteExchange(sourceExchange: Exchange, targetExchange: Exchange) {
VimPlugin.getMark().setChangeMarks(editor, TextRange(editor.getMarkOffset(targetExchange.start), editor.getMarkOffset(targetExchange.end)+1))
// do this instead of direct text manipulation to set change marks
// setRegister('z', stringToKeys(sourceExchange.text))
VimPlugin.getRegister().storeTextInternal(
editor,
// sourceExchange.getRange(editor),
TextRange(-1, 0),
sourceExchange.text,
SelectionType.fromSubMode(sourceExchange.type),
'z',
false
)
executeNormal(stringToKeys("""`[${targetExchange.type.getString()}`]"zp"""), editor)
}
fun fixCursor(ex1: Exchange, ex2: Exchange, reverse: Boolean) {
val primaryCaret = editor.caretModel.primaryCaret
if(reverse) {
primaryCaret.moveToOffset(editor.getMarkOffset(ex1.start))
} else {
if (ex1.start.logicalLine == ex2.start.logicalLine) {
val horizontalOffset = ex1.end.col - ex2.end.col
primaryCaret.moveToLogicalPosition(LogicalPosition(ex1.start.logicalLine, ex1.start.col - horizontalOffset))
} else if(ex1.end.logicalLine - ex1.start.logicalLine != ex2.end.logicalLine - ex2.start.logicalLine) {
val verticalOffset = ex1.end.logicalLine - ex2.end.logicalLine
primaryCaret.moveToLogicalPosition(LogicalPosition(ex1.start.logicalLine - verticalOffset, ex1.start.col))
}
}
}
val zRegText = getRegister('z')
val unnRegText = getRegister('"')
val startRegText = getRegister('*')
val plusRegText = getRegister('+')
runWriteAction {
// TODO handle:
// " Compare using =~ because "'==' != 0" returns 0
// let indent = s:get_setting('exchange_indent', 1) !~ 0 && a:x.type ==# 'V' && a:y.type ==# 'V'
pasteExchange(ex1, ex2)
if (!expand) {
pasteExchange(ex2, ex1)
}
// TODO: handle: if ident
if (!expand) {
fixCursor(ex1, ex2, reverse)
}
setRegister('z', zRegText)
setRegister('"', unnRegText)
setRegister('*', startRegText)
setRegister('+', plusRegText)
}
}
private fun compareExchanges(x: Exchange, y: Exchange): ExchangeCompareResult {
fun intersects(x: Exchange, y: Exchange) =
x.end.logicalLine < y.start.logicalLine ||
x.start.logicalLine > y.end.logicalLine ||
x.end.col < y.start.col ||
x.start.col > y.end.col
fun comparePos(x: Mark, y: Mark): Int =
if (x.logicalLine == y.logicalLine) {
x.col - y.col
} else {
x.logicalLine - y.logicalLine
}
return if (x.type == CommandState.SubMode.VISUAL_BLOCK && y.type == CommandState.SubMode.VISUAL_BLOCK) {
when {
intersects(x, y) -> {
ExchangeCompareResult.OVERLAP
}
x.start.col <= y.start.col -> {
ExchangeCompareResult.LT
}
else -> {
ExchangeCompareResult.GT
}
}
} else if (comparePos(x.start, y.start) <=0 && comparePos(x.end, y.end) >=0) {
ExchangeCompareResult.OUTER
} else if (comparePos(y.start, x.start) <=0 && comparePos(y.end, x.end) >=0) {
ExchangeCompareResult.INNER
} else if (comparePos(x.start, y.end) <=0 && comparePos(y.start, x.end) <=0 ||
comparePos(y.start, x.end) <=0 && comparePos(x.start, y.end) <=0
) {
ExchangeCompareResult.OVERLAP
} else {
val cmp = comparePos(x.start, y.start)
when {
cmp == 0 -> ExchangeCompareResult.OVERLAP
cmp < 0 -> ExchangeCompareResult.LT
else -> ExchangeCompareResult.GT
}
}
}
enum class ExchangeCompareResult {
OVERLAP,
OUTER,
INNER,
LT,
GT,
}
private fun getExchange(editor: Editor, isVisual: Boolean, selectionType: SelectionType): Exchange {
fun getEndCol(selectionEnd: Mark, type: CommandState.SubMode): Int {
return if (type == CommandState.SubMode.VISUAL_LINE) {
EditorHelper.getLineLength(editor, selectionEnd.logicalLine)
} else {
selectionEnd.col
}
}
// TODO: improve KeyStroke list to sting conversion
fun getRegisterText(reg: Char): String = getRegister(reg)?.map { it.keyChar }?.joinToString("") ?: ""
val unnRegText = getRegister('"')
val starRegText = getRegister('*')
val plusRegText = getRegister('+')
val text = if (isVisual) {
// TODO: improve
val selectionStart = VimPlugin.getMark().getMark(editor, MarkGroup.MARK_VISUAL_START)!!
val selectionEnd = VimPlugin.getMark().getMark(editor, MarkGroup.MARK_VISUAL_END)!!
executeNormal(parseKeys("gvy"), editor)
val text = getRegisterText('"')
// TODO: handle
//if &selection ==# 'exclusive' && start != end
// let end.column -= len(matchstr(@@, '\_.$'))
Exchange(
selectionType.toSubMode(),
selectionStart,
selectionEnd,
// getSelectionText(selectionType)
// EditorHelper.getText(editor, editor.getMarkOffset(selectionStart), editor.getMarkOffset(selectionEnd)+1)
text
)
} else {
val selectionStart = VimPlugin.getMark().getMark(editor, MarkGroup.MARK_CHANGE_START)!!
val selectionEnd = VimPlugin.getMark().getMark(editor, MarkGroup.MARK_CHANGE_END)!!.let {
VimMark.create(
it.key,
it.logicalLine,
getEndCol(it, selectionType.toSubMode()),
it.filename,
it.protocol
)!!
}
when (selectionType) {
SelectionType.LINE_WISE -> executeNormal(stringToKeys("`[V`]y"), editor)
SelectionType.BLOCK_WISE -> executeNormal(stringToKeys("""`[<C-V>`]y"""), editor)
SelectionType.CHARACTER_WISE -> executeNormal(stringToKeys("`[v`]y"), editor)
}
val text = getRegisterText('"')
Exchange(
selectionType.toSubMode(),
selectionStart,
selectionEnd,
// getSelectionText(selectionType)
// EditorHelper.getText(editor, editor.getMarkOffset(selectionStart), editor.getMarkOffset(selectionEnd)+1)
text
)
}
setRegister('"', unnRegText)
setRegister('*', starRegText)
setRegister('+', plusRegText)
return text
}
}
}