1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-04-19 01:15:44 +02:00
IntelliJ-IdeaVim/src/test/java/org/jetbrains/plugins/ideavim/VimTestCase.kt
2023-03-27 11:14:00 +03:00

823 lines
31 KiB
Kotlin

/*
* 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,
}
}
}