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)