mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-05-16 21:34:04 +02:00
355 lines
14 KiB
Java
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;
|
|
}
|
|
}
|