Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VIM-1970 | Working solution of plugin vim-highlightedyank #245

Merged
merged 12 commits into from
Jul 27, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions doc/emulated-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,16 @@ Available extensions:
* Emulates [vim-textobj-entire](https://github.com/kana/vim-textobj-entire)
* Additional text objects: `ae`, `ie`
* By [Alexandre Grison](https://github.com/agrison)

## highlightedyank
KostkaBrukowa marked this conversation as resolved.
Show resolved Hide resolved

* Setup:
* `set highlightedyank`
* if you want to optimize highlight duration, assign a time in milliseconds:
`let g:highlightedyank_highlight_duration = "1000"`
A negative number makes the highlight persistent.
`let g:highlightedyank_highlight_duration = "-1"`
* if you want to change background color of highlight you can provide the rgba of the color you want e.g.
`let g:highlightedyank_highlight_color = "rgba(160, 160, 160, 155)"`
* Emulates [vim-highlightedyank](https://github.com/machakann/vim-highlightedyank)
* By [KostkaBrukowa](https://github.com/KostkaBrukowa)
1 change: 1 addition & 0 deletions resources/META-INF/includes/VimExtensions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
<vimExtension implementation="com.maddyhome.idea.vim.extension.argtextobj.VimArgTextObjExtension"/>
<vimExtension implementation="com.maddyhome.idea.vim.extension.replacewithregister.ReplaceWithRegister"/>
<vimExtension implementation="com.maddyhome.idea.vim.extension.exchange.VimExchangeExtension"/>
<vimExtension implementation="com.maddyhome.idea.vim.extension.highlightedyank.VimHighlightedYank"/>
</extensions>
</idea-plugin>
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* 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.highlightedyank

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.markup.EffectType
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.ex.vimscript.VimScriptGlobalEnvironment
import com.maddyhome.idea.vim.extension.VimExtension
import java.awt.Color
import java.awt.Font
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

private const val DEFAULT_HIGHLIGHT_DURATION: Long = 300
private const val HIGHLIGHT_DURATION_VARIABLE_NAME = "g:highlightedyank_highlight_duration"
private const val HIGHLIGHT_COLOR_VARIABLE_NAME = "g:highlightedyank_highlight_color"
private val DEFAULT_HIGHLIGHT_TEXT_COLOR: Color = EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES.defaultAttributes.backgroundColor


/**
* @author KostkaBrukowa (@kostkabrukowa)
*
* Port of vim-highlightedyank
* See https://github.com/machakann/vim-highlightedyank
*
* if you want to optimize highlight duration, use g:highlightedyank_highlight_duration. Assign a time in milliseconds.
*
* let g:highlightedyank_highlight_duration = "1000"

KostkaBrukowa marked this conversation as resolved.
Show resolved Hide resolved
* A negative number makes the highlight persistent.
* let g:highlightedyank_highlight_duration = "-1"

* if you want to change background color of highlight you can provide the rgba of the color you want e.g.
* let g:highlightedyank_highlight_color = "rgba(160, 160, 160, 155)"
*
* When a new text is yanked or user starts editing, the old highlighting would be deleted.
*/
class VimHighlightedYank: VimExtension {
override fun getName() = "highlightedyank"

override fun init() {
highlightEnabled = true
}


override fun dispose() {
super.dispose()
highlightEnabled = false
}

companion object {
var highlightEnabled: Boolean = false
KostkaBrukowa marked this conversation as resolved.
Show resolved Hide resolved
private val highlightHandler = HighlightHandler()

fun highlightYankRange(editor: Editor, range: TextRange) {
highlightHandler.highlightYankRange(editor, range, highlightEnabled)
}

fun clearAllYankHighlighters() {
highlightHandler.clearAllYankHighlighters()
}
}

private class HighlightHandler {
private val yankHighlighters: MutableSet<Pair<Editor, RangeHighlighter>> = mutableSetOf()
Copy link
Member

@AlexPl292 AlexPl292 Jul 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any cases with more that one highlighter? If not, I suggest just store one current highlighter in the fields:

private var highlighter: RangeHighligher? = null
private var editor: Editor? = null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm creating multiple highlighters when user is in multi cursor mode. I haven't found the way to create highlighter with multiple ranges so I'm creating multiple highlighters
On the other hand I don't need editor for every highlighter, I just need one, so I've removed editor from set


fun highlightYankRange(editor: Editor, range: TextRange, highlightEnabled: Boolean) {
if(!highlightEnabled) return

//from vim-highlightedyank docs: When a new text is yanked or user starts editing, the old highlighting would be deleted
clearAllYankHighlighters()

if (range.isMultiple) {
for (i in 0 until range.size()) {
highlightSingleRange(editor, range.startOffsets[i]..range.endOffsets[i])
}
} else {
highlightSingleRange(editor, range.startOffset..range.endOffset)
}
}

fun clearAllYankHighlighters() {
yankHighlighters.forEach { (editor, highlighter) ->
editor.markupModel.removeHighlighter(highlighter)
}

yankHighlighters.clear()
}

private fun highlightSingleRange(editor: Editor, range: ClosedRange<Int>) {
val textAttributes = TextAttributes(
null,
extractUsersHighlightColor(),
editor.colorsScheme.getColor(EditorColors.CARET_COLOR),
EffectType.SEARCH_MATCH, Font.PLAIN
)

val highlighter = editor.markupModel.addRangeHighlighter(
range.start,
range.endInclusive,
HighlighterLayer.SELECTION - 1,
textAttributes,
HighlighterTargetArea.EXACT_RANGE
)

yankHighlighters.add(Pair(editor, highlighter))
AlexPl292 marked this conversation as resolved.
Show resolved Hide resolved

setClearHighlightRangeTimer(editor, highlighter)
}

private fun setClearHighlightRangeTimer(editor: Editor, highlighter: RangeHighlighter) {
val timeout = extractUsersHighlightDuration()

//from vim-highlightedyank docs: A negative number makes the highlight persistent.
if(timeout >= 0) {
Executors.newSingleThreadScheduledExecutor().schedule({
KostkaBrukowa marked this conversation as resolved.
Show resolved Hide resolved
ApplicationManager.getApplication().invokeLater {
editor.markupModel.removeHighlighter(highlighter)
}
}, timeout, TimeUnit.MILLISECONDS)
}
}

private fun extractUsersHighlightDuration(): Long {
return extractVariable(HIGHLIGHT_DURATION_VARIABLE_NAME, DEFAULT_HIGHLIGHT_DURATION) {
it.toLong()
}
}

private fun extractUsersHighlightColor(): Color {
return extractVariable(HIGHLIGHT_COLOR_VARIABLE_NAME, DEFAULT_HIGHLIGHT_TEXT_COLOR) { value ->
val rgba = value
.substring(4)
.filter { it != '(' && it != ')' && !it.isWhitespace() }
.split(',')
.map { it.toInt() }

Color(rgba[0], rgba[1], rgba[2], rgba[3])
}
}

private fun<T> extractVariable(variableName: String, default: T, extractFun: (value: String) -> T): T {
val env = VimScriptGlobalEnvironment.getInstance()
val value = env.variables[variableName]

if(value is String) {
return try {
extractFun(value)
}
catch (e: Exception){
VimPlugin.showMessage("highlightedyank: Invalid value of $variableName -- ${e.message}")

default
}
}

return default
}
}
}
3 changes: 3 additions & 0 deletions src/com/maddyhome/idea/vim/group/ChangeGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import com.maddyhome.idea.vim.common.Register;
import com.maddyhome.idea.vim.common.TextRange;
import com.maddyhome.idea.vim.ex.ranges.LineRange;
import com.maddyhome.idea.vim.extension.highlightedyank.VimHighlightedYank;
import com.maddyhome.idea.vim.group.visual.VimSelection;
import com.maddyhome.idea.vim.group.visual.VisualGroupKt;
import com.maddyhome.idea.vim.group.visual.VisualModeHelperKt;
Expand Down Expand Up @@ -421,6 +422,8 @@ private void initInsert(@NotNull Editor editor, @NotNull DataContext context, @N

VisualGroupKt.updateCaretState(editor);
}

VimHighlightedYank.Companion.clearAllYankHighlighters();
}

// Workaround for VIM-1546. Another solution is highly appreciated.
Expand Down
3 changes: 3 additions & 0 deletions src/com/maddyhome/idea/vim/group/copy/YankGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.maddyhome.idea.vim.action.motion.updown.MotionDownLess1FirstNonSpaceA
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.SelectionType
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.extension.highlightedyank.VimHighlightedYank
import com.maddyhome.idea.vim.group.MotionGroup
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.fileSize
Expand Down Expand Up @@ -172,6 +173,8 @@ class YankGroup {
startOffsets: Map<Caret, Int>?): Boolean {
startOffsets?.forEach { caret, offset -> MotionGroup.moveCaret(editor, caret, offset) }

VimHighlightedYank.highlightYankRange(editor, range)
KostkaBrukowa marked this conversation as resolved.
Show resolved Hide resolved

return VimPlugin.getRegister().storeText(editor, range, type, false)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* 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 org.jetbrains.plugins.ideavim.extension.highlightedyank

import com.intellij.openapi.editor.markup.RangeHighlighter
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.command.CommandState
import com.maddyhome.idea.vim.helper.StringHelper
import junit.framework.Assert
import org.jetbrains.plugins.ideavim.VimTestCase


class VimHighlightedYankTest : VimTestCase() {
override fun setUp() {
super.setUp()
enableExtensions("highlightedyank")
}

fun `test highlighting whole line when whole line is yanked`() {
doTest("yy", code, code, CommandState.Mode.COMMAND, CommandState.SubMode.NONE)

assertAllHighlightersCount(1)
assertHighlighterRange(1, 40, getFirstHighlighter())
}

fun `test highlighting single word when single word is yanked`() {
doTest("yiw", code, code, CommandState.Mode.COMMAND, CommandState.SubMode.NONE)

assertAllHighlightersCount(1)
assertHighlighterRange(5, 8, getFirstHighlighter())
}

fun `test removing previous highlight when new range is yanked`() {
configureByJavaText(code)
typeText(StringHelper.parseKeys("yyjyy"))

assertAllHighlightersCount(1)
assertHighlighterRange(40, 59, getFirstHighlighter())
}

fun `test removing previous highlight when entering insert mode`() {
doTest("yyi", code, code, CommandState.Mode.INSERT, CommandState.SubMode.NONE)
KostkaBrukowa marked this conversation as resolved.
Show resolved Hide resolved

assertAllHighlightersCount(0)
}

fun `test indicating error when incorrect highlight duration was provided by user`() {
configureByJavaText(code)
typeText(StringHelper.parseKeys(":let g:highlightedyank_highlight_duration = \"500.15\"<CR>"))
typeText(StringHelper.parseKeys("yy"))

Assert.assertEquals(VimPlugin.getMessage(), "highlightedyank: Invalid value of g:highlightedyank_highlight_duration -- For input string: \"500.15\"")
}
fun `test not indicating error when correct highlight duration was provided by user`() {

configureByJavaText(code)
typeText(StringHelper.parseKeys(":let g:highlightedyank_highlight_duration = \"-1\"<CR>"))
typeText(StringHelper.parseKeys("yy"))

Assert.assertEquals(VimPlugin.getMessage(), "")
}

fun `test indicating error when incorrect highlight color was provided by user`() {
configureByJavaText(code)

listOf("rgba(1,2,3)", "rgba(1, 2, 3, 0.1)", "rgb(1,2,3)", "rgba(260, 2, 5, 6)").forEach { color ->
typeText(StringHelper.parseKeys(":let g:highlightedyank_highlight_color = \"$color\"<CR>"))
typeText(StringHelper.parseKeys("yy"))

Assert.assertTrue(color, VimPlugin.getMessage().contains("highlightedyank: Invalid value of g:highlightedyank_highlight_color"))
}
}

fun `test indicating error when correct highlight color was provided by user`() {
configureByJavaText(code)

listOf("rgba(1,2,3,5)", "rgba1, 2, 3, 1", "rgba(1, 2, 3, 4").forEach { color ->
typeText(StringHelper.parseKeys(":let g:highlightedyank_highlight_color = \"$color\"<CR>"))
typeText(StringHelper.parseKeys("yy"))

Assert.assertEquals("", VimPlugin.getMessage())
}
}

fun `test highlighting with multiple cursors`() {
doTest("yiw", codeWithMultipleCurors, codeWithMultipleCurors, CommandState.Mode.COMMAND, CommandState.SubMode.NONE)

val highlighters = myFixture.editor.markupModel.allHighlighters
assertAllHighlightersCount(3)
assertHighlighterRange(12, 15, highlighters[1])
assertHighlighterRange(20, 23, highlighters[0])
assertHighlighterRange(28, 31, highlighters[2])
}

fun `test clearing all highlighters with multiple cursors`() {
doTest("yiwi", codeWithMultipleCurors, codeWithMultipleCurors, CommandState.Mode.INSERT, CommandState.SubMode.NONE)

assertAllHighlightersCount(0)
}

val code = """
fun ${c}sum(x: Int, y: Int, z: Int): Int {
return x + y + z
}
"""

val codeWithMultipleCurors = """
fun sum(x: ${c}Int, y: ${c}Int, z: ${c}Int): Int {
return x + y + z
}
"""


private fun assertHighlighterRange(start: Int, end: Int, highlighter: RangeHighlighter) {
assertEquals(start, highlighter.startOffset)
assertEquals(end, highlighter.endOffset)
}

private fun assertAllHighlightersCount(count: Int) {
Assert.assertEquals(count, myFixture.editor.markupModel.allHighlighters.size)
}

private fun getFirstHighlighter(): RangeHighlighter {
return myFixture.editor.markupModel.allHighlighters.first()
}
}