/* * Copyright 2003-2023 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at * https://opensource.org/licenses/MIT. */ package org.jetbrains.plugins.ideavim import com.intellij.ide.ClipboardSynchronizer import com.intellij.ide.bookmark.BookmarksManager import com.intellij.ide.highlighter.JavaFileType import com.intellij.ide.highlighter.XmlFileType import com.intellij.json.JsonFileType import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.WriteAction import com.intellij.openapi.editor.CaretVisualAttributes import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Inlay import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.VisualPosition import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileTypes.FileType import com.intellij.openapi.fileTypes.PlainTextFileType import com.intellij.openapi.project.Project import com.intellij.testFramework.EditorTestUtil import com.intellij.testFramework.LightProjectDescriptor import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl import com.intellij.testFramework.junit5.RunInEdt import com.intellij.util.ui.EmptyClipboardOwner import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.action.VimShortcutKeyAction import com.maddyhome.idea.vim.api.VimOptionGroup import com.maddyhome.idea.vim.api.getKnownToggleOption import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.options import com.maddyhome.idea.vim.api.setToggleOption import com.maddyhome.idea.vim.api.visualLineToBufferLine import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.command.VimStateMachine import com.maddyhome.idea.vim.command.VimStateMachine.SubMode import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.ex.ExOutputModel.Companion.getInstance import com.maddyhome.idea.vim.group.visual.VimVisualTimer.swingTimer import com.maddyhome.idea.vim.handler.isOctopusEnabled import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.GuicursorChangeListener import com.maddyhome.idea.vim.helper.RunnableHelper.runWriteCommand import com.maddyhome.idea.vim.helper.TestInputModel import com.maddyhome.idea.vim.helper.editorMode import com.maddyhome.idea.vim.helper.getGuiCursorMode import com.maddyhome.idea.vim.helper.inBlockSubMode import com.maddyhome.idea.vim.helper.subMode import com.maddyhome.idea.vim.key.MappingOwner import com.maddyhome.idea.vim.key.ToKeysMappingInfo import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor 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.options.OptionConstants import com.maddyhome.idea.vim.options.OptionScope import com.maddyhome.idea.vim.options.OptionValueAccessor import com.maddyhome.idea.vim.options.ToggleOption import com.maddyhome.idea.vim.options.helpers.GuiCursorOptionHelper import com.maddyhome.idea.vim.options.helpers.GuiCursorType import com.maddyhome.idea.vim.ui.ex.ExEntryPanel import com.maddyhome.idea.vim.vimscript.model.datatypes.VimFuncref import com.maddyhome.idea.vim.vimscript.parser.errors.IdeavimErrorListener import org.assertj.core.api.Assertions import org.jetbrains.annotations.ApiStatus import org.jetbrains.plugins.ideavim.impl.EmptyTransferable import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.TestInfo import org.junit.jupiter.api.assertThrows import java.awt.event.KeyEvent import java.util.* import javax.swing.KeyStroke import kotlin.math.roundToInt import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue /** * JUnit 5 tests * * To plugin writers: this class is internal, thus not allowed to be used by third-party plugins. * This is done as we have no mechanism to guarantee compatibility as we update this test case. * Feel free to copy this class into your plugin, or copy just needed functions. */ @RunInEdt @ApiStatus.Internal abstract class VimTestCase { protected lateinit var fixture: CodeInsightTestFixture internal lateinit var testInfo: TestInfo @BeforeEach open fun setUp(testInfo: TestInfo) { val factory = IdeaTestFixtureFactory.getFixtureFactory() val projectDescriptor = LightProjectDescriptor.EMPTY_PROJECT_DESCRIPTOR val fixtureBuilder = factory.createLightFixtureBuilder(projectDescriptor, "IdeaVim") val fixture = fixtureBuilder.fixture this.fixture = IdeaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture( fixture, LightTempDirTestFixtureImpl(true), ) this.fixture.setUp() this.fixture.testDataPath = testDataPath // Note that myFixture.editor is usually null here. It's only set once configureByText has been called val editor = this.fixture.editor if (editor != null) { KeyHandler.getInstance().fullReset(editor.vim) } VimPlugin.getOptionGroup().resetAllOptions() VimPlugin.getKey().resetKeyMappings() VimPlugin.getSearch().resetState() if (!VimPlugin.isEnabled()) VimPlugin.setEnabled(true) (injector.optionGroup.getOption(OptionConstants.ideastrictmode) as? ToggleOption)?.let { option -> injector.optionGroup.setToggleOption(option, OptionScope.GLOBAL) } GuicursorChangeListener.processGlobalValueChange(null) Checks.reset() clearClipboard() // Make sure the entry text field gets a bounds, or we won't be able to work out caret location ExEntryPanel.getInstance().entry.setBounds(0, 0, 100, 25) NeovimTesting.setUp(testInfo) VimPlugin.clearError() this.testInfo = testInfo } private val testDataPath: String get() = PathManager.getHomePath() + "/community/plugins/ideavim/testData" @AfterEach open fun tearDown(testInfo: TestInfo) { val swingTimer = swingTimer swingTimer?.stop() val bookmarksManager = BookmarksManager.getInstance(fixture.project) bookmarksManager?.bookmarks?.forEach { bookmark -> bookmarksManager.remove(bookmark) } SelectionVimListenerSuppressor.lock().use { fixture.tearDown() } ExEntryPanel.getInstance().deactivate(false) VimPlugin.getVariableService().clear() VimFuncref.lambdaCounter = 0 VimFuncref.anonymousCounter = 0 IdeavimErrorListener.testLogger.clear() VimPlugin.getRegister().resetRegisters() VimPlugin.getSearch().resetState() injector.markService.resetAllMarks() injector.jumpService.resetJumps() VimPlugin.getChange().resetRepeat() VimPlugin.getKey().savedShortcutConflicts.clear() // Tear down neovim NeovimTesting.tearDown(testInfo) } protected fun enableExtensions(vararg extensionNames: String) { for (name in extensionNames) { injector.optionGroup.setToggleOption(injector.optionGroup.getKnownToggleOption(name), OptionScope.GLOBAL) } } protected fun <T> assertEmpty(collection: Collection<T>) { assertTrue(collection.isEmpty(), "Collection should be empty, but it contains ${collection.size} elements") } protected fun typeTextInFile(keys: List<KeyStroke?>, fileContents: String): Editor { configureByText(fileContents) return typeText(keys) } protected fun typeTextInFile(keys: String, fileContents: String): Editor { configureByText(fileContents) return typeText(keys) } protected val screenWidth: Int get() = 80 protected val screenHeight: Int get() = 35 protected fun setEditorVisibleSize(width: Int, height: Int) { val w = (width * EditorHelper.getPlainSpaceWidthFloat(fixture.editor)).roundToInt() val h = height * fixture.editor.lineHeight EditorTestUtil.setEditorVisibleSizeInPixels(fixture.editor, w, h) } protected fun setEditorVirtualSpace() { // Enable virtual space at the bottom of the file and force a layout to pick up the changes fixture.editor.settings.isAdditionalPageAtBottom = true (fixture.editor as EditorEx).scrollPane.viewport.doLayout() } protected fun configureByText(content: String) = configureByText(PlainTextFileType.INSTANCE, content) protected fun configureByJavaText(content: String) = configureByText(JavaFileType.INSTANCE, content) protected fun configureByXmlText(content: String) = configureByText(XmlFileType.INSTANCE, content) protected fun configureByJsonText(@Suppress("SameParameterValue") content: String) = configureByText(JsonFileType.INSTANCE, content) protected fun configureAndGuard(content: String) { val ranges = extractBrackets(content) for ((start, end) in ranges) { fixture.editor.document.createGuardedBlock(start, end) } } protected fun configureAndFold(content: String, @Suppress("SameParameterValue") placeholder: String) { val ranges = extractBrackets(content) fixture.editor.foldingModel.runBatchFoldingOperation { for ((start, end) in ranges) { val foldRegion = fixture.editor.foldingModel.addFoldRegion(start, end, placeholder) foldRegion?.isExpanded = false } } } private fun extractBrackets(content: String): ArrayList<Pair<Int, Int>> { var myContent = content.replace(c, "").replace(s, "").replace(se, "") val ranges = ArrayList<Pair<Int, Int>>() while (true) { val start = myContent.indexOfFirst { it == '[' } if (start < 0) break myContent = myContent.removeRange(start, start + 1) val end = myContent.indexOfFirst { it == ']' } if (end < 0) break myContent = myContent.removeRange(end, end + 1) ranges.add(start to end) } configureByText(content.replace("[", "").replace("]", "")) return ranges } private fun configureByText(fileType: FileType, content: String): Editor { fixture.configureByText(fileType, content) NeovimTesting.setupEditor(fixture.editor, testInfo) setEditorVisibleSize(screenWidth, screenHeight) return fixture.editor } private fun configureByText(fileName: String, content: String): Editor { fixture.configureByText(fileName, content) NeovimTesting.setupEditor(fixture.editor, testInfo) setEditorVisibleSize(screenWidth, screenHeight) return fixture.editor } protected fun configureByFileName(fileName: String): Editor { fixture.configureByText(fileName, "\n") NeovimTesting.setupEditor(fixture.editor, testInfo) setEditorVisibleSize(screenWidth, screenHeight) return fixture.editor } @Suppress("SameParameterValue") protected fun configureByPages(pageCount: Int) { val stringBuilder = StringBuilder() repeat(pageCount * screenHeight) { stringBuilder.appendLine("Lorem ipsum dolor sit amet,") } configureByText(stringBuilder.toString()) } protected fun configureByLines(lineCount: Int, line: String) { val stringBuilder = StringBuilder() repeat(lineCount - 1) { stringBuilder.appendLine(line) } stringBuilder.append(line) configureByText(stringBuilder.toString()) } protected fun configureByColumns(columnCount: Int) { val content = buildString { repeat(columnCount) { append('0' + (it % 10)) } } configureByText(content) } @JvmOverloads protected fun setPositionAndScroll(scrollToLogicalLine: Int, caretLogicalLine: Int, caretLogicalColumn: Int = 0) { // Note that it is possible to request a position which would be invalid under normal Vim! // We disable scrolloff + scrolljump, position as requested, and reset. When resetting scrolloff, Vim will // recalculate the correct offsets, and that could move the top and/or caret line val scrolloff = options().getIntValue(OptionConstants.scrolloff) val scrolljump = options().getIntValue(OptionConstants.scrolljump) enterCommand("set scrolloff=0") enterCommand("set scrolljump=1") typeText("${scrollToLogicalLine + 1}z<CR>", "${caretLogicalLine + 1}G", "${caretLogicalColumn + 1}|") enterCommand("set scrolloff=$scrolloff") enterCommand("set scrolljump=$scrolljump") // Make sure we're where we want to be. If there are block inlays, we can't easily assert the bottom line because // we'd have to duplicate the scrolling logic here. Asserting top when we know height is good enough assertTopLogicalLine(scrollToLogicalLine) assertPosition(caretLogicalLine, caretLogicalColumn) // Belt and braces. Let's make sure that the caret is fully onscreen val bottomLogicalLine = fixture.editor.vim.visualLineToBufferLine( EditorHelper.getVisualLineAtBottomOfScreen(fixture.editor), ) assertTrue(bottomLogicalLine >= caretLogicalLine) assertTrue(caretLogicalLine >= scrollToLogicalLine) } protected fun typeText(vararg keys: String) = typeText(keys.flatMap { injector.parser.parseKeys(it) }) protected fun typeText(keys: List<KeyStroke?>): Editor { val editor = fixture.editor NeovimTesting.typeCommand( keys.filterNotNull().joinToString(separator = "") { injector.parser.toKeyNotation(it) }, testInfo, editor, ) val project = fixture.project when (Checks.keyHandler) { Checks.KeyHandlerMethod.DIRECT_TO_VIM -> typeText(keys.filterNotNull(), editor, project) Checks.KeyHandlerMethod.VIA_IDE -> typeTextViaIde(keys.filterNotNull(), editor) } return editor } protected fun enterCommand(command: String): Editor { return typeText(commandToKeys(command)) } protected fun enterSearch(pattern: String, forwards: Boolean = true): Editor { return typeText(searchToKeys(pattern, forwards)) } protected fun setText(text: String) { WriteAction.runAndWait<RuntimeException> { fixture.editor.document.setText(text) } } /** * Gets an accessor for effective option value * * This will return an accessor to retrieve the effective value for the current editor - a global value for global * options (e.g. 'clipboard') or a (potentially) local value for local to buffer, local to window or global-local * options (e.g. 'iskeyword', 'relativenumber' or 'scrolloff' respectively). Tests are only expected to require * effective values. To test other global/local values, use [VimOptionGroup]. */ protected fun options(): OptionValueAccessor { assertNotNull( fixture.editor, "Editor is null! Move the call to after editor is initialised, or use optionsNoEditor", ) return injector.options(fixture.editor.vim) } /** * Gets an option value accessor purely for global options, when there is no editor available * * Tests should normally use effective option values, via [options], but that requires a test that has created an * editor. If the editor isn't available, this function will return an accessor that can be used to access global * options only. It should not be used to access local options, as there is nothing for them to be local to. * * Note that this isn't handled automatically by [options] to avoid the scenario of trying to use effective values * before the editor has been initialised. */ protected fun optionsNoEditor(): OptionValueAccessor { assertNull(fixture.editor, "Editor is not null! Use options() to access effective option values") return injector.globalOptions() } fun assertState(textAfter: String) { fixture.checkResult(textAfter) NeovimTesting.assertState(fixture.editor, testInfo) } protected fun assertState(modeAfter: VimStateMachine.Mode, subModeAfter: SubMode) { assertMode(modeAfter) assertSubMode(subModeAfter) assertCaretsVisualAttributes() } fun assertPosition(line: Int, column: Int) { val carets = fixture.editor.caretModel.allCarets assertEquals(1, carets.size, "Wrong amount of carets") val actualPosition = carets[0].logicalPosition assertEquals(LogicalPosition(line, column), actualPosition) NeovimTesting.assertCaret(fixture.editor, testInfo) } fun assertVisualPosition(visualLine: Int, visualColumn: Int) { val carets = fixture.editor.caretModel.allCarets assertEquals(1, carets.size, "Wrong amount of carets") val actualPosition = carets[0].visualPosition assertEquals(VisualPosition(visualLine, visualColumn), actualPosition) } fun assertOffset(vararg expectedOffsets: Int) { val carets = fixture.editor.caretModel.allCarets if (expectedOffsets.size == 2 && carets.size == 1) { assertEquals( expectedOffsets.size, carets.size, "Wrong amount of carets. Did you mean to use assertPosition?", ) } assertEquals(expectedOffsets.size, carets.size, "Wrong amount of carets") for (i in expectedOffsets.indices) { assertEquals(expectedOffsets[i], carets[i].offset) } NeovimTesting.assertState(fixture.editor, testInfo) } fun assertOffsetAt(text: String) { val indexOf = fixture.editor.document.charsSequence.indexOf(text) if (indexOf < 0) kotlin.test.fail() assertOffset(indexOf) } // Use logical rather than visual lines, so we can correctly test handling of collapsed folds and soft wraps fun assertVisibleArea(topLogicalLine: Int, bottomLogicalLine: Int) { assertTopLogicalLine(topLogicalLine) assertBottomLogicalLine(bottomLogicalLine) } fun assertTopLogicalLine(topLogicalLine: Int) { val actualVisualTop = EditorHelper.getVisualLineAtTopOfScreen(fixture.editor) val actualLogicalTop = fixture.editor.vim.visualLineToBufferLine(actualVisualTop) assertEquals(topLogicalLine, actualLogicalTop, "Top logical lines don't match") } fun assertBottomLogicalLine(bottomLogicalLine: Int) { val actualVisualBottom = EditorHelper.getVisualLineAtBottomOfScreen(fixture.editor) val actualLogicalBottom = fixture.editor.vim.visualLineToBufferLine(actualVisualBottom) assertEquals(bottomLogicalLine, actualLogicalBottom, "Bottom logical lines don't match") } fun assertVisibleLineBounds(logicalLine: Int, leftLogicalColumn: Int, rightLogicalColumn: Int) { val visualLine = IjVimEditor(fixture.editor).bufferLineToVisualLine(logicalLine) val actualLeftVisualColumn = EditorHelper.getVisualColumnAtLeftOfDisplay(fixture.editor, visualLine) val actualLeftLogicalColumn = fixture.editor.visualToLogicalPosition(VisualPosition(visualLine, actualLeftVisualColumn)).column val actualRightVisualColumn = EditorHelper.getVisualColumnAtRightOfDisplay(fixture.editor, visualLine) val actualRightLogicalColumn = fixture.editor.visualToLogicalPosition(VisualPosition(visualLine, actualRightVisualColumn)).column val expected = ScreenBounds(leftLogicalColumn, rightLogicalColumn) val actual = ScreenBounds(actualLeftLogicalColumn, actualRightLogicalColumn) assertEquals(expected, actual) } fun assertLineCount(expected: Int) { assertEquals(expected, fixture.editor.vim.lineCount()) } fun putMapping(modes: Set<MappingMode>, from: String, to: String, recursive: Boolean) { VimPlugin.getKey().putKeyMapping( modes, injector.parser.parseKeys(from), MappingOwner.IdeaVim.System, injector.parser.parseKeys(to), recursive, ) } fun assertNoMapping(from: String) { val keys = injector.parser.parseKeys(from) for (mode in MappingMode.ALL) { assertNull(VimPlugin.getKey().getKeyMapping(mode)[keys]) } } fun assertNoMapping(from: String, modes: Set<MappingMode>) { val keys = injector.parser.parseKeys(from) for (mode in modes) { assertNull(VimPlugin.getKey().getKeyMapping(mode)[keys]) } } fun assertMappingExists(from: String, to: String, modes: Set<MappingMode>) { val keys = injector.parser.parseKeys(from) val toKeys = injector.parser.parseKeys(to) for (mode in modes) { val info = VimPlugin.getKey().getKeyMapping(mode)[keys] assertNotNull<Any>(info) if (info is ToKeysMappingInfo) { assertEquals(toKeys, info.toKeys) } } } private data class ScreenBounds(val leftLogicalColumn: Int, val rightLogicalColumn: Int) { override fun toString(): String { return "[$leftLogicalColumn-$rightLogicalColumn]" } } fun assertMode(expectedMode: VimStateMachine.Mode) { val mode = fixture.editor.editorMode assertEquals(expectedMode, mode) } fun assertSubMode(expectedSubMode: SubMode) { val subMode = fixture.editor.subMode assertEquals(expectedSubMode, subMode) } fun assertSelection(expected: String?) { val selected = fixture.editor.selectionModel.selectedText assertEquals(expected, selected) } fun assertExOutput(expected: String) { val actual = getInstance(fixture.editor).text assertNotNull("No Ex output", actual) assertEquals(expected, actual) NeovimTesting.typeCommand("<esc>", testInfo, fixture.editor) } fun assertNoExOutput() { val actual = getInstance(fixture.editor).text assertNull(actual, "Ex output not null") } fun assertPluginError(isError: Boolean) { assertEquals(isError, injector.messages.isError()) } fun assertPluginErrorMessageContains(message: String) { Assertions.assertThat(VimPlugin.getMessage()).contains(message) } protected fun assertCaretsVisualAttributes() { if (!Checks.caretShape) return val editor = fixture.editor val attributes = GuiCursorOptionHelper.getAttributes(getGuiCursorMode(editor)) val colour = editor.colorsScheme.getColor(EditorColors.CARET_COLOR) editor.caretModel.allCarets.forEach { caret -> // All carets should be the same except when in block sub mode, where we "hide" them (by drawing a zero width bar) if (caret !== editor.caretModel.primaryCaret && editor.inBlockSubMode) { assertEquals(CaretVisualAttributes.Shape.BAR, caret.visualAttributes.shape) assertEquals(0F, caret.visualAttributes.thickness) } else { val shape = when (attributes.type) { GuiCursorType.BLOCK -> CaretVisualAttributes.Shape.BLOCK GuiCursorType.VER -> CaretVisualAttributes.Shape.BAR GuiCursorType.HOR -> CaretVisualAttributes.Shape.UNDERSCORE } assertEquals(shape, editor.caretModel.primaryCaret.visualAttributes.shape) assertEquals( attributes.thickness / 100.0F, editor.caretModel.primaryCaret.visualAttributes.thickness, ) editor.caretModel.primaryCaret.visualAttributes.color?.let { assertEquals(colour, it) } } } } @JvmOverloads fun doTest( keys: List<String>, before: String, after: String, modeAfter: VimStateMachine.Mode = VimStateMachine.Mode.COMMAND, subModeAfter: SubMode = SubMode.NONE, fileType: FileType? = null, fileName: String? = null, afterEditorInitialized: ((Editor) -> Unit)? = null, ) { doTest( keys.joinToString(separator = ""), before, after, modeAfter, subModeAfter, fileType, fileName, afterEditorInitialized, ) } @JvmOverloads fun doTest( keys: String, before: String, after: String, modeAfter: VimStateMachine.Mode = VimStateMachine.Mode.COMMAND, subModeAfter: SubMode = SubMode.NONE, fileType: FileType? = null, fileName: String? = null, afterEditorInitialized: ((Editor) -> Unit)? = null, ) { if (fileName != null) { configureByText(fileName, before) } else if (fileType != null) { configureByText(fileType, before) } else { configureByText(before) } afterEditorInitialized?.invoke(fixture.editor) performTest(keys, after, modeAfter, subModeAfter) } protected fun performTest(keys: String, after: String, modeAfter: VimStateMachine.Mode, subModeAfter: SubMode) { typeText(keys) PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() assertState(after) assertState(modeAfter, subModeAfter) } protected fun setRegister(register: Char, keys: String) { VimPlugin.getRegister().setKeys(register, injector.parser.stringToKeys(keys)) NeovimTesting.setRegister(register, keys, testInfo) } protected val fileManager: FileEditorManagerEx get() = FileEditorManagerEx.getInstanceEx(fixture.project) // Specify width in columns, not pixels, just like we do for visible screen size. The default text char width differs // per platform (e.g. Windows is 7, Mac is 8) so we can't guarantee correct positioning for tests if we use hard coded // pixel widths protected fun addInlay( offset: Int, relatesToPrecedingText: Boolean, @Suppress("SameParameterValue") widthInColumns: Int, ): Inlay<*> { val widthInPixels = (EditorHelper.getPlainSpaceWidthFloat(fixture.editor) * widthInColumns).roundToInt() return EditorTestUtil.addInlay(fixture.editor, offset, relatesToPrecedingText, widthInPixels) } // As for inline inlays, height is specified as a multiplier of line height, as we can't guarantee the same line // height on all platforms, so can't guarantee correct positioning for tests if we use pixels. This currently limits // us to integer multiples of line heights. I don't think this will cause any issues, but we can change this to a // float if necessary. We'd still be working scaled to the line height, so fractional values should still work. protected fun addBlockInlay( offset: Int, @Suppress("SameParameterValue") showAbove: Boolean, heightInRows: Int, ): Inlay<*> { val widthInColumns = 10 // Arbitrary width. We don't care. val widthInPixels = (EditorHelper.getPlainSpaceWidthFloat(fixture.editor) * widthInColumns).roundToInt() val heightInPixels = fixture.editor.lineHeight * heightInRows return EditorTestUtil.addBlockInlay(fixture.editor, offset, false, showAbove, widthInPixels, heightInPixels) } // Disable or enable checks for the particular test protected inline fun setupChecks(setup: Checks.() -> Unit) { Checks.setup() } protected fun assertExException(expectedErrorMessage: String, action: () -> Unit) { val exception = assertThrows<ExException> { action() } assertEquals(expectedErrorMessage, exception.message) } private fun typeTextViaIde(keys: List<KeyStroke?>, editor: Editor) { TestInputModel.getInstance(editor).setKeyStrokes(keys.filterNotNull()) val inputModel = TestInputModel.getInstance(editor) var key = inputModel.nextKeyStroke() while (key != null) { val keyChar = key.getChar(editor) when (keyChar) { is CharType.CharDetected -> { fixture.type(keyChar.char) PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() } is CharType.EditorAction -> { fixture.performEditorAction(keyChar.name) } CharType.UNDEFINED -> { val event = KeyEvent(editor.component, KeyEvent.KEY_PRESSED, Date().time, key.modifiers, key.keyCode, key.keyChar) val e = AnActionEvent( event, injector.executionContextManager.onEditor(editor.vim).ij, ActionPlaces.KEYBOARD_SHORTCUT, VimShortcutKeyAction.instance.templatePresentation.clone(), ActionManager.getInstance(), 0, ) if (ActionUtil.lastUpdateAndCheckDumb(VimShortcutKeyAction.instance, e, true)) { ActionUtil.performActionDumbAwareWithCallbacks(VimShortcutKeyAction.instance, e) } } } key = inputModel.nextKeyStroke() } } private fun KeyStroke.getChar(editor: Editor): CharType { if (keyChar != KeyEvent.CHAR_UNDEFINED) return CharType.CharDetected(keyChar) if (isOctopusEnabled(this, editor)) { if (keyCode in setOf(KeyEvent.VK_ENTER)) return CharType.CharDetected(keyCode.toChar()) if (keyCode == KeyEvent.VK_ESCAPE) return CharType.EditorAction("EditorEscape") } return CharType.UNDEFINED } private fun clearClipboard() { ClipboardSynchronizer.getInstance().resetContent() ClipboardSynchronizer.getInstance().setContent(EmptyTransferable, EmptyClipboardOwner.INSTANCE) } sealed interface CharType { object UNDEFINED : CharType class CharDetected(val char: Char) : CharType class EditorAction(val name: String) : CharType } companion object { const val c = EditorTestUtil.CARET_TAG const val s = EditorTestUtil.SELECTION_START_TAG const val se = EditorTestUtil.SELECTION_END_TAG fun typeText(keys: List<KeyStroke?>, editor: Editor, project: Project?) { val keyHandler = KeyHandler.getInstance() val dataContext = injector.executionContextManager.onEditor(editor.vim) TestInputModel.getInstance(editor).setKeyStrokes(keys.filterNotNull()) runWriteCommand( project, Runnable { val inputModel = TestInputModel.getInstance(editor) var key = inputModel.nextKeyStroke() while (key != null) { keyHandler.handleKey(editor.vim, key, dataContext) key = inputModel.nextKeyStroke() } }, null, null, ) } @JvmStatic fun commandToKeys(command: String): List<KeyStroke> { val keys: MutableList<KeyStroke> = ArrayList() if (!command.startsWith(":")) { keys.addAll(injector.parser.parseKeys(":")) } keys.addAll(injector.parser.stringToKeys(command)) // Avoids trying to parse 'command ... <args>' as a special char keys.addAll(injector.parser.parseKeys("<Enter>")) return keys } fun exCommand(command: String) = ":$command<CR>" fun searchToKeys(pattern: String, forwards: Boolean): List<KeyStroke> { val keys: MutableList<KeyStroke> = ArrayList() keys.addAll(injector.parser.parseKeys(if (forwards) "/" else "?")) keys.addAll(injector.parser.stringToKeys(pattern)) // Avoids trying to parse 'command ... <args>' as a special char keys.addAll(injector.parser.parseKeys("<CR>")) return keys } fun searchCommand(pattern: String) = "$pattern<CR>" fun String.dotToTab(): String = replace('.', '\t') fun String.dotToSpace(): String = replace('.', ' ') } object Checks { var caretShape: Boolean = true val neoVim = NeoVim() var keyHandler = KeyHandlerMethod.VIA_IDE fun reset() { caretShape = true neoVim.reset() keyHandler = KeyHandlerMethod.VIA_IDE } class NeoVim { var ignoredRegisters: Set<Char> = setOf() var exitOnTearDown = true fun reset() { ignoredRegisters = setOf() exitOnTearDown = true } } enum class KeyHandlerMethod { VIA_IDE, DIRECT_TO_VIM, } } }