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 all 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 [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({
KostkaBrukowa marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -421,6 +425,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 @@ -1968,6 +1974,18 @@ else if (ch == '-' || Character.isDigit(ch)) {
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