From c89620570de2f17e87abe8cb8112377a0d2b8170 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Tue, 9 Jan 2024 16:00:31 +0000 Subject: [PATCH] Add 'colorcolumn' option to show visual guides IntelliJ ties the hard wrap right margin guide with the other visual guides, and it's not possible to show one without the other. In Vim, you can show the hard wrap margin by adding "+0" to 'colorcolumn', so in IdeaVim, we automatically add this. --- .../idea/vim/group/IjOptionProperties.kt | 1 + .../com/maddyhome/idea/vim/group/IjOptions.kt | 16 + .../maddyhome/idea/vim/group/OptionGroup.kt | 89 ++++ src/main/resources/dictionaries/ideavim.dic | 1 + .../implementation/commands/SetCommandTest.kt | 31 +- .../commands/SetglobalCommandTest.kt | 31 +- .../commands/SetlocalCommandTest.kt | 31 +- .../ideavim/option/ColorColumnOptionTest.kt | 387 ++++++++++++++++++ .../idea/vim/api/VimOptionGroupBase.kt | 9 +- 9 files changed, 552 insertions(+), 44 deletions(-) create mode 100644 src/test/java/org/jetbrains/plugins/ideavim/option/ColorColumnOptionTest.kt diff --git a/src/main/java/com/maddyhome/idea/vim/group/IjOptionProperties.kt b/src/main/java/com/maddyhome/idea/vim/group/IjOptionProperties.kt index a4adf583c69..c4fa4052cba 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/IjOptionProperties.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/IjOptionProperties.kt @@ -48,6 +48,7 @@ public open class GlobalIjOptions(scope: OptionAccessScope) : OptionsPropertiesB public class EffectiveIjOptions(scope: OptionAccessScope.EFFECTIVE): GlobalIjOptions(scope) { // Vim options that are implemented purely by existing IntelliJ features and not used by vim-engine public var breakindent: Boolean by optionProperty(IjOptions.breakindent) + public val colorcolumn: StringListOptionValue by optionProperty(IjOptions.colorcolumn) public var cursorline: Boolean by optionProperty(IjOptions.cursorline) public var list: Boolean by optionProperty(IjOptions.list) public var textwidth: Int by optionProperty(IjOptions.textwidth) diff --git a/src/main/java/com/maddyhome/idea/vim/group/IjOptions.kt b/src/main/java/com/maddyhome/idea/vim/group/IjOptions.kt index c318e988704..2fa9b190824 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/IjOptions.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/IjOptions.kt @@ -10,6 +10,7 @@ package com.maddyhome.idea.vim.group import com.intellij.openapi.application.ApplicationNamesInfo import com.maddyhome.idea.vim.api.Options +import com.maddyhome.idea.vim.ex.exExceptionMessage import com.maddyhome.idea.vim.options.NumberOption import com.maddyhome.idea.vim.options.Option import com.maddyhome.idea.vim.options.OptionDeclaredScope.GLOBAL @@ -38,6 +39,21 @@ public object IjOptions { // Vim options that are implemented purely by existing IntelliJ features and not used by vim-engine public val breakindent: ToggleOption = addOption(ToggleOption("breakindent", LOCAL_TO_WINDOW, "bri", false)) + public val colorcolumn: StringListOption = addOption(object : StringListOption("colorcolumn", LOCAL_TO_WINDOW, "cc", "") { + override fun checkIfValueValid(value: VimDataType, token: String) { + super.checkIfValueValid(value, token) + if (value != VimString.EMPTY) { + // Each element in the comma separated string list needs to be a number. No spaces. Vim supports numbers + // beginning "+" or "-" to draw a highlight column relative to the 'textwidth' value. We don't fully support + // that, but we do automtaically add "+0" because IntelliJ always displays the right margin + split((value as VimString).asString()).forEach { + if (!it.matches(Regex("[+-]?[0-9]+"))) { + throw exExceptionMessage("E474", token) + } + } + } + } + }) public val cursorline: ToggleOption = addOption(ToggleOption("cursorline", LOCAL_TO_WINDOW, "cul", false)) public val list: ToggleOption = addOption(ToggleOption("list", LOCAL_TO_WINDOW, "list", false)) public val textwidth: NumberOption = addOption(UnsignedNumberOption("textwidth", LOCAL_TO_BUFFER, "tw", 0)) diff --git a/src/main/java/com/maddyhome/idea/vim/group/OptionGroup.kt b/src/main/java/com/maddyhome/idea/vim/group/OptionGroup.kt index 0b382376a81..5c08c614294 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/OptionGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/OptionGroup.kt @@ -27,8 +27,10 @@ import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.options.NumberOption import com.maddyhome.idea.vim.options.OptionAccessScope +import com.maddyhome.idea.vim.options.StringListOption import com.maddyhome.idea.vim.options.ToggleOption import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt +import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString import com.maddyhome.idea.vim.vimscript.model.datatypes.asVimInt internal interface IjVimOptionGroup: VimOptionGroup { @@ -46,6 +48,7 @@ internal interface IjVimOptionGroup: VimOptionGroup { internal class OptionGroup : VimOptionGroupBase(), IjVimOptionGroup { init { addOptionValueOverride(IjOptions.breakindent, BreakIndentOptionValueProvider(IjOptions.breakindent)) + addOptionValueOverride(IjOptions.colorcolumn, ColorColumnOptionValueProvider(IjOptions.colorcolumn)) addOptionValueOverride(IjOptions.cursorline, CursorLineOptionValueProvider(IjOptions.cursorline)) addOptionValueOverride(IjOptions.list, ListOptionValueProvider(IjOptions.list)) addOptionValueOverride(IjOptions.textwidth, TextWidthOptionValueProvider(IjOptions.textwidth)) @@ -136,6 +139,92 @@ private class BreakIndentOptionValueProvider(breakIndentOption: ToggleOption) } } +private class ColorColumnOptionValueProvider(private val colorColumnOption: StringListOption) : + IdeOptionValueOverride(colorColumnOption) { + override fun getIdeCurrentValue(editor: VimEditor): VimString { + // If isRightMarginShown is disabled, then we don't show any visual guides, including the right margin + if (!editor.ij.settings.isRightMarginShown) { + return VimString.EMPTY + } + + val softMargins = editor.ij.settings.softMargins + return VimString(buildString { + softMargins.joinTo(this, ",") + + // IntelliJ treats right margin and visual guides as the same - if we're showing either, we're showing both. + // Vim supports the "+0" syntax to show a highlight column relative to the 'textwidth' value. The user can set + // the value to an empty string to remove this, and disable the right margin. + // IntelliJ behaves slightly differently to Vim here - "+0" in Vim will only show the column if 'textwidth' is + // set, while IntelliJ will show the current right margin even if wrap at margin is false. + if (this.isNotEmpty()) append(",") + append("+0") + }) + } + + override fun getIdeDefaultValue(editor: VimEditor): VimString { + if (!EditorSettingsExternalizable.getInstance().isRightMarginShown) { + return VimString.EMPTY + } + + val ijEditor = editor.ij + val language = ijEditor.project?.let { TextEditorImpl.getDocumentLanguage(ijEditor) } + val softMargins = CodeStyle.getSettings(ijEditor).getSoftMargins(language) + return VimString(buildString { + softMargins.joinTo(this, ",") + + // Add the default "+0" to mimic Vim showing the 'textwidth' column. See above. + if (this.isNotEmpty()) append(",") + append("+0") + }) + } + + override fun setIdeValue(editor: VimEditor, value: VimString) { + // Given an empty string, hide the margin. + if (value == VimString.EMPTY) { + editor.ij.settings.isRightMarginShown = false + } + else { + editor.ij.settings.isRightMarginShown = true + + val softMargins = mutableListOf() + colorColumnOption.split(value.value).forEach { + if (it.startsWith("+") || it.startsWith("-")) { + // TODO: Support ±1, ±2, ±n, etc. But this is difficult + // This would need a listener for the right margin IntelliJ value, and would still add a visual guide at +0 + // We'd also need some mechanism for saving the relative offsets. The override getters would return real + // column values, while the stored Vim option will be relative + // We could perhaps add a property change listener from editor settings state? + // (editor.ij as EditorImpl).state.addPropertyChangeListener(...) + // (editor.ij.settings as SettingsImpl).getState().addPropertyChangeListener(...) + } + else { + it.toIntOrNull()?.let(softMargins::add) + } + } + editor.ij.settings.setSoftMargins(softMargins) + } + } + + override fun resetDefaultValue(editor: VimEditor) { + // Reset the current settings back to default by setting both the flag and the visual guides + val ijEditor = editor.ij + val language = ijEditor.project?.let { TextEditorImpl.getDocumentLanguage(ijEditor) } + + // Remember to only update if the value has changed! We don't want to force the global-local values to local only + if (ijEditor.settings.isRightMarginShown != EditorSettingsExternalizable.getInstance().isRightMarginShown) { + ijEditor.settings.isRightMarginShown = EditorSettingsExternalizable.getInstance().isRightMarginShown + } + + val codeStyle = CodeStyle.getSettings(ijEditor) + val globalSoftMargins = codeStyle.getSoftMargins(language) + val localSoftMargins = ijEditor.settings.softMargins + + if (globalSoftMargins.count() != localSoftMargins.count() || !localSoftMargins.containsAll(globalSoftMargins)) { + ijEditor.settings.setSoftMargins(codeStyle.getSoftMargins(language)) + } + } +} + private class CursorLineOptionValueProvider(cursorLineOption: ToggleOption) : IdeOptionValueOverride(cursorLineOption) { override fun getIdeCurrentValue(editor: VimEditor) = editor.ij.settings.isCaretRowShown.asVimInt() diff --git a/src/main/resources/dictionaries/ideavim.dic b/src/main/resources/dictionaries/ideavim.dic index e3d099507ba..d5484e84cbc 100644 --- a/src/main/resources/dictionaries/ideavim.dic +++ b/src/main/resources/dictionaries/ideavim.dic @@ -3,6 +3,7 @@ ideavimrc maddyhome breakindent +colorcolumn cursorline gdefault guicursor diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt index c288441b5d6..6017f882378 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt @@ -33,6 +33,7 @@ class SetCommandTest : VimTestCase() { configureByText("\n") // Some options reflect the state of IntelliJ values when not explicitly set. Make sure we've got consistent values + fixture.editor.settings.isRightMarginShown = false // Otherwise we get `colorcolumn=+0` fixture.editor.settings.isCaretRowShown = IjOptions.cursorline.defaultValue.asBoolean() fixture.editor.settings.isUseCustomSoftWrapIndent = IjOptions.breakindent.defaultValue.asBoolean() fixture.editor.settings.isWhitespacesShown = IjOptions.list.defaultValue.asBoolean() @@ -173,20 +174,21 @@ class SetCommandTest : VimTestCase() { assertCommandOutput("set all", """ |--- Options --- - |noargtextobj ideastrictmode scroll=0 notextobj-entire - |nobreakindent noideatracetime scrolljump=1 notextobj-indent - | closenotebooks ideawrite=all scrolloff=0 textwidth=0 - |nocommentary noignorecase selectmode= timeout - |nocursorline noincsearch shellcmdflag=-x timeoutlen=1000 - |nodigraph nolist shellxescape=@ notrackactionids - |noexchange nomatchit shellxquote={ undolevels=1000 - |nogdefault maxmapdepth=20 showcmd unifyjumps - |nohighlightedyank more showmode virtualedit= - | history=50 nomultiple-cursors sidescroll=0 novisualbell - |nohlsearch noNERDTree sidescrolloff=0 visualdelay=100 - |noideaglobalmode nrformats=hex nosmartcase whichwrap=b,s - |noideajoin nonumber startofline wrap - | ideamarks norelativenumber nosurround wrapscan + |noargtextobj ideastrictmode scrolljump=1 textwidth=0 + |nobreakindent noideatracetime scrolloff=0 timeout + | closenotebooks ideawrite=all selectmode= timeoutlen=1000 + | colorcolumn= noignorecase shellcmdflag=-x notrackactionids + |nocommentary noincsearch shellxescape=@ undolevels=1000 + |nocursorline nolist shellxquote={ unifyjumps + |nodigraph nomatchit showcmd virtualedit= + |noexchange maxmapdepth=20 showmode novisualbell + |nogdefault more sidescroll=0 visualdelay=100 + |nohighlightedyank nomultiple-cursors sidescrolloff=0 whichwrap=b,s + | history=50 noNERDTree nosmartcase wrap + |nohlsearch nrformats=hex startofline wrapscan + |noideaglobalmode nonumber nosurround + |noideajoin norelativenumber notextobj-entire + | ideamarks scroll=0 notextobj-indent | clipboard=ideaput,autoselect,exclude:cons\|linux | excommandannotation | guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175 @@ -238,6 +240,7 @@ class SetCommandTest : VimTestCase() { |nobreakindent | clipboard=ideaput,autoselect,exclude:cons\|linux | closenotebooks + | colorcolumn= |nocommentary |nocursorline |nodigraph diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt index 9fe1e1abf06..d9a337963ec 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt @@ -34,6 +34,7 @@ class SetglobalCommandTest : VimTestCase() { configureByText("\n") // Some options reflect the state of IntelliJ values when not explicitly set. Make sure we've got consistent values + fixture.editor.settings.isRightMarginShown = false // Otherwise we get `colorcolumn=+0` fixture.editor.settings.isCaretRowShown = IjOptions.cursorline.defaultValue.asBoolean() fixture.editor.settings.isUseCustomSoftWrapIndent = IjOptions.breakindent.defaultValue.asBoolean() fixture.editor.settings.isWhitespacesShown = IjOptions.list.defaultValue.asBoolean() @@ -359,20 +360,21 @@ class SetglobalCommandTest : VimTestCase() { setOsSpecificOptionsToSafeValues() assertCommandOutput("setglobal all", """ |--- Global option values --- - |noargtextobj ideastrictmode scroll=0 notextobj-entire - |nobreakindent noideatracetime scrolljump=1 notextobj-indent - | closenotebooks ideawrite=all scrolloff=0 textwidth=0 - |nocommentary noignorecase selectmode= timeout - |nocursorline noincsearch shellcmdflag=-x timeoutlen=1000 - |nodigraph nolist shellxescape=@ notrackactionids - |noexchange nomatchit shellxquote={ undolevels=1000 - |nogdefault maxmapdepth=20 showcmd unifyjumps - |nohighlightedyank more showmode virtualedit= - | history=50 nomultiple-cursors sidescroll=0 novisualbell - |nohlsearch noNERDTree sidescrolloff=0 visualdelay=100 - |noideaglobalmode nrformats=hex nosmartcase whichwrap=b,s - |noideajoin nonumber startofline wrap - | ideamarks norelativenumber nosurround wrapscan + |noargtextobj ideastrictmode scrolljump=1 textwidth=0 + |nobreakindent noideatracetime scrolloff=0 timeout + | closenotebooks ideawrite=all selectmode= timeoutlen=1000 + | colorcolumn= noignorecase shellcmdflag=-x notrackactionids + |nocommentary noincsearch shellxescape=@ undolevels=1000 + |nocursorline nolist shellxquote={ unifyjumps + |nodigraph nomatchit showcmd virtualedit= + |noexchange maxmapdepth=20 showmode novisualbell + |nogdefault more sidescroll=0 visualdelay=100 + |nohighlightedyank nomultiple-cursors sidescrolloff=0 whichwrap=b,s + | history=50 noNERDTree nosmartcase wrap + |nohlsearch nrformats=hex startofline wrapscan + |noideaglobalmode nonumber nosurround + |noideajoin norelativenumber notextobj-entire + | ideamarks scroll=0 notextobj-indent | clipboard=ideaput,autoselect,exclude:cons\|linux | excommandannotation | guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175 @@ -420,6 +422,7 @@ class SetglobalCommandTest : VimTestCase() { |nobreakindent | clipboard=ideaput,autoselect,exclude:cons\|linux | closenotebooks + | colorcolumn= |nocommentary |nocursorline |nodigraph diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt index 37559c0e3fe..adbfa59ecb2 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt @@ -35,6 +35,7 @@ class SetlocalCommandTest : VimTestCase() { configureByText("\n") // Some options reflect the state of IntelliJ values when not explicitly set. Make sure we've got consistent values + fixture.editor.settings.isRightMarginShown = false // Otherwise we get `colorcolumn=+0` fixture.editor.settings.isCaretRowShown = IjOptions.cursorline.defaultValue.asBoolean() fixture.editor.settings.isUseCustomSoftWrapIndent = IjOptions.breakindent.defaultValue.asBoolean() fixture.editor.settings.isWhitespacesShown = IjOptions.list.defaultValue.asBoolean() @@ -391,20 +392,21 @@ class SetlocalCommandTest : VimTestCase() { setOsSpecificOptionsToSafeValues() assertCommandOutput("setlocal all", """ |--- Local option values --- - |noargtextobj idearefactormode= norelativenumber nosurround - |nobreakindent ideastrictmode scroll=0 notextobj-entire - | closenotebooks noideatracetime scrolljump=1 notextobj-indent - |nocommentary ideawrite=all scrolloff=-1 textwidth=0 - |nocursorline noignorecase selectmode= timeout - |nodigraph noincsearch shellcmdflag=-x timeoutlen=1000 - |noexchange nolist shellxescape=@ notrackactionids - |nogdefault nomatchit shellxquote={ unifyjumps - |nohighlightedyank maxmapdepth=20 showcmd virtualedit= - | history=50 more showmode novisualbell - |nohlsearch nomultiple-cursors sidescroll=0 visualdelay=100 - |noideaglobalmode noNERDTree sidescrolloff=-1 whichwrap=b,s - |--ideajoin nrformats=hex nosmartcase wrap - | ideamarks nonumber startofline wrapscan + |noargtextobj idearefactormode= scroll=0 notextobj-indent + |nobreakindent ideastrictmode scrolljump=1 textwidth=0 + | closenotebooks noideatracetime scrolloff=-1 timeout + | colorcolumn= ideawrite=all selectmode= timeoutlen=1000 + |nocommentary noignorecase shellcmdflag=-x notrackactionids + |nocursorline noincsearch shellxescape=@ unifyjumps + |nodigraph nolist shellxquote={ virtualedit= + |noexchange nomatchit showcmd novisualbell + |nogdefault maxmapdepth=20 showmode visualdelay=100 + |nohighlightedyank more sidescroll=0 whichwrap=b,s + | history=50 nomultiple-cursors sidescrolloff=-1 wrap + |nohlsearch noNERDTree nosmartcase wrapscan + |noideaglobalmode nrformats=hex startofline + |--ideajoin nonumber nosurround + | ideamarks norelativenumber notextobj-entire | clipboard=ideaput,autoselect,exclude:cons\|linux | excommandannotation | guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175 @@ -458,6 +460,7 @@ class SetlocalCommandTest : VimTestCase() { |nobreakindent | clipboard=ideaput,autoselect,exclude:cons\|linux | closenotebooks + | colorcolumn= |nocommentary |nocursorline |nodigraph diff --git a/src/test/java/org/jetbrains/plugins/ideavim/option/ColorColumnOptionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/option/ColorColumnOptionTest.kt new file mode 100644 index 00000000000..c6d8c57aaa6 --- /dev/null +++ b/src/test/java/org/jetbrains/plugins/ideavim/option/ColorColumnOptionTest.kt @@ -0,0 +1,387 @@ +/* + * Copyright 2003-2024 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.intellij.application.options.CodeStyle +import com.intellij.lang.Language +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.ex.EditorSettingsExternalizable +import com.intellij.openapi.editor.impl.SettingsImpl +import com.intellij.openapi.fileEditor.impl.text.TextEditorImpl +import com.intellij.testFramework.fixtures.CodeInsightTestFixture +import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory +import org.jetbrains.plugins.ideavim.SkipNeovimReason +import org.jetbrains.plugins.ideavim.TestWithoutNeovim +import org.jetbrains.plugins.ideavim.VimTestCase +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInfo + +@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) +class ColorColumnOptionTest : VimTestCase() { + private var originalIsRightMarginShown = false + + override fun setUp(testInfo: TestInfo) { + super.setUp(testInfo) + + configureByText("\n") + + originalIsRightMarginShown = EditorSettingsExternalizable.getInstance().isRightMarginShown + + EditorSettingsExternalizable.getInstance().isRightMarginShown = false + } + + override fun tearDown(testInfo: TestInfo) { + EditorSettingsExternalizable.getInstance().isRightMarginShown = originalIsRightMarginShown + super.tearDown(testInfo) + } + + override fun createFixture(factory: IdeaTestFixtureFactory): CodeInsightTestFixture { + val fixture = factory.createFixtureBuilder("IdeaVim").fixture + return factory.createCodeInsightFixture(fixture) + } + + @Suppress("SameParameterValue") + private fun openNewBufferWindow(filename: String, content: String): Editor { + // This replaces fixture.editor + fixture.openFileInEditor(fixture.createFile(filename, content)) + + // But our selection changed callback doesn't get called immediately, and that callback will deactivate the ex entry + // panel (which causes problems if our next command is `:set`). So type something (`0` is a good no-op) to give time + // for the event to propagate + typeText("0") + + return fixture.editor + } + + @Test + fun `test 'colorcolumn' accepts empty string`() { + enterCommand("set colorcolumn=") + assertPluginError(false) + } + + @Test + fun `test 'colorcolumn' accepts comma separated string of numbers`() { + enterCommand("set colorcolumn=10,20,30") + assertPluginError(false) + } + + @Test + fun `test 'colorcolumn' accepts relative values`() { + enterCommand("set colorcolumn=10,+20,-30") + assertPluginError(false) + } + + @Test + fun `test 'colorcolumn' reports invalid argument with space separated values`() { + enterCommand("set colorcolumn=10, 20") + assertPluginError(true) + assertPluginErrorMessageContains("E474: Invalid argument: colorcolumn=10,") + } + + @Test + fun `test 'colorcolumn' reports invalid argument with non-numeric values`() { + enterCommand("set colorcolumn=10,aa,20") + assertPluginError(true) + assertPluginErrorMessageContains("E474: Invalid argument: colorcolumn=10,aa,20") + } + + @Test + fun `test 'colorcolumn' defaults to current intellij setting`() { + assertFalse(fixture.editor.settings.isRightMarginShown) + assertEquals("", optionsIj().colorcolumn.value) + } + + @Test + fun `test 'colorcolumn' defaults to global intellij setting`() { + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEquals("", optionsIj().colorcolumn.value) + } + + @Test + fun `test 'colorcolumn' reports '+0' to show right margin is visible`() { + // IntelliJ only has one setting for visual guides and hard-wrap typing margin, so we have to report a special value + // of "+0", which makes Vim show a highlight column at 'textwidth' (IntelliJ shows it even if 'textwidth' is 0) + fixture.editor.settings.isRightMarginShown = true + assertCommandOutput("set colorcolumn?", " colorcolumn=+0\n") + } + + @Test + fun `test 'colorcolumn' reports '+0' at end of visual guide list`() { + fixture.editor.settings.isRightMarginShown = true + fixture.editor.settings.setSoftMargins(listOf(10,20,30)) + assertCommandOutput("set colorcolumn?", " colorcolumn=10,20,30,+0\n") + } + + @Test + fun `test 'colorcolumn' option reports global intellij setting if not explicitly set`() { + EditorSettingsExternalizable.getInstance().isRightMarginShown = true + setGlobalSoftMargins(listOf(10, 20, 30)) + assertCommandOutput("set colorcolumn?", " colorcolumn=10,20,30,+0\n") + + setGlobalSoftMargins(listOf(90, 80, 70)) + assertCommandOutput("set colorcolumn?", " colorcolumn=70,80,90,+0\n") + + setGlobalSoftMargins(emptyList()) + assertCommandOutput("set colorcolumn?", " colorcolumn=+0\n") + + EditorSettingsExternalizable.getInstance().isRightMarginShown = false + assertCommandOutput("set colorcolumn?", " colorcolumn=\n") + } + + @Test + fun `test local 'colorcolumn' option reports global intellij setting if not explicitly set`() { + EditorSettingsExternalizable.getInstance().isRightMarginShown = true + setGlobalSoftMargins(listOf(10, 20, 30)) + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=10,20,30,+0\n") + + setGlobalSoftMargins(listOf(90, 80, 70)) + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=70,80,90,+0\n") + + setGlobalSoftMargins(emptyList()) + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=+0\n") + + EditorSettingsExternalizable.getInstance().isRightMarginShown = false + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=\n") + } + + @Test + fun `test 'colorcolumn' option reports local intellij setting if set via IDE`() { + fixture.editor.settings.isRightMarginShown = true + fixture.editor.settings.setSoftMargins(listOf(10, 20, 30)) + assertCommandOutput("set colorcolumn?", " colorcolumn=10,20,30,+0\n") + + fixture.editor.settings.setSoftMargins(listOf(70, 80, 90)) + assertCommandOutput("set colorcolumn?", " colorcolumn=70,80,90,+0\n") + + fixture.editor.settings.setSoftMargins(emptyList()) + assertCommandOutput("set colorcolumn?", " colorcolumn=+0\n") + + fixture.editor.settings.isRightMarginShown = false + assertCommandOutput("set colorcolumn?", " colorcolumn=\n") + } + + @Test + fun `test local 'colorcolumn' option reports local intellij setting if set via IDE`() { + fixture.editor.settings.isRightMarginShown = true + fixture.editor.settings.setSoftMargins(listOf(10, 20, 30)) + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=10,20,30,+0\n") + + fixture.editor.settings.setSoftMargins(listOf(70, 80, 90)) + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=70,80,90,+0\n") + + fixture.editor.settings.setSoftMargins(emptyList()) + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=+0\n") + + fixture.editor.settings.isRightMarginShown = false + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=\n") + } + + @Test + fun `test 'colorcolumn' does not report current visual guides if global right margin option is disabled`() { + EditorSettingsExternalizable.getInstance().isRightMarginShown = false + fixture.editor.settings.setSoftMargins(listOf(10,20,30)) + assertCommandOutput("set colorcolumn?", " colorcolumn=\n") + } + + @Test + fun `test 'colorcolumn' does not report current visual guides if local right margin option is disabled`() { + fixture.editor.settings.isRightMarginShown = false + fixture.editor.settings.setSoftMargins(listOf(10,20,30)) + assertCommandOutput("set colorcolumn?", " colorcolumn=\n") + } + + @Test + fun `test set 'colorcolumn' modifies local intellij setting only`() { + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + + enterCommand("set colorcolumn=10,20,30") + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + assertTrue(fixture.editor.settings.isRightMarginShown) + assertEquals(listOf(10,20,30), fixture.editor.settings.softMargins) + + enterCommand("set colorcolumn=50") + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + assertTrue(fixture.editor.settings.isRightMarginShown) + assertEquals(listOf(50), fixture.editor.settings.softMargins) + + enterCommand("set colorcolumn=") + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + assertFalse(fixture.editor.settings.isRightMarginShown) + assertEquals(listOf(50), fixture.editor.settings.softMargins) // The guides aren't reset + } + + @Test + fun `test setlocal 'colorcolumn' modifies local intellij setting only`() { + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + + enterCommand("setlocal colorcolumn=10,20,30") + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + assertTrue(fixture.editor.settings.isRightMarginShown) + assertEquals(listOf(10,20,30), fixture.editor.settings.softMargins) + + enterCommand("setlocal colorcolumn=50") + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + assertTrue(fixture.editor.settings.isRightMarginShown) + assertEquals(listOf(50), fixture.editor.settings.softMargins) + + enterCommand("setlocal colorcolumn=") + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + assertFalse(fixture.editor.settings.isRightMarginShown) + assertEquals(listOf(50), fixture.editor.settings.softMargins) // The guides aren't reset + } + + @Test + fun `test setglobal 'colorcolumn' option affects IdeaVim global value only`() { + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + assertCommandOutput("setglobal colorcolumn?", " colorcolumn=\n") + + enterCommand("setglobal colorcolumn=10,20,30") + assertCommandOutput("setglobal colorcolumn?", " colorcolumn=10,20,30\n") + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + } + + @Test + fun `test set 'colorcolumn' updates IdeaVim global value as well as local`() { + enterCommand("set colorcolumn=10,20,30") + assertCommandOutput("setglobal colorcolumn?", " colorcolumn=10,20,30\n") + assertCommandOutput("set colorcolumn?", " colorcolumn=10,20,30,+0\n") + } + + @Test + fun `test setting IDE value is treated like setlocal`() { + // If we use `:set`, it updates the local and per-window global values. If we set the value from the IDE, it only + // affects the local value + fixture.editor.settings.isRightMarginShown = true + fixture.editor.settings.setSoftMargins(listOf(70, 80, 90)) + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=70,80,90,+0\n") + assertCommandOutput("set colorcolumn?", " colorcolumn=70,80,90,+0\n") + assertCommandOutput("setglobal colorcolumn?", " colorcolumn=\n") + } + + @Test + fun `test setglobal does not modify effective IDE value`() { + enterCommand("setglobal colorcolumn=10,20,30") + assertFalse(fixture.editor.settings.isRightMarginShown) + } + + @Test + fun `test setglobal does not modify persistent IDE global value`() { + enterCommand("setglobal colorcolumn=10,20,30") + assertFalse(EditorSettingsExternalizable.getInstance().isRightMarginShown) + assertEmpty(getGlobalSoftMargins()) + } + + @Test + fun `test reset 'colorcolun' to default copies current global intellij setting`() { + fixture.editor.settings.isRightMarginShown = true + fixture.editor.settings.setSoftMargins(listOf(10, 20, 30)) + + enterCommand("set colorcolumn&") + assertCommandOutput("set colorcolumn?", " colorcolumn=\n") + assertFalse(fixture.editor.settings.isRightMarginShown) + assertEmpty(fixture.editor.settings.softMargins) + + // Verify that IntelliJ doesn't allow us to "unset" a local editor setting - it's a copy of the global value + EditorSettingsExternalizable.getInstance().isRightMarginShown = true + assertFalse(fixture.editor.settings.isRightMarginShown) + } + + @Test + fun `test reset local 'colorcolun' to default copies current global intellij setting`() { + fixture.editor.settings.isRightMarginShown = true + fixture.editor.settings.setSoftMargins(listOf(10, 20, 30)) + + enterCommand("setlocal colorcolumn&") + assertCommandOutput("setlocal colorcolumn?", " colorcolumn=\n") + assertFalse(fixture.editor.settings.isRightMarginShown) + assertEmpty(fixture.editor.settings.softMargins) + + // Verify that IntelliJ doesn't allow us to "unset" a local editor setting - it's a copy of the global value + EditorSettingsExternalizable.getInstance().isRightMarginShown = true + assertFalse(fixture.editor.settings.isRightMarginShown) + } + + @Test + fun `test open new window without setting ideavim option will initialise 'colorcolumn' to defaults`() { + EditorSettingsExternalizable.getInstance().isRightMarginShown = true + setGlobalSoftMargins(listOf(10, 20, 30)) + assertCommandOutput("set colorcolumn?", " colorcolumn=10,20,30,+0\n") + + openNewBufferWindow("bbb.txt", "lorem ipsum") + + assertCommandOutput("set colorcolumn?", " colorcolumn=10,20,30,+0\n") + + // Changing the global value should update the editor + setGlobalSoftMargins(listOf(40, 50, 60, 70)) + assertCommandOutput("set colorcolumn?", " colorcolumn=40,50,60,70,+0\n") + + EditorSettingsExternalizable.getInstance().isRightMarginShown = false + assertCommandOutput("set colorcolumn?", " colorcolumn=\n") + } + + @Test + fun `test open new window after setting ideavim option will initialise 'colorcolumn' to setglobal value`() { + EditorSettingsExternalizable.getInstance().isRightMarginShown = true + setGlobalSoftMargins(listOf(10, 20, 30)) + enterCommand("set colorcolumn=40,50,60") + assertCommandOutput("set colorcolumn?", " colorcolumn=40,50,60,+0\n") + + openNewBufferWindow("bbb.txt", "lorem ipsum") + + assertCommandOutput("set colorcolumn?", " colorcolumn=40,50,60,+0\n") + + // Changing the global value should NOT update the editor + setGlobalSoftMargins(listOf(10, 20, 30)) + assertCommandOutput("set colorcolumn?", " colorcolumn=40,50,60,+0\n") + } + + @Test + fun `test setglobal value used when opening new window`() { + enterCommand("setglobal colorcolumn=10,20,30") + + openNewBufferWindow("bbb.txt", "lorem ipsum") + + assertCommandOutput("set colorcolumn?", " colorcolumn=10,20,30,+0\n") + + // Changing the global value should NOT update the editor + setGlobalSoftMargins(listOf(40, 50, 60)) + assertCommandOutput("set colorcolumn?", " colorcolumn=10,20,30,+0\n") + } + + private fun getGlobalSoftMargins(): List { + val language = TextEditorImpl.getDocumentLanguage(fixture.editor) + return CodeStyle.getSettings(fixture.editor).getSoftMargins(language) + } + + private fun setGlobalSoftMargins(margins: List) { + val language = TextEditorImpl.getDocumentLanguage(fixture.editor) + val commonSettings = CodeStyle.getSettings(fixture.editor).getCommonSettings(language) + if (language == null || commonSettings.language == Language.ANY) { + CodeStyle.getSettings(fixture.editor).defaultSoftMargins = margins + } + else { + CodeStyle.getSettings(fixture.editor).setSoftMargins(language, margins) + } + // Setting the value directly doesn't invalidate the cached property value. Not sure if there's a better way + (fixture.editor.settings as SettingsImpl).reinitSettings() + } +} \ No newline at end of file diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimOptionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimOptionGroupBase.kt index 553ffe772d4..a15ac2192f8 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimOptionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimOptionGroupBase.kt @@ -385,6 +385,11 @@ public abstract class IdeOptionValueOverride(private val option /** * Reset the current IDE value to the default IDE value, if different + * + * Implementers can override this function if they need to do more complex reset, such as resetting two IDE values. + * The overridden value should only update the local setting if the value has changed. This is especially important + * if the IDE values are global-local - updating the IDE value might set the local value to a copy of the default + * value, rather than leaving the local value "unset". */ protected open fun resetDefaultValue(editor: VimEditor) { // TODO: If we disable and re-enable the plugin, we reinitialise the options, and set defaults again @@ -760,9 +765,9 @@ private class OptionInitialisationStrategy(private val storage: OptionStorage) { } private fun initialiseLocalToBufferOptions(editor: VimEditor) { - val globalScope = OptionAccessScope.GLOBAL(editor) - val localScope = OptionAccessScope.LOCAL(editor) if (!storage.isLocalToBufferOptionStorageInitialised(editor)) { + val globalScope = OptionAccessScope.GLOBAL(editor) + val localScope = OptionAccessScope.LOCAL(editor) forEachOption(LOCAL_TO_BUFFER) { option -> val value = storage.getOptionValue(option, globalScope) storage.setOptionValue(option, localScope, value)