Skip to content

Commit

Permalink
Merge pull request #245 from KostkaBrukowa/VIM-1970
Browse files Browse the repository at this point in the history
VIM-1970 | Working solution of plugin vim-highlightedyank
  • Loading branch information
AlexPl292 authored Jul 27, 2020
2 parents 0e8a1bf + 041f6af commit 3586358
Show file tree
Hide file tree
Showing 9 changed files with 474 additions and 1 deletion.
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 [To Be Released]

* 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,196 @@
/*
* 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.Disposable
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.intellij.openapi.util.Disposer
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.VimProjectService
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.ex.vimscript.VimScriptGlobalEnvironment
import com.maddyhome.idea.vim.extension.VimExtension
import com.maddyhome.idea.vim.listener.VimInsertListener
import com.maddyhome.idea.vim.listener.VimYankListener
import com.maddyhome.idea.vim.option.StrictMode
import java.awt.Color
import java.awt.Font
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

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"
*
* 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, VimYankListener, VimInsertListener {
private val highlightHandler = HighlightHandler()

override fun getName() = "highlightedyank"

override fun init() {
VimPlugin.getYank().addListener(this)
VimPlugin.getChange().addInsertListener(this)
}

override fun dispose() {
VimPlugin.getYank().removeListener(this)
VimPlugin.getChange().removeInsertListener(this)
}

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

override fun insertModeStarted(editor: Editor) {
highlightHandler.clearAllYankHighlighters()
}

private class HighlightHandler {
private var editor: Editor? = null
private val yankHighlighters: MutableSet<RangeHighlighter> = mutableSetOf()

fun highlightYankRange(editor: Editor, range: TextRange) {
//from vim-highlightedyank docs: When a new text is yanked or user starts editing, the old highlighting would be deleted
clearAllYankHighlighters()

this.editor = editor
val project = editor.project
if (project != null) {
Disposer.register(VimProjectService.getInstance(project), Disposable {
this.editor = null
yankHighlighters.clear()
})
}

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 { highlighter ->
editor?.markupModel?.removeHighlighter(highlighter) ?: StrictMode.fail("Highlighters without an editor")
}

yankHighlighters.clear()
}

private fun highlightSingleRange(editor: Editor, range: ClosedRange<Int>) {
val highlighter = editor.markupModel.addRangeHighlighter(
range.start,
range.endInclusive,
HighlighterLayer.SELECTION,
getHighlightTextAttributes(),
HighlighterTargetArea.EXACT_RANGE
)

yankHighlighters.add(highlighter)

setClearHighlightRangeTimer(highlighter)
}

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

//from vim-highlightedyank docs: A negative number makes the highlight persistent.
if(timeout >= 0) {
Executors.newSingleThreadScheduledExecutor().schedule({
ApplicationManager.getApplication().invokeLater {
editor?.markupModel?.removeHighlighter(highlighter) ?: StrictMode.fail("Highlighters without an editor")
}
}, timeout, TimeUnit.MILLISECONDS)
}
}

private fun getHighlightTextAttributes() = TextAttributes(
null,
extractUsersHighlightColor(),
editor?.colorsScheme?.getColor(EditorColors.CARET_COLOR),
EffectType.SEARCH_MATCH,
Font.PLAIN
)

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
}
}
}
18 changes: 18 additions & 0 deletions src/com/maddyhome/idea/vim/group/ChangeGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.maddyhome.idea.vim.EventFacade;
import com.maddyhome.idea.vim.KeyHandler;
import com.maddyhome.idea.vim.RegisterActions;
Expand All @@ -61,6 +62,7 @@
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase;
import com.maddyhome.idea.vim.helper.*;
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor;
import com.maddyhome.idea.vim.listener.VimInsertListener;
import com.maddyhome.idea.vim.listener.VimListenerSuppressor;
import com.maddyhome.idea.vim.option.BoundListOption;
import com.maddyhome.idea.vim.option.OptionsManager;
Expand Down Expand Up @@ -92,6 +94,8 @@ public class ChangeGroup {

private @Nullable Command lastInsert;

private List<VimInsertListener> insertListeners = ContainerUtil.createLockFreeCopyOnWriteList();

private void setInsertRepeat(int lines, int column, boolean append) {
repeatLines = lines;
repeatColumn = column;
Expand Down Expand Up @@ -424,6 +428,8 @@ private void initInsert(@NotNull Editor editor, @NotNull DataContext context, @N

VisualGroupKt.updateCaretState(editor);
}

notifyListeners(editor);
}

// Workaround for VIM-1546. Another solution is highly appreciated.
Expand Down Expand Up @@ -1986,6 +1992,18 @@ else if (SearchHelper.NumberType.DEC.equals(numberType)) {
return number;
}

public void addInsertListener(VimInsertListener listener) {
insertListeners.add(listener);
}

public void removeInsertListener(VimInsertListener listener) {
insertListeners.remove(listener);
}

private void notifyListeners(Editor editor) {
insertListeners.forEach(listener -> listener.insertModeStarted(editor));
}

private int oldOffset = -1;

private class InsertActionsDocumentListener implements DocumentListener {
Expand Down
14 changes: 14 additions & 0 deletions src/com/maddyhome/idea/vim/group/copy/YankGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package com.maddyhome.idea.vim.group.copy
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.util.containers.ContainerUtil
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.action.motion.updown.MotionDownLess1FirstNonSpaceAction
import com.maddyhome.idea.vim.command.Argument
Expand All @@ -29,11 +30,22 @@ import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.group.MotionGroup
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.fileSize
import com.maddyhome.idea.vim.listener.VimYankListener
import org.jetbrains.annotations.Contract
import java.util.*
import kotlin.math.min

class YankGroup {
private val yankListeners: MutableList<VimYankListener> = ContainerUtil.createLockFreeCopyOnWriteList();

fun addListener(listener: VimYankListener) = yankListeners.add(listener)

fun removeListener(listener: VimYankListener) = yankListeners.remove(listener)

private fun notifyListeners(editor: Editor, textRange: TextRange) = yankListeners.forEach {
it.yankPerformed(editor, textRange)
}

/**
* This yanks the text moved over by the motion command argument.
*
Expand Down Expand Up @@ -172,6 +184,8 @@ class YankGroup {
startOffsets: Map<Caret, Int>?): Boolean {
startOffsets?.forEach { caret, offset -> MotionGroup.moveCaret(editor, caret, offset) }

notifyListeners(editor, range)

return VimPlugin.getRegister().storeText(editor, range, type, false)
}
}
25 changes: 25 additions & 0 deletions src/com/maddyhome/idea/vim/listener/VimInsertListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.listener

import com.intellij.openapi.editor.Editor

interface VimInsertListener {
fun insertModeStarted(editor: Editor)
}
26 changes: 26 additions & 0 deletions src/com/maddyhome/idea/vim/listener/VimYankListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.listener

import com.intellij.openapi.editor.Editor
import com.maddyhome.idea.vim.common.TextRange

interface VimYankListener {
fun yankPerformed(editor: Editor, range: TextRange)
}
10 changes: 9 additions & 1 deletion test/org/jetbrains/plugins/ideavim/TestHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,17 @@ fun waitAndAssertMode(fixture: CodeInsightTestFixture, mode: CommandState.Mode,
fun assertDoesntChange(timeInMillis: Int = 1000, condition: () -> Boolean) {
val end = System.currentTimeMillis() + timeInMillis
while (end > System.currentTimeMillis()) {
if (!condition()) fail()
if (!condition()) {
fail()
}

Thread.sleep(10)
IdeEventQueue.getInstance().flushQueue()
}
}

fun assertHappened(timeInMillis: Int = 1000, precision: Int, condition: () -> Boolean) {
assertDoesntChange(timeInMillis - precision) { !condition() }

waitAndAssert(precision * 2) { condition() }
}
Loading

0 comments on commit 3586358

Please sign in to comment.