1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-02-27 17:45:59 +01:00

Add fallback window to capture local option state

This commit is contained in:
Matt Ellis 2023-08-09 01:00:48 +01:00 committed by Alex Pláte
parent 0f19e50c69
commit 93037b6866
6 changed files with 250 additions and 82 deletions
src/main/java/com/maddyhome/idea/vim
vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api

View File

@ -40,6 +40,7 @@ import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimKeyListener
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.VimTypedActionHandler
import com.maddyhome.idea.vim.api.LocalOptionInitialisationScenario
import com.maddyhome.idea.vim.api.Options
import com.maddyhome.idea.vim.api.getLineEndForOffset
import com.maddyhome.idea.vim.api.getLineStartForOffset
@ -72,6 +73,7 @@ import com.maddyhome.idea.vim.listener.MouseEventsDataHolder.skipEvents
import com.maddyhome.idea.vim.listener.MouseEventsDataHolder.skipNDragEvents
import com.maddyhome.idea.vim.listener.VimListenerManager.EditorListeners.add
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.mode
@ -156,9 +158,16 @@ internal object VimListenerManager {
Disposer.register(disposable) { editor.contentComponent.removeKeyListener(VimKeyListener) }
// Initialise the local options. We MUST do this before anything has the chance to query options
val sourceEditor = getOpeningEditor(editor)
val isSplit = editor.document == sourceEditor?.document
VimPlugin.getOptionGroup().initialiseLocalOptions(editor.vim, sourceEditor?.vim, isSplit)
val sourceEditor = getOpeningEditor(editor)?.vim
// Note that IdeaVim implements `:edit {file}` as `:new {file}` and doesn't implement `:new`, so the only scenario
// we can handle here is NEW
val scenario = when {
sourceEditor == null -> LocalOptionInitialisationScenario.FALLBACK
editor.document == sourceEditor.ij.document -> LocalOptionInitialisationScenario.SPLIT
else -> LocalOptionInitialisationScenario.NEW
}
VimPlugin.getOptionGroup().initialiseLocalOptions(editor.vim, sourceEditor ?: injector.fallbackWindow, scenario)
val eventFacade = EventFacade.getInstance()
eventFacade.addEditorMouseListener(editor, EditorMouseHandler, disposable)

View File

@ -12,9 +12,11 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceIfCreated
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.textarea.TextComponentEditorImpl
import com.maddyhome.idea.vim.api.EngineEditorHelper
import com.maddyhome.idea.vim.api.ExEntryPanel
import com.maddyhome.idea.vim.api.ExecutionContextManager
import com.maddyhome.idea.vim.api.LocalOptionInitialisationScenario
import com.maddyhome.idea.vim.api.NativeActionManager
import com.maddyhome.idea.vim.api.SystemInfoService
import com.maddyhome.idea.vim.api.VimActionExecutor
@ -90,10 +92,17 @@ import com.maddyhome.idea.vim.vimscript.services.PatternService
import com.maddyhome.idea.vim.vimscript.services.VariableService
import com.maddyhome.idea.vim.yank.VimYankGroup
import com.maddyhome.idea.vim.yank.YankGroupBase
import javax.swing.JTextArea
internal class IjVimInjector : VimInjectorBase() {
override fun <T : Any> getLogger(clazz: Class<T>): VimLogger = IjVimLogger(Logger.getInstance(clazz))
override val fallbackWindow: VimEditor by lazy {
TextComponentEditorImpl(null, JTextArea()).vim.also {
optionGroup.initialiseLocalOptions(it, null, LocalOptionInitialisationScenario.DEFAULTS)
}
}
override val actionExecutor: VimActionExecutor
get() = service<IjActionExecutor>()
override val exEntryPanel: ExEntryPanel

View File

@ -12,6 +12,7 @@ import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.textarea.TextComponentEditorImpl
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.LocalOptionInitialisationScenario
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.newapi.vim
import java.awt.event.ActionEvent
@ -231,12 +232,15 @@ internal class DeletePreviousWordAction : TextAction(DefaultEditorKit.deletePrev
val caret = target.caret
val project = target.editor.project
// Note that we need an editor when searching because we need per-editor options (i.e. 'iskeyword'). We initialise
// it based on the owning editor, and treat it like a split, so we get a full copy of the local-to-window options
// TODO: Over time, we should migrate ex actions to be based on VimEditor
// This will give us an editor we can use for options, etc. and we can reuse the actions for other implementations
// Create a VimEditor instance on the Swing text field which we can pass to the search helpers. We need an editor
// rather than just working on a buffer because the search helpers need local options (specifically the local to
// buffer 'iskeyword'). We use the CMD_LINE scenario to initialise local options from the main editor. The options
// service will copy all local-to-buffer and local-to-window options, effectively cloning the options.
// TODO: Over time, we should migrate all ex actions to be based on VimEditor
// This will mean we always have an editor that has been initialised for options, etc. But also means that we can
// share the command line entry actions between IdeaVim implementations
val editor = TextComponentEditorImpl(project, target).vim
injector.optionGroup.initialiseLocalOptions(editor, target.editor.vim, true)
injector.optionGroup.initialiseLocalOptions(editor, target.editor.vim, LocalOptionInitialisationScenario.CMD_LINE)
val offset = injector.searchHelper.findNextWord(editor, caret.dot, -1, bigWord = false, spaceWords = false)
if (logger.isDebugEnabled) logger.debug("offset=$offset")

View File

@ -23,6 +23,20 @@ import com.maddyhome.idea.vim.vimscript.services.VariableService
import com.maddyhome.idea.vim.yank.VimYankGroup
public interface VimInjector {
/**
* The window used when we need a window but there are no editor windows available.
*
* This is primarily used to capture state for local options, either at startup, or when all editor windows are
* closed.
*
* Vim always has at least one buffer and window. During startup, Vim will evaluate the appropriate `vimrc` files, and
* any local or global-local options are set against this initial buffer and window. IdeaVim does not always have an
* open buffer or window, so we create a hidden window, with a private buffer that can be used when evaluating the
* `~/.ideavimrc` file, and updated with the last set local options of the current window. This window (and buffer) is
* then used to initialise the local options of the first window that is subsequently opened or initialised.
*/
public val fallbackWindow: VimEditor
// [FINISHED] Fully moved to vim-engine. Should we remove it from injector?
public val parser: VimStringParser

View File

@ -11,8 +11,8 @@ package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.options.EffectiveOptionValueChangeListener
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
import com.maddyhome.idea.vim.options.Option
import com.maddyhome.idea.vim.options.OptionDeclaredScope
import com.maddyhome.idea.vim.options.OptionAccessScope
import com.maddyhome.idea.vim.options.OptionDeclaredScope
import com.maddyhome.idea.vim.options.StringListOption
import com.maddyhome.idea.vim.options.ToggleOption
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType
@ -30,22 +30,20 @@ public interface VimOptionGroup {
/**
* Initialise the local to buffer and local to window options for this editor
*
* Local to buffer options are copied from the current global values, while local to window options should be copied
* from the per-window "global" values of the editor that caused this editor to open. Both of these global values are
* updated by the `:set` or `:setglobal` commands.
*
* Note that global-local options are not copied from the source window. They are global values that are overridden
* locally, and local values are never copied.
* Depending on the initialisation scenario, the local-to-buffer, local-to-window and/or global-local options are
* initialised. The scenario dictates where the local options get their values from. Typically, local-to-buffer
* options are copied from the global values. Local-to-window options are either initialised from the per-window
* "global" value or copied directly from the opening window. Global-local options are usually not initialised.
*
* TODO: IdeaVim currently does not support per-window "global" values
*
* @param editor The editor to initialise
* @param sourceEditor The editor which is opening the new editor. This source editor is used to get the per-window
* "global" values to initialise the new editor. If null, there is no source editor (e.g. all
* editor windows are closed), and the options should be initialised to some other value.
* @param isSplit True if the new editor is a split view of the source editor
* editor windows are closed), and the options should be initialised to default values.
* @param scenario The scenario for initialising the local options
*/
public fun initialiseLocalOptions(editor: VimEditor, sourceEditor: VimEditor?, isSplit: Boolean)
public fun initialiseLocalOptions(editor: VimEditor, sourceEditor: VimEditor?, scenario: LocalOptionInitialisationScenario)
/**
* Get the [Option] by its name or abbreviation
@ -259,4 +257,78 @@ public fun VimOptionGroup.invertToggleOption(option: ToggleOption, scope: Option
public fun VimOptionGroup.hasValue(option: StringListOption, scope: OptionAccessScope, value: String): Boolean {
val optionValue = getOptionValue(option, scope)
return option.split(optionValue.asString()).contains(value)
}
}
/**
* The scenario for initialising local options
*/
public enum class LocalOptionInitialisationScenario {
/**
* Set the local options to default (global) values.
*/
DEFAULTS,
/**
* The new window is being initialised with the values of the fallback window
*
* Vim always has at least one buffer and window open, and the `vimrc` files are evaluated in this context. Any
* options set during evaluation are applied to the first open window and buffer, as if the user had interactively
* typed them in. IdeaVim does not always have an open window (and therefore buffer), so we evaluate `~/.ideavimrc` in
* a special, hidden "fallback" window, that is always available even if there are no editor windows. This fallback
* window is used to initialise the first editor window.
*
* Since Vim will evaluate `vimrc` in the context of the first window, any local-to-buffer options are set against the
* first window's buffer. Therefore, this scenario will copy buffer and window local values, including global-local
* values, and the per-window "global" values of local-to-window options.
*/
FALLBACK,
/**
* The new window is a split of the opening/current window
*
* In this scenario, Vim is trying to make the new window behave exactly like the opening window, so will copy both
* local and per-window "global" values of local-to-window and global-local (to window) options from the opening
* window to the new window. Local-to-buffer windows are obviously already initialised and not modified.
*/
SPLIT,
/**
* The user has opened a new buffer in the current window
*
* This scenario is not currently supported by IdeaVim.
*
* This is the `:edit {file}` command, where the current window is reused to edit a new or previously edited buffer.
* Vim will reset any explicitly set local-to-window values. The local-to-buffer options are initialised for a new
* buffer, by copying from the global values. Local-to-window values are reset to the existing per-window "global"
* values.
*/
EDIT,
/**
* The user has opened a new window
*
* This is Vim's `:new {file}` command, which will open a new or existing buffer in a new window. Vim treats this as
* a split followed by `:edit`, which means copying local and per-window "global" local-to-window option values from
* the opening window and then resetting any explicitly set local-to-window options to the per-window "global" values.
*
* Note that this scenario is used for IdeaVim's current implementation of the `:edit {file}` command.
*/
NEW,
/**
* Initialise the [VimEditor] used for the `ex` command line text field
*
* Vim doesn't really have the concept of "editor". It has a window, which is a view on a buffer, and which can edit
* the text of the buffer. The `ex` command line and search text entry are implemented as part of this window, and
* therefore automatically uses the window's local options (e.g. search requires `'iskeyword'`)
*
* For IdeaVim, the `ex`/search text entry is a separate UI component to the main editor, implements [VimEditor] and
* so needs its own copy of the local options. This scenario makes a full copy of the local to buffer and local to
* window options, so has the same effect as [FALLBACK].
*
* We need to migrate more of the command line text handling to work with a [VimEditor]-based implementation (it's
* currently very heavily based on Swing). As part of the implementation detail, we could look at sharing options
* instead of copying them.
*/
CMD_LINE
}

View File

@ -12,12 +12,12 @@ import com.maddyhome.idea.vim.options.EffectiveOptionValueChangeListener
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.OptionAccessScope
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
import com.maddyhome.idea.vim.options.OptionDeclaredScope.LOCAL_TO_BUFFER
import com.maddyhome.idea.vim.options.OptionDeclaredScope.LOCAL_TO_WINDOW
import com.maddyhome.idea.vim.options.OptionAccessScope
import com.maddyhome.idea.vim.options.ToggleOption
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType
@ -34,12 +34,82 @@ public abstract class VimOptionGroupBase : VimOptionGroup {
Options.initialise()
}
override fun initialiseLocalOptions(editor: VimEditor, sourceEditor: VimEditor?, isSplit: Boolean) {
// Initialise local-to-buffer options
// They are stored per-buffer, so shared across all editors for the buffer. If the key exists, they've previously
// been initialised, else initialise the options from the global values, which is always the most recently set value
// (`:set` on a buffer-local option will set the local value, but it also sets the global value, for exactly this
// reason)
override fun initialiseLocalOptions(editor: VimEditor, sourceEditor: VimEditor?, scenario: LocalOptionInitialisationScenario) {
when (scenario) {
// We're initialising the first (hidden) editor. Vim always has at least one window (and buffer); IdeaVim doesn't.
// We fake this with a hidden editor that is created when the plugin first starts. It is used to capture state
// when initially evaluating `~/.ideavimrc`, and then used to initialise subsequent windows. But first, we must
// initialise all local options to the default (global) values
LocalOptionInitialisationScenario.DEFAULTS -> {
check(sourceEditor == null) { "sourceEditor must be null for DEFAULTS scenario" }
initialiseLocalToBufferOptions(editor)
initialiseLocalToWindowOptions(editor)
}
// The opening window is either:
// a) the special initialisation window used to evaluate `~/.ideavimrc` during initialisation and potentially
// before any windows are open or
// b) the "ex" or search command line text field/editor associated with a main editor
// Either way, the target window should be a clone of the source window, copying local to buffer and local to
// window values
LocalOptionInitialisationScenario.FALLBACK,
LocalOptionInitialisationScenario.CMD_LINE -> {
check(sourceEditor != null) { "sourceEditor must not be null for IDEAVIMRC or CMD_LINE scenarios" }
copyLocalToBufferLocalValues(editor, sourceEditor)
copyLocalToWindowLocalValues(editor, sourceEditor)
copyLocalToWindowGlobalValues(editor, sourceEditor)
}
// The opening/current window is being split. Clone the local-to-window options, both the local values and the
// per-window "global" values. The buffer local options are obviously already initialised
LocalOptionInitialisationScenario.SPLIT -> {
check(sourceEditor != null) { "sourceEditor must not be null for SPLIT scenario" }
initialiseLocalToBufferOptions(editor) // Should be a no-op
copyLocalToWindowLocalValues(editor, sourceEditor)
copyLocalToWindowGlobalValues(editor, sourceEditor)
}
// Editing a new buffer in the current window (`:edit {file}`). Remove explicitly set local values, which means to
// copy the per-window "global" value of local-to-window options to the local value, and to reset all window
// global-local options. Since it's a new buffer, we initialise buffer local options.
// Note that IdeaVim does not support this scenario because it implements `:edit {file}` as `:new {file}`
LocalOptionInitialisationScenario.EDIT -> {
check(sourceEditor != null) { "sourceEditor must not be null for EDIT scenario" }
initialiseLocalToBufferOptions(editor)
resetLocalToWindowOptions(editor)
}
// Editing a new buffer in a new window (`:new {file}`). Vim treats this as a split followed by an edit. That
// means, clone the window, then reset its local values to its global values
LocalOptionInitialisationScenario.NEW -> {
check(sourceEditor != null) { "sourceEditor must not be null for NEW scenario" }
initialiseLocalToBufferOptions(editor)
copyLocalToWindowLocalValues(editor, sourceEditor) // Technically redundant
copyLocalToWindowGlobalValues(editor, sourceEditor)
resetLocalToWindowOptions(editor)
}
}
}
private fun copyLocalToBufferLocalValues(targetEditor: VimEditor, sourceEditor: VimEditor) {
val localValues = getBufferLocalOptionStorage(targetEditor)
getAllOptions().forEach { option ->
if (option.declaredScope == LOCAL_TO_BUFFER || option.declaredScope == GLOBAL_OR_LOCAL_TO_BUFFER) {
localValues[option.name] = getBufferLocalOptionValue(option, sourceEditor)
}
}
}
/**
* Initialise local-to-buffer options by copying the global value
*
* Note that the buffer might have been previously initialised. The global value is the most recently set value,
* across any buffer and any window. This makes most sense for non-visible options - the user always gets what they
* last set, regardless of where it was set.
*
* Remember that `:set` on a buffer-local option will set both the local value and the global value.
*/
private fun initialiseLocalToBufferOptions(editor: VimEditor) {
injector.vimStorageService.getOrPutBufferData(editor, localOptionsKey) {
mutableMapOf<String, VimDataType>().also { bufferOptions ->
getAllOptions().forEach { option ->
@ -51,67 +121,57 @@ public abstract class VimOptionGroupBase : VimOptionGroup {
}
}
}
}
// TODO: We don't support per-window "global" values right now
// These functions are here so we know what the semantics should be when it comes time to implement.
// Default to getting the per-instance global value for now (per-instance meaning per VimOptionGroup service instance)
// Set does nothing, because it's called with the current "global" value, which would be a no-op
fun getPerWindowGlobalOptionValue(option: Option<VimDataType>, editor: VimEditor?) = getGlobalOptionValue(option)
fun setPerWindowGlobalOptionValue(option: Option<VimDataType>, editor: VimEditor, value: VimDataType) {}
// Initialising local-to-window options is a little more involved (see [OptionDeclaredScope])
// Assumptions:
// * Vim always has at least one open window. A new window or buffer is initialised from this source window
// IdeaVim does not always have an open window. The passed source window might be null.
// TODO: How does this handle `:setlocal` in `~/.ideavimrc`?
// We might need to create a dummy "root" window that evaluates `~/.ideavimrc` and is used to initialise the local
// options of other windows.
// * Vim's local-to-window options store "global" values as per-window global values.
// TODO: IdeaVim does not currently support per-window global values
// Scenarios:
// 1. Split the current window
// Vim tries to make the split an exact clone. Copy the source window's local and per-window global values to the
// new window. This applies to local-to-window and "global or local to window" options
// 2. Edit a new buffer in the current window (`:edit {file}`)
// Reapply the current window's per-window global values as local values, to get rid of explicitly local values
// IdeaVim does not currently support this scenario, because IdeaVim's implementation of `:edit` does not open
// a file in the current window. It instead behaves like `:new {file}`.
// We could implement it like the platform implements preview tabs, or reusing unmodified tabs - by opening a new
// editor and immediately closing the old one. This would still behave like `:new` - copying the per-window
// global values and applying them as local values, which would be the correct behaviour
// 3. Open a new buffer in a new window (`:new {file}`)
// Vim implements this as a split, then editing a new buffer in the new current window. This will copy the source
// window's local and per-window global values to the new split window, then reapply the new window's per-window
// global values to the new window's local options. In effect, copying the source window's per-window global
// values to the new window
// 3. Edit a previously edited buffer (in the current window)
// Vim will reapply options saved from the last window used to edit this buffer. Details are a bit sketchy - when
// are the options saved, when are the released, etc. so this scenario is not currently supported.
// IdeaVim does not support this
injector.vimStorageService.getOrPutWindowData(editor, localOptionsKey) {
mutableMapOf<String, VimDataType>().also { windowOptions ->
getAllOptions()
.filter { it.declaredScope == LOCAL_TO_WINDOW || it.declaredScope == GLOBAL_OR_LOCAL_TO_WINDOW }
.forEach { option ->
if (isSplit && sourceEditor != null) {
// Splitting the current window, make it look and behave the same as the source editor
windowOptions[option.name] = getWindowLocalOptionValue(option, sourceEditor)
setPerWindowGlobalOptionValue(option, editor, getPerWindowGlobalOptionValue(option, sourceEditor))
}
else {
// All other scenarios (open new buffer in new or current window)
windowOptions[option.name] = if (option.declaredScope == GLOBAL_OR_LOCAL_TO_WINDOW) {
option.unsetValue
}
else {
getPerWindowGlobalOptionValue(option, sourceEditor)
}
}
}
private fun copyLocalToWindowLocalValues(targetEditor: VimEditor, sourceEditor: VimEditor) {
val localValues = getWindowLocalOptionStorage(targetEditor)
getAllOptions().forEach { option ->
if (option.declaredScope == LOCAL_TO_WINDOW || option.declaredScope == GLOBAL_OR_LOCAL_TO_WINDOW) {
localValues[option.name] = getWindowLocalOptionValue(option, sourceEditor)
}
}
}
private fun copyLocalToWindowGlobalValues(targetEditor: VimEditor, sourceEditor: VimEditor) {
getAllOptions().forEach { option ->
if (option.declaredScope == LOCAL_TO_WINDOW || option.declaredScope == GLOBAL_OR_LOCAL_TO_WINDOW) {
val localValue = getPerWindowGlobalOptionValue(option, sourceEditor)
setPerWindowGlobalOptionValue(option, targetEditor, localValue)
}
}
}
private fun resetLocalToWindowOptions(editor: VimEditor) {
val localValues = getWindowLocalOptionStorage(editor)
getAllOptions().forEach { option ->
if (option.declaredScope == LOCAL_TO_WINDOW) {
localValues[option.name] = getPerWindowGlobalOptionValue(option, editor)
}
else if (option.declaredScope == GLOBAL_OR_LOCAL_TO_WINDOW) {
localValues[option.name] = option.unsetValue
}
}
}
private fun initialiseLocalToWindowOptions(editor: VimEditor) {
val localValues = getWindowLocalOptionStorage(editor)
getAllOptions().forEach { option ->
if (option.declaredScope == LOCAL_TO_WINDOW) {
localValues[option.name] = getGlobalOptionValue(option)
}
else if (option.declaredScope == GLOBAL_OR_LOCAL_TO_WINDOW) {
localValues[option.name] = option.unsetValue
}
}
}
// TODO: We don't support per-window "global" values right now
// These functions are here so we know what the semantics should be when it comes time to implement.
// Default to getting the per-instance global value for now (per-instance meaning per VimOptionGroup service instance)
// Set does nothing, because it's called with the current "global" value, which would be a no-op
private fun getPerWindowGlobalOptionValue(option: Option<VimDataType>, editor: VimEditor?) = getGlobalOptionValue(option)
private fun setPerWindowGlobalOptionValue(option: Option<VimDataType>, editor: VimEditor, value: VimDataType) {}
override fun <T : VimDataType> getOptionValue(option: Option<T>, scope: OptionAccessScope): T = when (scope) {
is OptionAccessScope.EFFECTIVE -> getEffectiveOptionValue(option, scope.editor)
is OptionAccessScope.LOCAL -> getLocalOptionValue(option, scope.editor)