1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-05-16 21:34:04 +02:00
IntelliJ-IdeaVim/src/main/java/com/maddyhome/idea/vim/group/KeyGroup.java
Matt Ellis 84c7e1159b Introduce KeyStrokeTrie to find commands
Should also restore compatibility with idea-which-key
2024-11-13 17:57:31 +02:00

355 lines
14 KiB
Java

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