diff --git a/src/main/java/com/maddyhome/idea/vim/ui/ExOutputPanel.java b/src/main/java/com/maddyhome/idea/vim/ui/ExOutputPanel.java index 2570139680..d6df38d5d7 100644 --- a/src/main/java/com/maddyhome/idea/vim/ui/ExOutputPanel.java +++ b/src/main/java/com/maddyhome/idea/vim/ui/ExOutputPanel.java @@ -68,7 +68,9 @@ private ExOutputPanel(@NotNull Editor editor) { add(myScrollPane, BorderLayout.CENTER); add(myLabel, BorderLayout.SOUTH); + // Set the text area read only, and support wrap myText.setEditable(false); + myText.setLineWrap(true); myAdapter = new ComponentAdapter() { @Override diff --git a/src/test/java/org/jetbrains/plugins/ideavim/VimTestCase.kt b/src/test/java/org/jetbrains/plugins/ideavim/VimTestCase.kt index 0b227085b5..57bb020985 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/VimTestCase.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/VimTestCase.kt @@ -531,9 +531,14 @@ abstract class VimTestCase { assertEquals(expected, selected) } + fun assertCommandOutput(command: String, expected: String) { + enterCommand(command) + assertExOutput(expected) + } + fun assertExOutput(expected: String) { val actual = getInstance(fixture.editor).text - assertNotNull("No Ex output", actual) + assertNotNull(actual, "No Ex output") assertEquals(expected, actual) NeovimTesting.typeCommand("", testInfo, fixture.editor) } 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 accae14817..69d0378f55 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 @@ -11,19 +11,32 @@ package org.jetbrains.plugins.ideavim.ex.implementation.commands import com.maddyhome.idea.vim.api.Options import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.options.OptionScope -import org.jetbrains.plugins.ideavim.SkipNeovimReason -import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.VimTestCase +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInfo import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue +@Suppress("SpellCheckingInspection") class SetCommandTest : VimTestCase() { + @BeforeEach + override fun setUp(testInfo: TestInfo) { + super.setUp(testInfo) + configureByText("\n") + } + + private fun setOsSpecificOptionsToSafeValues() { + enterCommand("set shell=/dummy/path/to/bash") + enterCommand("set shellcmdflag=-x") + enterCommand("set shellxescape=@") + enterCommand("set shellxquote={") + } + @Test fun `test unknown option`() { - configureByText("\n") enterCommand("set unknownOption") assertPluginError(true) assertPluginErrorMessageContains("Unknown option: unknownOption") @@ -31,46 +44,34 @@ class SetCommandTest : VimTestCase() { @Test fun `test toggle option`() { - configureByText("\n") enterCommand("set rnu") assertTrue(options().relativenumber) enterCommand("set rnu!") assertFalse(options().relativenumber) } - // todo we have spaces in assertExOutput because of pad(20) in the com.maddyhome.idea.vim.vimscript.model.commands.SetCommandKt#showOptions method - @TestWithoutNeovim(reason = SkipNeovimReason.OPTION) @Test fun `test number option`() { - configureByText("\n") enterCommand("set scrolloff&") assertEquals(0, options().scrolloff) - enterCommand("set scrolloff?") - assertExOutput("scrolloff=0 \n") + assertCommandOutput("set scrolloff?", " scrolloff=0\n") enterCommand("set scrolloff=5") assertEquals(5, options().scrolloff) - enterCommand("set scrolloff?") - assertExOutput("scrolloff=5 \n") + assertCommandOutput("set scrolloff?", " scrolloff=5\n") } - @TestWithoutNeovim(reason = SkipNeovimReason.OPTION) @Test fun `test toggle option as a number`() { - configureByText("\n") enterCommand("set number&") assertEquals(0, injector.optionGroup.getOptionValue(Options.number, OptionScope.GLOBAL).asDouble().toInt()) - enterCommand("set number?") - assertExOutput("nonumber \n") + assertCommandOutput("set number?", "nonumber\n") enterCommand("let &nu=1000") assertEquals(1000, injector.optionGroup.getOptionValue(Options.number, OptionScope.GLOBAL).asDouble().toInt()) - enterCommand("set number?") - assertExOutput(" number \n") + assertCommandOutput("set number?", " number\n") } - @TestWithoutNeovim(reason = SkipNeovimReason.PLUGIN_ERROR) @Test fun `test toggle option exceptions`() { - configureByText("\n") enterCommand("set number+=10") assertPluginError(true) assertPluginErrorMessageContains("E474: Invalid argument: number+=10") @@ -93,10 +94,8 @@ class SetCommandTest : VimTestCase() { assertPluginErrorMessageContains("E474: Invalid argument: number-=test") } - @TestWithoutNeovim(reason = SkipNeovimReason.PLUGIN_ERROR) @Test fun `test number option exceptions`() { - configureByText("\n") enterCommand("set scrolloff+=10") assertPluginError(false) enterCommand("set scrolloff+=test") @@ -116,33 +115,188 @@ class SetCommandTest : VimTestCase() { assertPluginErrorMessageContains("E521: Number required after =: scrolloff-=test") } - @TestWithoutNeovim(reason = SkipNeovimReason.OPTION) @Test fun `test string option`() { - configureByText("\n") enterCommand("set selection&") assertEquals("inclusive", options().selection) - enterCommand("set selection?") - assertExOutput("selection=inclusive \n") + assertCommandOutput("set selection?", " selection=inclusive\n") enterCommand("set selection=exclusive") assertEquals("exclusive", options().selection) - enterCommand("set selection?") - assertExOutput("selection=exclusive \n") + assertCommandOutput("set selection?", " selection=exclusive\n") } - @TestWithoutNeovim(reason = SkipNeovimReason.OPTION) @Test fun `test show numbered value`() { - configureByText("\n") - enterCommand("set so") - assertExOutput("scrolloff=0 \n") + assertCommandOutput("set so", " scrolloff=0\n") } - @TestWithoutNeovim(reason = SkipNeovimReason.OPTION) @Test fun `test show numbered value with question mark`() { - configureByText("\n") - enterCommand("set so?") - assertExOutput("scrolloff=0 \n") + assertCommandOutput("set so?", " scrolloff=0\n") + } + + @Test + fun `test show all modified effective option values`() { + enterCommand("set number relativenumber scrolloff nrformats") + assertCommandOutput("set", + """ + |--- Options --- + | ideastrictmode number relativenumber + | + """.trimMargin()) + } + + @Test + fun `test show all effective option values`() { + setOsSpecificOptionsToSafeValues() + assertCommandOutput("set all", + """ + |--- Options --- + |noargtextobj ideawrite=all scrolljump=1 notextobj-indent + | closenotebooks noignorecase scrolloff=0 timeout + |nocommentary noincsearch selectmode= timeoutlen=1000 + |nodigraph nomatchit shellcmdflag=-x notrackactionids + |noexchange maxmapdepth=20 shellxescape=@ undolevels=1000 + |nogdefault more shellxquote={ unifyjumps + |nohighlightedyank nomultiple-cursors showcmd virtualedit= + | history=50 noNERDTree showmode novisualbell + |nohlsearch nrformats=hex sidescroll=0 visualdelay=100 + |noideaglobalmode nonumber sidescrolloff=0 whichwrap=b,s + |noideajoin nooctopushandler nosmartcase wrapscan + | ideamarks oldundo startofline + | ideastrictmode norelativenumber nosurround + |noideatracetime scroll=0 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 + | ide=IntelliJ IDEA Community Edition + |noideacopypreprocess + | idearefactormode=select + | ideastatusicon=enabled + | ideavimsupport=dialog + | iskeyword=@,48-57,_ + | keymodel=continueselect,stopselect + | lookupkeys=,,,,,,,,,,, + | matchpairs=(:),{:},[:] + |noReplaceWithRegister + | selection=inclusive + | shell=/dummy/path/to/bash + |novim-paragraph-motion + | viminfo='100,<50,s10,h + | vimscriptfunctionannotation + | + """.trimMargin()) + } + + @Test + fun `test show named options`() { + assertCommandOutput("set number? relativenumber? scrolloff? nrformats?", """ + | nrformats=hex nonumber norelativenumber scrolloff=0 + |""".trimMargin() + ) + } + + @Test + fun `test show all modified option values in single column`() { + enterCommand("set number relativenumber scrolloff nrformats") + assertCommandOutput("set!", + """ + |--- Options --- + | ideastrictmode + | number + | relativenumber + |""".trimMargin() + ) + } + + @Test + fun `test show all option values in single column`() { + setOsSpecificOptionsToSafeValues() + assertCommandOutput("set! all", """ + |--- Options --- + |noargtextobj + | clipboard=ideaput,autoselect,exclude:cons\|linux + | closenotebooks + |nocommentary + |nodigraph + |noexchange + | excommandannotation + |nogdefault + | 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 + |nohighlightedyank + | history=50 + |nohlsearch + | ide=IntelliJ IDEA Community Edition + |noideacopypreprocess + |noideaglobalmode + |noideajoin + | ideamarks + | idearefactormode=select + | ideastatusicon=enabled + | ideastrictmode + |noideatracetime + | ideavimsupport=dialog + | ideawrite=all + |noignorecase + |noincsearch + | iskeyword=@,48-57,_ + | keymodel=continueselect,stopselect + | lookupkeys=,,,,,,,,,,, + |nomatchit + | matchpairs=(:),{:},[:] + | maxmapdepth=20 + | more + |nomultiple-cursors + |noNERDTree + | nrformats=hex + |nonumber + |nooctopushandler + | oldundo + |norelativenumber + |noReplaceWithRegister + | scroll=0 + | scrolljump=1 + | scrolloff=0 + | selection=inclusive + | selectmode= + | shell=/dummy/path/to/bash + | shellcmdflag=-x + | shellxescape=@ + | shellxquote={ + | showcmd + | showmode + | sidescroll=0 + | sidescrolloff=0 + |nosmartcase + | startofline + |nosurround + |notextobj-entire + |notextobj-indent + | timeout + | timeoutlen=1000 + |notrackactionids + | undolevels=1000 + | unifyjumps + |novim-paragraph-motion + | viminfo='100,<50,s10,h + | vimscriptfunctionannotation + | virtualedit= + |novisualbell + | visualdelay=100 + | whichwrap=b,s + | wrapscan + |""".trimMargin() + ) + } + + @Test + fun `test show named options in single column`() { + assertCommandOutput("set! number? relativenumber? scrolloff? nrformats?", """ + | nrformats=hex + |nonumber + |norelativenumber + | scrolloff=0 + |""".trimMargin() + ) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/SetCommand.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/SetCommand.kt index 56f6147fc4..f3ba5aa6b1 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/SetCommand.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/SetCommand.kt @@ -34,7 +34,6 @@ import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString import java.util.* import kotlin.math.ceil -import kotlin.math.min /** * see "h :set" @@ -95,17 +94,21 @@ public data class SetLocalCommand(val ranges: Ranges, val argument: String) : Co public fun parseOptionLine(editor: VimEditor, args: String, scope: OptionScope, failOnBad: Boolean): Boolean { // No arguments so we show changed values val optionGroup = injector.optionGroup + + val columnFormat = args.startsWith("!") + val argument = args.removePrefix("!").trimStart() + when { - args.isEmpty() -> { + argument.isEmpty() -> { val changedOptions = optionGroup.getAllOptions().filter { !optionGroup.isDefaultValue(it, scope) } - showOptions(editor, changedOptions.map { Pair(it.name, it.name) }, scope, true) + showOptions(editor, changedOptions.map { Pair(it.name, it.name) }, scope, true, columnFormat) return true } - args == "all" -> { - showOptions(editor, optionGroup.getAllOptions().map { Pair(it.name, it.name) }, scope, true) + argument == "all" -> { + showOptions(editor, optionGroup.getAllOptions().map { Pair(it.name, it.name) }, scope, true, columnFormat) return true } - args == "all&" -> { + argument == "all&" -> { optionGroup.resetAllOptions() return true } @@ -114,7 +117,7 @@ public fun parseOptionLine(editor: VimEditor, args: String, scope: OptionScope, // We now have 1 or more option operators separator by spaces var error: String? = null var token = "" - val tokenizer = StringTokenizer(args) + val tokenizer = StringTokenizer(argument) val toShow = mutableListOf>() while (tokenizer.hasMoreTokens()) { token = tokenizer.nextToken() @@ -183,7 +186,7 @@ public fun parseOptionLine(editor: VimEditor, args: String, scope: OptionScope, // Now show all options that were individually requested if (toShow.size > 0) { - showOptions(editor, toShow, scope, false) + showOptions(editor, toShow, scope, false, columnFormat) } if (error != null) { @@ -199,11 +202,18 @@ private fun getValidOption(optionName: String, token: String = optionName) = private fun getValidToggleOption(optionName: String, token: String) = getValidOption(optionName, token) as? ToggleOption ?: throw exExceptionMessage("E474", token) -private fun showOptions(editor: VimEditor, nameAndToken: Collection>, scope: OptionScope, showIntro: Boolean) { +private fun showOptions( + editor: VimEditor, + nameAndToken: Collection>, + scope: OptionScope, + showIntro: Boolean, + columnFormat: Boolean +) { val optionService = injector.optionGroup val optionsToShow = mutableListOf>() var unknownOption: Pair? = null - for (pair in nameAndToken) { + + for (pair in nameAndToken.sortedWith { o1, o2 -> String.CASE_INSENSITIVE_ORDER.compare(o1.first, o2.first) }) { val myOption = optionService.getOption(pair.first) if (myOption != null) { optionsToShow.add(myOption) @@ -213,54 +223,47 @@ private fun showOptions(editor: VimEditor, nameAndToken: Collection() + val colWidth = 20 + val cells = mutableListOf() val extra = mutableListOf() for (option in optionsToShow) { val optionAsString = formatKnownOptionValue(option, scope) - if (optionAsString.length > 19) extra.add(optionAsString) else cols.add(optionAsString) + if (columnFormat || optionAsString.length >= colWidth) extra.add(optionAsString) else cells.add(optionAsString) } - cols.sort() - extra.sort() + // Note that this is the approximate width of the associated editor, not the ex output panel! + // It excludes gutter width, for example + val width = injector.engineEditorHelper.getApproximateScreenWidth(editor).let { if (it < 20) 80 else it } + val colCount = width / colWidth + val height = ceil(cells.size.toDouble() / colCount.toDouble()).toInt() - var width = injector.engineEditorHelper.getApproximateScreenWidth(editor) - if (width < 20) { - width = 80 - } - val colCount = width / 20 - val height = ceil(cols.size.toDouble() / colCount.toDouble()).toInt() - var empty = cols.size % colCount - empty = if (empty == 0) colCount else empty + val output = buildString { + if (showIntro) { + appendLine("--- Options ---") + } - val res = StringBuilder() - if (showIntro) { - res.append("--- Options ---\n") - } - for (h in 0 until height) { - for (c in 0 until colCount) { - if (h == height - 1 && c >= empty) { - break - } + for (h in 0 until height) { + val lengthAtStartOfLine = length + for (c in 0 until colCount) { + val index = c * height + h + if (index < cells.size) { + val padLength = lengthAtStartOfLine + (c * colWidth) - length + for (i in 1..padLength) { + append(' ') + } - var pos = c * height + h - if (c > empty) { - pos -= c - empty + append(cells[index]) + } } - - val opt = cols[pos] - res.append(opt.padEnd(20)) + appendLine() } - res.append("\n") - } - for (opt in extra) { - val seg = (opt.length - 1) / width - for (j in 0..seg) { - res.append(opt, j * width, min(j * width + width, opt.length)) - res.append("\n") + // Add any lines that are too long to fit into columns. The panel will soft wrap text + for (option in extra) { + appendLine(option) } } - injector.exOutputPanel.getPanel(editor).output(res.toString()) + injector.exOutputPanel.getPanel(editor).output(output) if (unknownOption != null) { throw exExceptionMessage("E518", unknownOption.second) @@ -272,7 +275,7 @@ private fun formatKnownOptionValue(option: Option, scope: Optio return if (option is ToggleOption) { if (value.asBoolean()) " ${option.name}" else "no${option.name}" } else { - "${option.name}=$value" + " ${option.name}=$value" } }