/* * 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 com.maddyhome.idea.vim.group; import com.google.common.collect.ImmutableList; import com.intellij.codeInsight.lookup.impl.LookupImpl; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionManagerEx; import com.intellij.openapi.actionSystem.ex.ActionUtil; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.keymap.Keymap; import com.intellij.openapi.keymap.KeymapManager; import com.intellij.openapi.keymap.ex.KeymapManagerEx; import com.maddyhome.idea.vim.EventFacade; import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.action.VimShortcutKeyAction; import com.maddyhome.idea.vim.action.change.LazyVimCommand; import com.maddyhome.idea.vim.api.*; import com.maddyhome.idea.vim.command.MappingMode; import com.maddyhome.idea.vim.key.*; import com.maddyhome.idea.vim.newapi.IjNativeAction; import com.maddyhome.idea.vim.newapi.IjVimEditor; import kotlin.Pair; import kotlin.text.StringsKt; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.util.List; import java.util.*; import static com.maddyhome.idea.vim.api.VimInjectorKt.injector; import static java.util.stream.Collectors.toList; /** * @author vlan */ @State(name = "VimKeySettings", storages = {@Storage(value = "$APP_CONFIG$/vim_settings.xml")}) public class KeyGroup extends VimKeyGroupBase implements PersistentStateComponent<Element>{ public static final @NonNls String SHORTCUT_CONFLICTS_ELEMENT = "shortcut-conflicts"; private static final @NonNls String SHORTCUT_CONFLICT_ELEMENT = "shortcut-conflict"; private static final @NonNls String OWNER_ATTRIBUTE = "owner"; private static final String TEXT_ELEMENT = "text"; private static final Logger logger = Logger.getInstance(KeyGroup.class); public void registerRequiredShortcutKeys(@NotNull VimEditor editor) { EventFacade.getInstance() .registerCustomShortcutSet(VimShortcutKeyAction.getInstance(), toShortcutSet(getRequiredShortcutKeys()), ((IjVimEditor)editor).getEditor().getComponent()); } public void registerShortcutsForLookup(@NotNull LookupImpl lookup) { EventFacade.getInstance() .registerCustomShortcutSet(VimShortcutKeyAction.getInstance(), toShortcutSet(getRequiredShortcutKeys()), lookup.getComponent(), lookup); } void unregisterShortcutKeys(@NotNull VimEditor editor) { EventFacade.getInstance().unregisterCustomShortcutSet(VimShortcutKeyAction.getInstance(), ((IjVimEditor)editor).getEditor().getComponent()); } @Override public void updateShortcutKeysRegistration() { for (VimEditor editor : injector.getEditorGroup().getEditors()) { unregisterShortcutKeys(editor); registerRequiredShortcutKeys(editor); } } public void saveData(@NotNull Element element) { final Element conflictsElement = new Element(SHORTCUT_CONFLICTS_ELEMENT); for (Map.Entry<KeyStroke, ShortcutOwnerInfo> entry : myShortcutConflicts.entrySet()) { final ShortcutOwner owner; ShortcutOwnerInfo myValue = entry.getValue(); if (myValue instanceof ShortcutOwnerInfo.AllModes) { owner = ((ShortcutOwnerInfo.AllModes)myValue).getOwner(); } else if (myValue instanceof ShortcutOwnerInfo.PerMode) { owner = null; } else { throw new RuntimeException(); } if (owner != null && owner != ShortcutOwner.UNDEFINED) { final Element conflictElement = new Element(SHORTCUT_CONFLICT_ELEMENT); conflictElement.setAttribute(OWNER_ATTRIBUTE, owner.getOwnerName()); final Element textElement = new Element(TEXT_ELEMENT); VimPlugin.getXML().setSafeXmlText(textElement, entry.getKey().toString()); conflictElement.addContent(textElement); conflictsElement.addContent(conflictElement); } } element.addContent(conflictsElement); } public void readData(@NotNull Element element) { final Element conflictsElement = element.getChild(SHORTCUT_CONFLICTS_ELEMENT); if (conflictsElement != null) { final java.util.List<Element> conflictElements = conflictsElement.getChildren(SHORTCUT_CONFLICT_ELEMENT); for (Element conflictElement : conflictElements) { final String ownerValue = conflictElement.getAttributeValue(OWNER_ATTRIBUTE); ShortcutOwner owner = ShortcutOwner.UNDEFINED; try { owner = ShortcutOwner.fromString(ownerValue); } catch (IllegalArgumentException ignored) { } final Element textElement = conflictElement.getChild(TEXT_ELEMENT); if (textElement != null) { final String text = VimPlugin.getXML().getSafeXmlText(textElement); if (text != null) { final KeyStroke keyStroke = KeyStroke.getKeyStroke(text); if (keyStroke != null) { myShortcutConflicts.put(keyStroke, new ShortcutOwnerInfo.AllModes(owner)); } } } } } } @Override public @NotNull List<NativeAction> getKeymapConflicts(@NotNull KeyStroke keyStroke) { final KeymapManagerEx keymapManager = KeymapManagerEx.getInstanceEx(); final Keymap keymap = keymapManager.getActiveKeymap(); final KeyboardShortcut shortcut = new KeyboardShortcut(keyStroke, null); final Map<String, ? extends List<KeyboardShortcut>> conflicts = keymap.getConflicts("", shortcut); final List<AnAction> actions = new ArrayList<>(); for (String actionId : conflicts.keySet()) { final AnAction action = ActionManagerEx.getInstanceEx().getAction(actionId); if (action != null) { actions.add(action); } } return actions.stream().map(IjNativeAction::new).collect(toList()); } public @NotNull Map<KeyStroke, ShortcutOwnerInfo> getShortcutConflicts() { final Set<RequiredShortcut> requiredShortcutKeys = this.getRequiredShortcutKeys(); final Map<KeyStroke, ShortcutOwnerInfo> savedConflicts = getSavedShortcutConflicts(); final Map<KeyStroke, ShortcutOwnerInfo> results = new HashMap<>(); for (RequiredShortcut requiredShortcut : requiredShortcutKeys) { KeyStroke keyStroke = requiredShortcut.getKeyStroke(); if (!VimShortcutKeyAction.VIM_ONLY_EDITOR_KEYS.contains(keyStroke)) { final List<NativeAction> conflicts = getKeymapConflicts(keyStroke); if (!conflicts.isEmpty()) { ShortcutOwnerInfo owner = savedConflicts.get(keyStroke); if (owner == null) { owner = ShortcutOwnerInfo.allUndefined; } results.put(keyStroke, owner); } } } return results; } /** * Registers a shortcut that is handled by KeyHandler#handleKey directly, rather than by an action * * <p> * Digraphs are handled directly by KeyHandler#handleKey instead of via an action, but we need to still make sure the * shortcuts are registered, or the key handler won't see them * </p> * * @param keyStroke The shortcut to register */ public void registerShortcutWithoutAction(KeyStroke keyStroke, MappingOwner owner) { registerRequiredShortcut(Collections.singletonList(keyStroke), owner); } public void registerCommandAction(@NotNull LazyVimCommand command) { if (ApplicationManager.getApplication().isUnitTestMode()) { initIdentityChecker(); for (List<KeyStroke> keys : command.getKeys()) { checkCommand(command.getModes(), command, keys); } } for (List<KeyStroke> keyStrokes : command.getKeys()) { registerRequiredShortcut(keyStrokes, MappingOwner.IdeaVim.System.INSTANCE); for (MappingMode mappingMode : command.getModes()) { getBuiltinCommandsTrie(mappingMode).add(keyStrokes, command); } } } private void registerRequiredShortcut(@NotNull List<KeyStroke> keys, MappingOwner owner) { for (KeyStroke key : keys) { if (key.getKeyChar() == KeyEvent.CHAR_UNDEFINED) { if (!injector.getApplication().isOctopusEnabled() || !(key.getKeyCode() == KeyEvent.VK_ESCAPE && key.getModifiers() == 0) && !(key.getKeyCode() == KeyEvent.VK_ENTER && key.getModifiers() == 0)) { getRequiredShortcutKeys().add(new RequiredShortcut(key, owner)); } } } } public static @NotNull ShortcutSet toShortcutSet(@NotNull Collection<RequiredShortcut> requiredShortcuts) { final List<Shortcut> shortcuts = new ArrayList<>(); for (RequiredShortcut key : requiredShortcuts) { shortcuts.add(new KeyboardShortcut(key.getKeyStroke(), null)); } return new CustomShortcutSet(shortcuts.toArray(new Shortcut[0])); } private static @NotNull List<Pair<EnumSet<MappingMode>, MappingInfo>> getKeyMappingRows(@NotNull Set<? extends MappingMode> modes) { final Map<ImmutableList<KeyStroke>, EnumSet<MappingMode>> actualModes = new HashMap<>(); for (MappingMode mode : modes) { final KeyMapping mapping = VimPlugin.getKey().getKeyMapping(mode); for (List<? extends KeyStroke> fromKeys : mapping) { final ImmutableList<KeyStroke> key = ImmutableList.copyOf(fromKeys); final EnumSet<MappingMode> value = actualModes.get(key); final EnumSet<MappingMode> newValue; if (value != null) { newValue = value.clone(); newValue.add(mode); } else { newValue = EnumSet.of(mode); } actualModes.put(key, newValue); } } final List<Pair<EnumSet<MappingMode>, MappingInfo>> rows = new ArrayList<>(); for (Map.Entry<ImmutableList<KeyStroke>, EnumSet<MappingMode>> entry : actualModes.entrySet()) { final ArrayList<KeyStroke> fromKeys = new ArrayList<>(entry.getKey()); final EnumSet<MappingMode> mappingModes = entry.getValue(); if (!mappingModes.isEmpty()) { final MappingMode mode = mappingModes.iterator().next(); final KeyMapping mapping = VimPlugin.getKey().getKeyMapping(mode); final MappingInfo mappingInfo = mapping.get(fromKeys); if (mappingInfo != null) { rows.add(new Pair<>(mappingModes, mappingInfo)); } } } rows.sort(Comparator.comparing(Pair<EnumSet<MappingMode>, MappingInfo>::getSecond)); return rows; } private static @NotNull @NonNls String getModesStringCode(@NotNull Set<MappingMode> modes) { if (modes.equals(MappingMode.NVO)) { return ""; } else if (modes.contains(MappingMode.INSERT)) { return "i"; } else if (modes.contains(MappingMode.NORMAL)) { return "n"; } // TODO: Add more codes return ""; } private @NotNull List<AnAction> getActions(@NotNull Component component, @NotNull KeyStroke keyStroke) { final List<AnAction> results = new ArrayList<>(); results.addAll(getLocalActions(component, keyStroke)); results.addAll(getKeymapActions(keyStroke)); return results; } @Override public @NotNull List<NativeAction> getActions(@NotNull VimEditor editor, @NotNull KeyStroke keyStroke) { return getActions(((IjVimEditor)editor).getEditor().getComponent(), keyStroke).stream() .map(IjNativeAction::new).collect(toList()); } private static @NotNull List<AnAction> getLocalActions(@NotNull Component component, @NotNull KeyStroke keyStroke) { final List<AnAction> results = new ArrayList<>(); final KeyboardShortcut keyStrokeShortcut = new KeyboardShortcut(keyStroke, null); for (Component c = component; c != null; c = c.getParent()) { if (c instanceof JComponent) { final List<AnAction> actions = ActionUtil.getActions((JComponent)c); for (AnAction action : actions) { if (action instanceof VimShortcutKeyAction) { continue; } final Shortcut[] shortcuts = action.getShortcutSet().getShortcuts(); for (Shortcut shortcut : shortcuts) { if (shortcut.isKeyboard() && shortcut.startsWith(keyStrokeShortcut) && !results.contains(action)) { results.add(action); } } } } } return results; } private static @NotNull List<AnAction> getKeymapActions(@NotNull KeyStroke keyStroke) { final List<AnAction> results = new ArrayList<>(); final Keymap keymap = KeymapManager.getInstance().getActiveKeymap(); for (String id : keymap.getActionIds(keyStroke)) { final AnAction action = ActionManager.getInstance().getAction(id); if (action != null) { results.add(action); } } return results; } @Nullable @Override public Element getState() { @NonNls Element element = new Element("key"); saveData(element); return element; } @Override public void loadState(@NotNull Element state) { readData(state); } @Override public boolean showKeyMappings(@NotNull Set<? extends MappingMode> modes, @NotNull VimEditor editor) { List<Pair<EnumSet<MappingMode>, MappingInfo>> rows = getKeyMappingRows(modes); final StringBuilder builder = new StringBuilder(); for (Pair<EnumSet<MappingMode>, MappingInfo> row : rows) { MappingInfo mappingInfo = row.getSecond(); builder.append(StringsKt.padEnd(getModesStringCode(row.getFirst()), 2, ' ')); builder.append(" "); builder.append(StringsKt.padEnd(VimInjectorKt.getInjector().getParser().toKeyNotation(mappingInfo.getFromKeys()), 11, ' ')); builder.append(" "); builder.append(mappingInfo.isRecursive() ? " " : "*"); builder.append(" "); builder.append(mappingInfo.getPresentableString()); builder.append("\n"); } VimOutputPanel outputPanel = injector.getOutputPanel().getOrCreate(editor, injector.getExecutionContextManager().getEditorExecutionContext(editor)); outputPanel.addText(builder.toString(), true); outputPanel.show(); return true; } }