1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-08-10 06:40:37 +02:00

Introduce listener for global option changes

This commit is contained in:
Matt Ellis 2023-04-28 17:37:41 +01:00 committed by Alex Pláte
parent e6e4b81f3b
commit c8c9d1729e
12 changed files with 249 additions and 35 deletions
src
main/java/com/maddyhome/idea/vim
test/java/org/jetbrains/plugins/ideavim
vim-engine/src/main/kotlin/com/maddyhome/idea/vim

View File

@ -15,12 +15,10 @@ import com.maddyhome.idea.vim.api.VimExtensionRegistrator
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.setToggleOption
import com.maddyhome.idea.vim.key.MappingOwner.Plugin.Companion.remove
import com.maddyhome.idea.vim.options.OptionChangeListener
import com.maddyhome.idea.vim.options.OptionDeclaredScope
import com.maddyhome.idea.vim.options.OptionScope
import com.maddyhome.idea.vim.options.ToggleOption
import com.maddyhome.idea.vim.statistic.PluginState
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt
internal object VimExtensionRegistrar : VimExtensionRegistrator {
internal val registeredExtensions: MutableSet<String> = HashSet()
@ -61,19 +59,14 @@ internal object VimExtensionRegistrar : VimExtensionRegistrator {
registerAliases(extensionBean)
val option = ToggleOption(name, OptionDeclaredScope.GLOBAL, getAbbrev(name), false)
VimPlugin.getOptionGroup().addOption(option)
VimPlugin.getOptionGroup().addListener(
option,
object : OptionChangeListener<VimInt> {
override fun processGlobalValueChange(oldValue: VimInt?) {
if (injector.optionGroup.getOptionValue(option, OptionScope.GLOBAL).asBoolean()) {
initExtension(extensionBean, name)
PluginState.enabledExtensions.add(name)
} else {
extensionBean.instance.dispose()
}
}
},
)
VimPlugin.getOptionGroup().addGlobalOptionChangeListener(option) {
if (injector.optionGroup.getOptionValue(option, OptionScope.GLOBAL).asBoolean()) {
initExtension(extensionBean, name)
PluginState.enabledExtensions.add(name)
} else {
extensionBean.instance.dispose()
}
}
}
private fun getAbbrev(name: String): String {

View File

@ -36,7 +36,7 @@ import com.maddyhome.idea.vim.history.HistoryConstants;
import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext;
import com.maddyhome.idea.vim.newapi.IjVimCaret;
import com.maddyhome.idea.vim.newapi.IjVimEditor;
import com.maddyhome.idea.vim.options.OptionChangeListener;
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener;
import com.maddyhome.idea.vim.regexp.CharPointer;
import com.maddyhome.idea.vim.regexp.CharacterClasses;
import com.maddyhome.idea.vim.regexp.RegExp;
@ -70,18 +70,18 @@ import static com.maddyhome.idea.vim.register.RegisterConstants.LAST_SEARCH_REGI
})
public class SearchGroup extends VimSearchGroupBase implements PersistentStateComponent<Element> {
public SearchGroup() {
VimPlugin.getOptionGroup().addListener(Options.hlsearch, oldValue -> {
VimPlugin.getOptionGroup().addGlobalOptionChangeListener(Options.hlsearch, () -> {
resetShowSearchHighlight();
forceUpdateSearchHighlights();
}, false);
});
final OptionChangeListener<VimInt> updateHighlightsIfVisible = oldValue -> {
final GlobalOptionChangeListener updateHighlightsIfVisible = () -> {
if (showSearchHighlight) {
forceUpdateSearchHighlights();
}
};
VimPlugin.getOptionGroup().addListener(Options.ignorecase, updateHighlightsIfVisible, false);
VimPlugin.getOptionGroup().addListener(Options.smartcase, updateHighlightsIfVisible, false);
VimPlugin.getOptionGroup().addGlobalOptionChangeListener(Options.ignorecase, updateHighlightsIfVisible);
VimPlugin.getOptionGroup().addGlobalOptionChangeListener(Options.smartcase, updateHighlightsIfVisible);
}
public void turnOn() {

View File

@ -31,9 +31,12 @@ import com.maddyhome.idea.vim.undo.UndoRedoBase
@Service
internal class UndoRedoHelper : UndoRedoBase() {
init {
injector.optionGroup.addListener(IjOptions.oldundo, {
fun onOldUndoChanged() {
UndoManagerImpl.ourNeverAskUser = !injector.globalIjOptions().oldundo
}, true)
}
injector.optionGroup.addGlobalOptionChangeListener(IjOptions.oldundo, ::onOldUndoChanged)
onOldUndoChanged()
}
override fun undo(editor: VimEditor, context: ExecutionContext): Boolean {

View File

@ -114,7 +114,7 @@ internal object VimListenerManager {
optionGroup.addListener(Options.number, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.addListener(Options.relativenumber, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.addListener(Options.scrolloff, ScrollGroup.ScrollOptionsChangeListener)
optionGroup.addListener(Options.showcmd, ShowCmdOptionChangeListener)
optionGroup.addGlobalOptionChangeListener(Options.showcmd, ShowCmdOptionChangeListener)
optionGroup.addListener(Options.guicursor, GuicursorChangeListener)
EventFacade.getInstance().addEditorFactoryListener(VimEditorFactoryListener, VimPlugin.getInstance().onOffDisposable)
@ -129,7 +129,7 @@ internal object VimListenerManager {
optionGroup.removeListener(Options.number, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.removeListener(Options.relativenumber, EditorGroup.NumberChangeListener.INSTANCE)
optionGroup.removeListener(Options.scrolloff, ScrollGroup.ScrollOptionsChangeListener)
optionGroup.removeListener(Options.showcmd, ShowCmdOptionChangeListener)
optionGroup.removeGlobalOptionChangeListener(Options.showcmd, ShowCmdOptionChangeListener)
optionGroup.removeListener(Options.guicursor, GuicursorChangeListener)
}
}

View File

@ -27,8 +27,7 @@ import com.maddyhome.idea.vim.helper.EngineStringHelper
import com.maddyhome.idea.vim.helper.VimNlsSafe
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.OptionChangeListener
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
import org.jetbrains.annotations.NonNls
import java.awt.Component
import java.awt.event.MouseEvent
@ -65,8 +64,8 @@ internal object ShowCmd {
}
}
internal object ShowCmdOptionChangeListener : OptionChangeListener<VimInt> {
override fun processGlobalValueChange(oldValue: VimInt?) {
internal object ShowCmdOptionChangeListener : GlobalOptionChangeListener {
override fun onGlobalOptionChanged() {
ShowCmd.update()
val extension = StatusBarWidgetFactory.EP_NAME.findExtension(ShowCmdStatusBarWidgetFactory::class.java) ?: return

View File

@ -72,7 +72,7 @@ internal class StatusBarIconFactory : StatusBarWidgetFactory/*, LightEditCompati
}
override fun createWidget(project: Project): StatusBarWidget {
VimPlugin.getOptionGroup().addListener(IjOptions.ideastatusicon, { updateAll() })
VimPlugin.getOptionGroup().addGlobalOptionChangeListener(IjOptions.ideastatusicon) { updateAll() }
return VimStatusBar()
}

View File

@ -16,6 +16,7 @@ import com.maddyhome.idea.vim.api.VimInjector
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.group.IjVimOptionGroup
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
import com.maddyhome.idea.vim.options.NumberOption
import com.maddyhome.idea.vim.options.Option
import com.maddyhome.idea.vim.options.OptionChangeListener
@ -207,6 +208,32 @@ internal class OptionsTracer(
return vimOptionGroup.getOptionValue(option, scope)
}
override fun <T : VimDataType> addGlobalOptionChangeListener(
option: Option<T>,
listener: GlobalOptionChangeListener
) {
ignoreFlag.set(true)
try {
vimOptionGroup.addGlobalOptionChangeListener(option, listener)
}
finally {
ignoreFlag.set(false)
}
}
override fun <T : VimDataType> removeGlobalOptionChangeListener(
option: Option<T>,
listener: GlobalOptionChangeListener
) {
ignoreFlag.set(true)
try {
vimOptionGroup.removeGlobalOptionChangeListener(option, listener)
}
finally {
ignoreFlag.set(false)
}
}
override fun <T : VimDataType> addListener(option: Option<T>, listener: OptionChangeListener<T>, executeOnAdd: Boolean) {
ignoreFlag.set(true)
try {

View File

@ -0,0 +1,126 @@
/*
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package org.jetbrains.plugins.ideavim.option
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
import com.maddyhome.idea.vim.options.OptionDeclaredScope
import com.maddyhome.idea.vim.options.OptionScope
import com.maddyhome.idea.vim.options.StringOption
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInfo
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertTrue
@TestWithoutNeovim(reason = SkipNeovimReason.OPTION)
class GlobalOptionChangeListenerTest: VimTestCase() {
private object Listener : GlobalOptionChangeListener {
var called = false
override fun onGlobalOptionChanged() {
called = true
}
}
@BeforeEach
override fun setUp(testInfo: TestInfo) {
super.setUp(testInfo)
Listener.called = false
}
@Test
fun `test listener called when global option changes`() {
val option = StringOption("test", OptionDeclaredScope.GLOBAL, "test", "defaultValue")
try {
injector.optionGroup.addOption(option)
injector.optionGroup.addGlobalOptionChangeListener(option, Listener)
injector.optionGroup.setOptionValue(option, OptionScope.GLOBAL, VimString("newValue"))
assertTrue(Listener.called)
}
finally {
injector.optionGroup.removeGlobalOptionChangeListener(option, Listener)
injector.optionGroup.removeOption(option.name)
}
}
@Test
fun `test listener called when effective value of global option changes`() {
configureByText("\n")
val option = StringOption("test", OptionDeclaredScope.GLOBAL, "test", "defaultValue")
try {
injector.optionGroup.addOption(option)
injector.optionGroup.addGlobalOptionChangeListener(option, Listener)
injector.optionGroup.setOptionValue(option, OptionScope.AUTO(fixture.editor.vim), VimString("newValue"))
assertTrue(Listener.called)
}
finally {
injector.optionGroup.removeGlobalOptionChangeListener(option, Listener)
injector.optionGroup.removeOption(option.name)
}
}
@Test
fun `test listener called when local value of global option changes`() {
configureByText("\n")
val option = StringOption("test", OptionDeclaredScope.GLOBAL, "test", "defaultValue")
try {
injector.optionGroup.addOption(option)
injector.optionGroup.addGlobalOptionChangeListener(option, Listener)
injector.optionGroup.setOptionValue(option, OptionScope.LOCAL(fixture.editor.vim), VimString("newValue"))
assertTrue(Listener.called)
}
finally {
injector.optionGroup.removeGlobalOptionChangeListener(option, Listener)
injector.optionGroup.removeOption(option.name)
}
}
@Test
fun `test cannot register listener for local option`() {
val option = StringOption("test", OptionDeclaredScope.LOCAL_TO_BUFFER, "test", "defaultValue")
try {
injector.optionGroup.addOption(option)
assertThrows<IllegalStateException> {
injector.optionGroup.addGlobalOptionChangeListener(option, Listener)
}
}
finally {
injector.optionGroup.removeOption(option.name)
}
}
@Test
fun `test cannot register listener for global-local option`() {
val option = StringOption("test", OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_WINDOW, "test", "defaultValue")
try {
injector.optionGroup.addOption(option)
assertThrows<IllegalStateException> {
injector.optionGroup.addGlobalOptionChangeListener(option, Listener)
}
}
finally {
injector.optionGroup.removeOption(option.name)
}
}
}

View File

@ -8,8 +8,10 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
import com.maddyhome.idea.vim.options.Option
import com.maddyhome.idea.vim.options.OptionChangeListener
import com.maddyhome.idea.vim.options.OptionDeclaredScope
import com.maddyhome.idea.vim.options.OptionScope
import com.maddyhome.idea.vim.options.StringListOption
import com.maddyhome.idea.vim.options.ToggleOption
@ -120,6 +122,27 @@ public interface VimOptionGroup {
*/
public fun removeOption(optionName: String)
/**
* Add a listener for when a global option value changes
*
* This listener will get called once when a global option's value changes. It is intended for non-editor features,
* such as updating the status bar widget for `'showcmd'` or updating the default register when `'clipboard'` changes.
* It can only be used for global options, and will not be called when the global value of a local-to-buffer or
* local-to-window option is changed. It is also not called when a global-local option is changed.
*
* @param option The option to listen to for changes. It must be a [OptionDeclaredScope.GLOBAL] option
* @param listener The listener that will be invoked when the global option changes
*/
public fun <T : VimDataType> addGlobalOptionChangeListener(option: Option<T>, listener: GlobalOptionChangeListener)
/**
* Remove a global option change listener
*
* @param option The global option that has previously been subscribed to
* @param listener The listener to remove
*/
public fun <T : VimDataType> removeGlobalOptionChangeListener(option: Option<T>, listener: GlobalOptionChangeListener)
/**
* Adds a listener to the option.
* @param option the option

View File

@ -8,9 +8,11 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
import com.maddyhome.idea.vim.options.NumberOption
import com.maddyhome.idea.vim.options.Option
import com.maddyhome.idea.vim.options.OptionChangeListener
import com.maddyhome.idea.vim.options.OptionDeclaredScope
import com.maddyhome.idea.vim.options.OptionDeclaredScope.GLOBAL
import com.maddyhome.idea.vim.options.OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_BUFFER
import com.maddyhome.idea.vim.options.OptionDeclaredScope.GLOBAL_OR_LOCAL_TO_WINDOW
@ -24,6 +26,7 @@ public abstract class VimOptionGroupBase : VimOptionGroup {
private val globalOptionsAccessor = GlobalOptions()
private val globalValues = mutableMapOf<String, VimDataType>()
private val globalParsedValues = mutableMapOf<String, Any>()
private val globalOptionListeners = mutableMapOf<String, MutableSet<GlobalOptionChangeListener>>()
private val localOptionsKey = Key<MutableMap<String, VimDataType>>("localOptions")
private val parsedEffectiveValueKey = Key<MutableMap<String, Any>>("parsedEffectiveOptionValues")
@ -126,6 +129,10 @@ public abstract class VimOptionGroupBase : VimOptionGroup {
OptionScope.GLOBAL -> setGlobalOptionValue(option, value)
}
if (option.declaredScope == GLOBAL) {
onGlobalOptionValueChanged(option)
}
if (oldValue != value) {
option.onChanged(scope, oldValue)
}
@ -198,6 +205,21 @@ public abstract class VimOptionGroupBase : VimOptionGroup {
Options.removeOption(optionName)
}
override fun <T : VimDataType> addGlobalOptionChangeListener(
option: Option<T>,
listener: GlobalOptionChangeListener
) {
check(option.declaredScope == OptionDeclaredScope.GLOBAL)
getGlobalOptionListeners(option).add(listener)
}
override fun <T : VimDataType> removeGlobalOptionChangeListener(
option: Option<T>,
listener: GlobalOptionChangeListener
) {
getGlobalOptionListeners(option).remove(listener)
}
override fun <T : VimDataType> addListener(
option: Option<T>,
listener: OptionChangeListener<T>,
@ -416,6 +438,16 @@ public abstract class VimOptionGroupBase : VimOptionGroup {
}
private fun <T : VimDataType> getGlobalOptionListeners(option: Option<T>) =
globalOptionListeners.getOrPut(option.name) { mutableSetOf() }
private fun <T : VimDataType> onGlobalOptionValueChanged(option: Option<T>) {
globalOptionListeners[option.name]?.forEach {
it.onGlobalOptionChanged()
}
}
private fun getParsedEffectiveOptionStorage(option: Option<out VimDataType>, editor: VimEditor): MutableMap<String, Any> {
return when (option.declaredScope) {
LOCAL_TO_WINDOW, GLOBAL_OR_LOCAL_TO_WINDOW -> {

View File

@ -10,6 +10,18 @@ package com.maddyhome.idea.vim.options
import com.maddyhome.idea.vim.api.VimEditor
/**
* Listener for changes to the value of a global option
*
* This listener will only be called when a global option's value is changed. It is intended for non-editor related
* options. That is, options that don't need to refresh editor(s) when the global value changes. For example, updating
* the status bar widget when `'showcmd'` changes, or updating the default register when `'clipboard'` changes, or
* enabling/disabling extensions.
*/
public fun interface GlobalOptionChangeListener {
public fun onGlobalOptionChanged()
}
public fun interface OptionChangeListener<in T> {
public fun processGlobalValueChange(oldValue: T?)

View File

@ -33,8 +33,6 @@ import com.maddyhome.idea.vim.register.RegisterConstants.SMALL_DELETION_REGISTER
import com.maddyhome.idea.vim.register.RegisterConstants.UNNAMED_REGISTER
import com.maddyhome.idea.vim.register.RegisterConstants.VALID_REGISTERS
import com.maddyhome.idea.vim.register.RegisterConstants.WRITABLE_REGISTERS
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import java.lang.RuntimeException
import javax.swing.KeyStroke
public abstract class VimRegisterGroupBase : VimRegisterGroup {
@ -73,7 +71,7 @@ public abstract class VimRegisterGroupBase : VimRegisterGroup {
override val lastRegister: Register?
get() = getRegister(lastRegisterChar)
private val listener: (oldValue: VimString?) -> Unit = {
private val onClipboardChanged: () -> Unit = {
val clipboardOptionValue = injector.globalOptions().clipboard
defaultRegisterChar = when {
"unnamedplus" in clipboardOptionValue && isPrimaryRegisterSupported() -> PRIMARY_REGISTER
@ -84,11 +82,12 @@ public abstract class VimRegisterGroupBase : VimRegisterGroup {
}
init {
injector.optionGroup.addListener(Options.clipboard, listener, true)
injector.optionGroup.addGlobalOptionChangeListener(Options.clipboard, onClipboardChanged)
onClipboardChanged()
}
public fun clearListener() {
injector.optionGroup.removeListener(Options.clipboard, listener)
injector.optionGroup.removeGlobalOptionChangeListener(Options.clipboard, onClipboardChanged)
}
override fun isValid(reg: Char): Boolean = VALID_REGISTERS.indexOf(reg) != -1