diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginDisposableService.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginDisposableService.kt
new file mode 100644
index 0000000..d9f1e06
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginDisposableService.kt
@@ -0,0 +1,9 @@
+package com.chylex.intellij.keyboardmaster
+
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.components.Service
+
+@Service
+internal class PluginDisposableService : Disposable {
+	override fun dispose() {}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginStartup.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginStartup.kt
index e678afb..9b1785f 100644
--- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginStartup.kt
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginStartup.kt
@@ -1,6 +1,8 @@
 package com.chylex.intellij.keyboardmaster
 
+import com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration
 import com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupKeyHandler
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigation
 import com.intellij.openapi.application.ApplicationManager
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.startup.ProjectActivity
@@ -12,13 +14,20 @@ class PluginStartup : ProjectActivity {
 		if (!isInitialized) {
 			isInitialized = true
 			
+			PluginConfiguration.load()
+			
 			val application = ApplicationManager.getApplication()
 			if (application.isUnitTestMode) {
-				CodeCompletionPopupKeyHandler.registerRawHandler()
+				initialize()
 			}
 			else {
-				application.invokeLater(CodeCompletionPopupKeyHandler.Companion::registerRawHandler)
+				application.invokeLater(::initialize)
 			}
 		}
 	}
+	
+	private fun initialize() {
+		CodeCompletionPopupKeyHandler.registerRawHandler()
+		VimNavigation.register()
+	}
 }
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfigurable.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfigurable.kt
index 51db572..ca6185e 100644
--- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfigurable.kt
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfigurable.kt
@@ -1,6 +1,8 @@
 package com.chylex.intellij.keyboardmaster.configuration
 
 import com.intellij.openapi.options.Configurable
+import com.intellij.openapi.ui.Messages
+import com.intellij.ui.components.JBCheckBox
 import com.intellij.ui.components.JBTextField
 import com.intellij.ui.dsl.builder.panel
 import javax.swing.JComponent
@@ -12,6 +14,14 @@ class PluginConfigurable : Configurable {
 	private val codeCompletionNextPageShortcut = JBTextField(2)
 	private val codeCompletionPrevPageShortcut = JBTextField(2)
 	
+	private val enableVimNavigation = JBCheckBox("Vim-style navigation in lists / trees / tables").also { checkBox ->
+		checkBox.addActionListener {
+			if (!checkBox.isSelected) {
+				Messages.showInfoMessage(checkBox, "Vim-style navigation will be fully disabled after restarting the IDE.", "Keyboard Master")
+			}
+		}
+	}
+	
 	override fun getDisplayName(): String {
 		return "Keyboard Master"
 	}
@@ -23,6 +33,10 @@ class PluginConfigurable : Configurable {
 				row("Next page shortcut:") { cell(codeCompletionNextPageShortcut) }
 				row("Prev page shortcut:") { cell(codeCompletionPrevPageShortcut) }
 			}
+			
+			group("Navigation") {
+				row { cell(enableVimNavigation) }
+			}
 		}
 		
 		return component
@@ -37,6 +51,8 @@ class PluginConfigurable : Configurable {
 			it.codeCompletionItemShortcuts = codeCompletionItemShortcuts.text
 			it.codeCompletionNextPageShortcut = codeCompletionNextPageShortcut.text.firstOrNull()?.code ?: 0
 			it.codeCompletionPrevPageShortcut = codeCompletionPrevPageShortcut.text.firstOrNull()?.code ?: 0
+			
+			it.enableVimNavigation = enableVimNavigation.isSelected
 		}
 	}
 	
@@ -45,6 +61,8 @@ class PluginConfigurable : Configurable {
 			codeCompletionItemShortcuts.text = it.codeCompletionItemShortcuts
 			codeCompletionNextPageShortcut.text = it.codeCompletionNextPageShortcut.let { code -> if (code == 0) "" else code.toChar().toString() }
 			codeCompletionPrevPageShortcut.text = it.codeCompletionPrevPageShortcut.let { code -> if (code == 0) "" else code.toChar().toString() }
+			
+			enableVimNavigation.isSelected = it.enableVimNavigation
 		}
 	}
 }
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfiguration.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfiguration.kt
index 8e08870..1253ba4 100644
--- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfiguration.kt
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfiguration.kt
@@ -1,6 +1,7 @@
 package com.chylex.intellij.keyboardmaster.configuration
 
 import com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupConfiguration
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigation
 import com.intellij.openapi.application.ApplicationManager
 import com.intellij.openapi.components.PersistentStateComponent
 import com.intellij.openapi.components.State
@@ -16,6 +17,8 @@ class PluginConfiguration : PersistentStateComponent<PluginConfiguration> {
 	var codeCompletionNextPageShortcut: Int = '0'.code
 	var codeCompletionPrevPageShortcut: Int = 0
 	
+	var enableVimNavigation = false
+	
 	companion object {
 		private val instance: PluginConfiguration
 			get() = ApplicationManager.getApplication().getService(PluginConfiguration::class.java)
@@ -34,6 +37,7 @@ class PluginConfiguration : PersistentStateComponent<PluginConfiguration> {
 		
 		private fun update(instance: PluginConfiguration) = with(instance) {
 			CodeCompletionPopupConfiguration.updateShortcuts(codeCompletionItemShortcuts, codeCompletionNextPageShortcut, codeCompletionPrevPageShortcut)
+			VimNavigation.isEnabled = enableVimNavigation
 		}
 	}
 	
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/codeCompletion/CodeCompletionPopupConfiguration.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/codeCompletion/CodeCompletionPopupConfiguration.kt
index 91cc52f..6aa7118 100644
--- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/codeCompletion/CodeCompletionPopupConfiguration.kt
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/codeCompletion/CodeCompletionPopupConfiguration.kt
@@ -1,6 +1,5 @@
 package com.chylex.intellij.keyboardmaster.feature.codeCompletion
 
-import com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration
 import com.intellij.util.containers.IntIntHashMap
 
 object CodeCompletionPopupConfiguration {
@@ -14,10 +13,6 @@ object CodeCompletionPopupConfiguration {
 	val itemShortcutCount
 		get() = hintTexts.size
 	
-	init {
-		PluginConfiguration.load()
-	}
-	
 	fun updateShortcuts(itemShortcutChars: String, nextPageShortcutCode: Int, previousPageShortcutCode: Int) {
 		charToShortcutMap.clear()
 		
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt
new file mode 100644
index 0000000..ae6e084
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt
@@ -0,0 +1,7 @@
+package com.chylex.intellij.keyboardmaster.feature.vimNavigation
+
+import javax.swing.JComponent
+
+internal interface ComponentHolder {
+	val component: JComponent
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt
new file mode 100644
index 0000000..779d569
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt
@@ -0,0 +1,88 @@
+package com.chylex.intellij.keyboardmaster.feature.vimNavigation
+
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.CustomShortcutSet
+import com.intellij.openapi.actionSystem.CustomizedDataContext
+import com.intellij.openapi.actionSystem.KeyboardShortcut
+import com.intellij.openapi.actionSystem.PlatformDataKeys
+import com.intellij.openapi.actionSystem.ex.ActionUtil
+import com.intellij.util.containers.map2Array
+import java.awt.event.KeyEvent
+import javax.swing.KeyStroke
+
+internal interface KeyStrokeNode<T> {
+	class Parent<T>(private val keys: Map<KeyStroke, KeyStrokeNode<T>>) : KeyStrokeNode<T> {
+		val allKeyStrokes: Set<KeyStroke> = mutableSetOf<KeyStroke>().apply {
+			for ((key, node) in keys) {
+				add(key)
+				
+				if (node is Parent) {
+					addAll(node.allKeyStrokes)
+				}
+			}
+		}
+		
+		fun getChild(keyEvent: KeyEvent): KeyStrokeNode<T> {
+			val keyStroke = when (keyEvent.id) {
+				KeyEvent.KEY_TYPED   -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv())
+				KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false)
+				else                 -> return this
+			}
+			
+			return keys[keyStroke] ?: this
+		}
+		
+		operator fun plus(other: Parent<T>): Parent<T> {
+			val mergedKeys = HashMap(keys)
+			
+			for ((otherKey, otherNode) in other.keys) {
+				if (otherNode is Parent) {
+					val ourNode = keys[otherKey]
+					if (ourNode is Parent) {
+						mergedKeys[otherKey] = ourNode + otherNode
+						continue
+					}
+				}
+				
+				mergedKeys[otherKey] = otherNode
+			}
+			
+			return Parent(mergedKeys)
+		}
+	}
+	
+	interface ActionNode<T> : KeyStrokeNode<T> {
+		fun performAction(holder: T, actionEvent: AnActionEvent, keyEvent: KeyEvent)
+	}
+	
+	class IdeaAction<T : ComponentHolder>(private val name: String) : ActionNode<T> {
+		override fun performAction(holder: T, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
+			val action = actionEvent.actionManager.getAction(name) ?: return
+			
+			val dataContext = CustomizedDataContext.create(actionEvent.dataContext) {
+				when {
+					PlatformDataKeys.CONTEXT_COMPONENT.`is`(it) -> holder.component
+					else                                        -> null
+				}
+			}
+			
+			ActionUtil.invokeAction(action, dataContext, actionEvent.place, null, null)
+		}
+	}
+	
+	companion object {
+		fun <T> getAllShortcuts(root: Parent<T>, extra: Set<KeyStroke>? = null): CustomShortcutSet {
+			val allKeyStrokes = HashSet(root.allKeyStrokes)
+			
+			if (extra != null) {
+				allKeyStrokes.addAll(extra)
+			}
+			
+			for (c in ('a'..'z') + ('A'..'Z')) {
+				allKeyStrokes.add(KeyStroke.getKeyStroke(c))
+			}
+			
+			return CustomShortcutSet(*allKeyStrokes.map2Array { KeyboardShortcut(it, null) })
+		}
+	}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt
new file mode 100644
index 0000000..0285869
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt
@@ -0,0 +1,32 @@
+package com.chylex.intellij.keyboardmaster.feature.vimNavigation
+
+import com.chylex.intellij.keyboardmaster.PluginDisposableService
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimListNavigation
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTableNavigation
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTreeNavigation
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.util.ui.StartupUiUtil
+import java.awt.AWTEvent
+import java.awt.event.FocusEvent
+import javax.swing.JList
+import javax.swing.JTable
+import javax.swing.JTree
+
+object VimNavigation {
+	@Volatile
+	var isEnabled = false
+	
+	fun register() {
+		StartupUiUtil.addAwtListener(::handleEvent, AWTEvent.FOCUS_EVENT_MASK, ApplicationManager.getApplication().getService(PluginDisposableService::class.java))
+	}
+	
+	private fun handleEvent(event: AWTEvent) {
+		if (event is FocusEvent && event.id == FocusEvent.FOCUS_GAINED && isEnabled) {
+			when (val source = event.source) {
+				is JList<*> -> VimListNavigation.install(source)
+				is JTree    -> VimTreeNavigation.install(source)
+				is JTable   -> VimTableNavigation.install(source)
+			}
+		}
+	}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt
new file mode 100644
index 0000000..c996695
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt
@@ -0,0 +1,99 @@
+package com.chylex.intellij.keyboardmaster.feature.vimNavigation
+
+import com.chylex.intellij.keyboardmaster.PluginDisposableService
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.CommonDataKeys
+import com.intellij.openapi.actionSystem.DataContext
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.project.DumbAwareAction
+import com.intellij.pom.Navigatable
+import com.intellij.toolWindow.InternalDecoratorImpl
+import com.intellij.ui.SpeedSearchBase
+import com.intellij.ui.speedSearch.SpeedSearch
+import com.intellij.ui.speedSearch.SpeedSearchSupply
+import java.awt.event.KeyEvent
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.swing.JComponent
+import javax.swing.KeyStroke
+
+internal class VimNavigationDispatcher<T : JComponent>(override val component: T, private val rootNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>>) : DumbAwareAction(), ComponentHolder {
+	companion object {
+		private val DISPOSABLE = ApplicationManager.getApplication().getService(PluginDisposableService::class.java)
+		private val EXTRA_SHORTCUTS = setOf(
+			KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
+			KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
+		)
+		
+		@Suppress("UnstableApiUsage")
+		fun JComponent.getParentToolWindowId(): String? {
+			return InternalDecoratorImpl.findNearestDecorator(this)?.toolWindowId
+		}
+	}
+	
+	private var currentNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>> = rootNode
+	var isSearching = AtomicBoolean(false)
+	
+	init {
+		registerCustomShortcutSet(KeyStrokeNode.getAllShortcuts(rootNode, EXTRA_SHORTCUTS), component, DISPOSABLE)
+		
+		SpeedSearchSupply.getSupply(component, true)?.addChangeListener {
+			if (it.propertyName == SpeedSearchSupply.ENTERED_PREFIX_PROPERTY_NAME && it.oldValue != null && it.newValue == null) {
+				isSearching.set(false)
+			}
+		}
+	}
+	
+	override fun actionPerformed(e: AnActionEvent) {
+		val keyEvent = e.inputEvent as? KeyEvent ?: return
+		
+		if (keyEvent.id == KeyEvent.KEY_PRESSED && handleSpecialKeyPress(keyEvent, e.dataContext)) {
+			currentNode = rootNode
+			return
+		}
+		
+		when (val nextNode = currentNode.getChild(keyEvent)) {
+			is KeyStrokeNode.Parent<VimNavigationDispatcher<T>>     -> currentNode = nextNode
+			is KeyStrokeNode.ActionNode<VimNavigationDispatcher<T>> -> {
+				nextNode.performAction(this, e, keyEvent)
+				currentNode = rootNode
+			}
+		}
+	}
+	
+	private fun handleSpecialKeyPress(keyEvent: KeyEvent, dataContext: DataContext): Boolean {
+		if (keyEvent.keyCode == KeyEvent.VK_ESCAPE) {
+			return true
+		}
+		
+		if (keyEvent.keyCode == KeyEvent.VK_ENTER) {
+			handleEnterKeyPress(dataContext)
+			return true
+		}
+		
+		return false
+	}
+	
+	private fun handleEnterKeyPress(dataContext: DataContext) {
+		if (isSearching.compareAndSet(true, false)) {
+			when (val supply = SpeedSearchSupply.getSupply(component)) {
+				is SpeedSearchBase<*> -> supply.hidePopup()
+				is SpeedSearch        -> supply.reset()
+			}
+		}
+		else {
+			val navigatables = dataContext.getData(CommonDataKeys.NAVIGATABLE_ARRAY)?.filter(Navigatable::canNavigate).orEmpty()
+			for ((index, navigatable) in navigatables.withIndex()) {
+				navigatable.navigate(index == navigatables.lastIndex)
+			}
+		}
+	}
+	
+	override fun update(e: AnActionEvent) {
+		e.presentation.isEnabled = !isSearching.get() || e.inputEvent.let { it is KeyEvent && it.id == KeyEvent.KEY_PRESSED && it.keyCode == KeyEvent.VK_ENTER }
+	}
+	
+	override fun getActionUpdateThread(): ActionUpdateThread {
+		return ActionUpdateThread.BGT
+	}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt
new file mode 100644
index 0000000..2c25481
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt
@@ -0,0 +1,60 @@
+package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
+
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.ActionNode
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.Companion.getParentToolWindowId
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.wm.ToolWindowId
+import com.intellij.openapi.wm.ex.ToolWindowManagerEx
+import com.intellij.ui.speedSearch.SpeedSearchActivator
+import com.intellij.ui.speedSearch.SpeedSearchSupply
+import java.awt.event.KeyEvent
+import javax.swing.JComponent
+import javax.swing.KeyStroke
+
+internal object VimCommonNavigation {
+	fun <T : JComponent> commonRootNode() = Parent<VimNavigationDispatcher<T>>(
+		mapOf(
+			KeyStroke.getKeyStroke('A') to IdeaAction("MaximizeToolWindow"),
+			KeyStroke.getKeyStroke('f') to StartSearch(),
+			KeyStroke.getKeyStroke('I') to ToggleExcludedFilesInProjectView(),
+			KeyStroke.getKeyStroke('m') to IdeaAction("ShowPopupMenu"),
+			KeyStroke.getKeyStroke('r') to IdeaAction("SynchronizeCurrentFile"),
+			KeyStroke.getKeyStroke('R') to IdeaAction("Synchronize"),
+			KeyStroke.getKeyStroke('q') to CloseParentToolWindow(),
+			KeyStroke.getKeyStroke('/') to StartSearch(),
+		)
+	)
+	
+	
+	private class StartSearch<T : JComponent> : ActionNode<VimNavigationDispatcher<T>> {
+		@Suppress("UnstableApiUsage")
+		override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
+			val speedSearch = SpeedSearchSupply.getSupply(holder.component, true) as? SpeedSearchActivator ?: return
+			if (speedSearch.isAvailable) {
+				holder.isSearching.set(true)
+				speedSearch.activate()
+			}
+		}
+	}
+	
+	private class CloseParentToolWindow<T : JComponent> : ActionNode<VimNavigationDispatcher<T>> {
+		override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
+			val project = actionEvent.project ?: return
+			val toolWindowId = holder.component.getParentToolWindowId() ?: return
+			ToolWindowManagerEx.getInstanceEx(project).hideToolWindow(toolWindowId, true)
+		}
+	}
+	
+	private class ToggleExcludedFilesInProjectView<T : JComponent> : ActionNode<VimNavigationDispatcher<T>> {
+		private val showExcludedFilesAction = IdeaAction<VimNavigationDispatcher<T>>("ProjectView.ShowExcludedFiles")
+		
+		override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
+			if (holder.component.getParentToolWindowId() == ToolWindowId.PROJECT_VIEW) {
+				showExcludedFilesAction.performAction(holder, actionEvent, keyEvent)
+			}
+		}
+	}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt
new file mode 100644
index 0000000..07e3e96
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt
@@ -0,0 +1,35 @@
+package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
+
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
+import com.intellij.openapi.ui.getUserData
+import com.intellij.openapi.ui.putUserData
+import com.intellij.openapi.util.Key
+import javax.swing.JList
+import javax.swing.KeyStroke
+
+internal object VimListNavigation {
+	private val KEY = Key.create<VimNavigationDispatcher<JList<*>>>("KeyboardMaster-VimListNavigation")
+	
+	private val ROOT_NODE = VimCommonNavigation.commonRootNode<JList<*>>() + Parent(
+		mapOf(
+			KeyStroke.getKeyStroke('g') to Parent(
+				mapOf(
+					KeyStroke.getKeyStroke('g') to IdeaAction("List-selectFirstRow"),
+				)
+			),
+			KeyStroke.getKeyStroke('G') to IdeaAction("List-selectLastRow"),
+			KeyStroke.getKeyStroke('h') to IdeaAction("List-selectPreviousColumn"),
+			KeyStroke.getKeyStroke('j') to IdeaAction("List-selectNextRow"),
+			KeyStroke.getKeyStroke('k') to IdeaAction("List-selectPreviousRow"),
+			KeyStroke.getKeyStroke('l') to IdeaAction("List-selectNextColumn"),
+		)
+	)
+	
+	fun install(component: JList<*>) {
+		if (component.getUserData(KEY) == null) {
+			component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE))
+		}
+	}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTableNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTableNavigation.kt
new file mode 100644
index 0000000..33b48f6
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTableNavigation.kt
@@ -0,0 +1,35 @@
+package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
+
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
+import com.intellij.openapi.ui.getUserData
+import com.intellij.openapi.ui.putUserData
+import com.intellij.openapi.util.Key
+import javax.swing.JTable
+import javax.swing.KeyStroke
+
+internal object VimTableNavigation {
+	private val KEY = Key.create<VimNavigationDispatcher<JTable>>("KeyboardMaster-VimTableNavigation")
+	
+	private val ROOT_NODE = VimCommonNavigation.commonRootNode<JTable>() + Parent(
+		mapOf(
+			KeyStroke.getKeyStroke('g') to Parent(
+				mapOf(
+					KeyStroke.getKeyStroke('g') to IdeaAction("Table-selectFirstRow"),
+				)
+			),
+			KeyStroke.getKeyStroke('G') to IdeaAction("Table-selectLastRow"),
+			KeyStroke.getKeyStroke('h') to IdeaAction("Table-selectPreviousColumn"),
+			KeyStroke.getKeyStroke('j') to IdeaAction("Table-selectNextRow"),
+			KeyStroke.getKeyStroke('k') to IdeaAction("Table-selectPreviousRow"),
+			KeyStroke.getKeyStroke('l') to IdeaAction("Table-selectNextColumn"),
+		)
+	)
+	
+	fun install(component: JTable) {
+		if (component.getUserData(KEY) == null) {
+			component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE))
+		}
+	}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTreeNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTreeNavigation.kt
new file mode 100644
index 0000000..2ed7c5f
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTreeNavigation.kt
@@ -0,0 +1,115 @@
+package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
+
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.ActionNode
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent
+import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.ui.getUserData
+import com.intellij.openapi.ui.putUserData
+import com.intellij.openapi.util.Key
+import java.awt.event.KeyEvent
+import javax.swing.JTree
+import javax.swing.KeyStroke
+
+internal object VimTreeNavigation {
+	private val KEY = Key.create<VimNavigationDispatcher<JTree>>("KeyboardMaster-VimTreeNavigation")
+	
+	private val ROOT_NODE = VimCommonNavigation.commonRootNode<JTree>() + Parent(
+		mapOf(
+			KeyStroke.getKeyStroke('g') to Parent(
+				mapOf(
+					KeyStroke.getKeyStroke('g') to IdeaAction("Tree-selectFirst"),
+				)
+			),
+			KeyStroke.getKeyStroke('G') to IdeaAction("Tree-selectLast"),
+			KeyStroke.getKeyStroke('j') to IdeaAction("Tree-selectNext"),
+			KeyStroke.getKeyStroke('j', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectNextSibling"),
+			KeyStroke.getKeyStroke('J') to SelectLastSibling,
+			KeyStroke.getKeyStroke('k') to IdeaAction("Tree-selectPrevious"),
+			KeyStroke.getKeyStroke('k', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectPreviousSibling"),
+			KeyStroke.getKeyStroke('K') to SelectFirstSibling,
+			KeyStroke.getKeyStroke('o') to ExpandOrCollapseTreeNode,
+			KeyStroke.getKeyStroke('O') to IdeaAction("FullyExpandTreeNode"),
+			KeyStroke.getKeyStroke('p') to IdeaAction("Tree-selectParentNoCollapse"),
+			KeyStroke.getKeyStroke('P') to IdeaAction("Tree-selectFirst"),
+			KeyStroke.getKeyStroke('x') to CollapseSelfOrParentNode,
+			KeyStroke.getKeyStroke('X') to IdeaAction("CollapseTreeNode"),
+		)
+	)
+	
+	fun install(component: JTree) {
+		if (component.getUserData(KEY) == null) {
+			component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE))
+		}
+	}
+	
+	private data object ExpandOrCollapseTreeNode : ActionNode<VimNavigationDispatcher<JTree>> {
+		override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
+			val tree = holder.component
+			val path = tree.selectionPath ?: return
+			
+			if (tree.isExpanded(path)) {
+				tree.collapsePath(path)
+			}
+			else {
+				tree.expandPath(path)
+			}
+		}
+	}
+	
+	private data object CollapseSelfOrParentNode : ActionNode<VimNavigationDispatcher<JTree>> {
+		override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
+			val tree = holder.component
+			val path = tree.selectionPath ?: return
+			
+			if (tree.isExpanded(path)) {
+				tree.collapsePath(path)
+			}
+			else {
+				val parentPath = path.parentPath
+				if (parentPath.parentPath != null || tree.isRootVisible) {
+					tree.collapsePath(parentPath)
+				}
+			}
+		}
+	}
+	
+	private data object SelectFirstSibling : ActionNode<VimNavigationDispatcher<JTree>> {
+		override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
+			val tree = holder.component
+			val path = tree.selectionPath ?: return
+			
+			val parentPath = path.parentPath ?: return
+			val parentRow = tree.getRowForPath(parentPath)
+			
+			tree.setSelectionRow(parentRow + 1)
+		}
+	}
+	
+	private data object SelectLastSibling : ActionNode<VimNavigationDispatcher<JTree>> {
+		override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
+			val tree = holder.component
+			val path = tree.selectionPath ?: return
+			
+			val siblingPathCount = path.pathCount
+			var testRow = tree.getRowForPath(path)
+			var targetRow = testRow
+			
+			while (true) {
+				testRow++
+				
+				val testPath = tree.getPathForRow(testRow) ?: break
+				val testPathCount = testPath.pathCount
+				if (testPathCount < siblingPathCount) {
+					break
+				}
+				else if (testPathCount == siblingPathCount) {
+					targetRow = testRow
+				}
+			}
+			
+			tree.setSelectionRow(targetRow)
+		}
+	}
+}