Source code: com/maddyhome/idea/vim/KeyHandler.java
1 package com.maddyhome.idea.vim;
2
3 /*
4 * IdeaVim - A Vim emulator plugin for IntelliJ Idea
5 * Copyright (C) 2003 Rick Maddy
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
20 */
21
22 import com.intellij.openapi.actionSystem.ActionManager;
23 import com.intellij.openapi.actionSystem.AnAction;
24 import com.intellij.openapi.actionSystem.AnActionEvent;
25 import com.intellij.openapi.actionSystem.DataContext;
26 import com.intellij.openapi.diagnostic.Logger;
27 import com.intellij.openapi.editor.Editor;
28 import com.intellij.openapi.editor.actionSystem.TypedActionHandler;
29 import com.maddyhome.idea.vim.command.Argument;
30 import com.maddyhome.idea.vim.command.Command;
31 import com.maddyhome.idea.vim.command.CommandState;
32 import com.maddyhome.idea.vim.group.CommandGroups;
33 import com.maddyhome.idea.vim.group.RegisterGroup;
34 import com.maddyhome.idea.vim.helper.RunnableHelper;
35 import com.maddyhome.idea.vim.key.ArgumentNode;
36 import com.maddyhome.idea.vim.key.BranchNode;
37 import com.maddyhome.idea.vim.key.CommandNode;
38 import com.maddyhome.idea.vim.key.KeyParser;
39 import com.maddyhome.idea.vim.key.Node;
40 import com.maddyhome.idea.vim.key.ParentNode;
41 import java.awt.event.KeyEvent;
42 import java.util.ArrayList;
43 import java.util.Stack;
44 import javax.swing.KeyStroke;
45
46 /**
47 * This handlers every keystroke that the user can argType except those that are still valid hotkeys for various
48 * Idea actions. This is a singleton.
49 */
50 public class KeyHandler
51 {
52 /**
53 * Returns a reference to the singleton instance of this class
54 * @return A reference to the singleton
55 */
56 public static KeyHandler getInstance()
57 {
58 if (instance == null)
59 {
60 instance = new KeyHandler();
61 }
62
63 return instance;
64
65 }
66
67 /**
68 * Creates an instance
69 */
70 private KeyHandler()
71 {
72 reset();
73 }
74
75 /**
76 * Sets the original key handler
77 * @param origHandler The original key handler
78 */
79 public void setOriginalHandler(TypedActionHandler origHandler)
80 {
81 this.origHandler = origHandler;
82 }
83
84 /**
85 * Gets the original key handler
86 * @return The orginal key handler
87 */
88 public TypedActionHandler getOriginalHandler()
89 {
90 return origHandler;
91 }
92
93 /**
94 * This is the main key handler for the Vim plugin. Every keystroke not handled directly by Idea is sent
95 * here for processing.
96 * @param editor The editor the key was typed into
97 * @param key The keystroke typed by the user
98 * @param context The data context
99 */
100 public void handleKey(Editor editor, KeyStroke key, DataContext context)
101 {
102 boolean isRecording = CommandState.getInstance().isRecording();
103 boolean shouldRecord = true;
104 // If this is a "regular" character keystroke, get the character
105 char chKey = key.getKeyChar() == KeyEvent.CHAR_UNDEFINED ? 0 : key.getKeyChar();
106
107 if ((CommandState.getInstance().getMode() == CommandState.MODE_COMMAND || mode == STATE_COMMAND) &&
108 (key.getKeyCode() == KeyEvent.VK_ESCAPE ||
109 (key.getKeyCode() == KeyEvent.VK_C && (key.getModifiers() & KeyEvent.CTRL_MASK) != 0) ||
110 (key.getKeyCode() == '[' && (key.getModifiers() & KeyEvent.CTRL_MASK) != 0)))
111 {
112 if (mode != STATE_COMMAND && count == 0 && currentArg == Argument.NONE && currentCmd.size() == 0 &&
113 CommandGroups.getInstance().getRegister().getCurrentRegister() == RegisterGroup.REGISTER_DEFAULT)
114 {
115 VimPlugin.indicateError();
116 }
117
118 reset();
119 }
120 // At this point the user must be typing in a command. Most commands can be preceeded by a number. Let's
121 // check if a number can be entered at this point, and if so, did the user send us a digit.
122 else if ((CommandState.getInstance().getMode() == CommandState.MODE_COMMAND ||
123 CommandState.getInstance().getMode() == CommandState.MODE_VISUAL) &&
124 mode == STATE_NEW_COMMAND && currentArg != Argument.CHARACTER && Character.isDigit(chKey) &&
125 (count != 0 || chKey != '0'))
126 {
127 // Update the count
128 count = count * 10 + (chKey - '0');
129 }
130 // Pressing delete while entering a count "removes" the last digit entered
131 else if ((CommandState.getInstance().getMode() == CommandState.MODE_COMMAND ||
132 CommandState.getInstance().getMode() == CommandState.MODE_VISUAL) &&
133 mode == STATE_NEW_COMMAND && currentArg != Argument.CHARACTER &&
134 key.getKeyCode() == KeyEvent.VK_DELETE && count != 0)
135 {
136 // "Remove" the last digit sent to us
137 count /= 10;
138 }
139 // If we got this far the user is entering a command or supplying an argument to an entered command.
140 // First let's check to see if we are at the point of expecting a single character argument to a command.
141 else if (currentArg == Argument.CHARACTER)
142 {
143 // We are expecting a character argument - is this a regular character the user typed?
144 // FIX - This doesn't handle r<Enter>, for example
145 if (chKey != 0)
146 {
147 // Create the character argument, add it to the current command, and signal we are ready to process
148 // the command
149 Argument arg = new Argument(chKey);
150 Command cmd = (Command)currentCmd.peek();
151 cmd.setArgument(arg);
152 mode = STATE_READY;
153 }
154 else
155 {
156 // Oops - this isn't a valid character argument
157 mode = STATE_ERROR;
158 }
159 }
160 // If we are this far - sheesh, then the user must be entering a command or a non-single-character argument
161 // to an entered command. Let's figure out which it is
162 else
163 {
164 // For debugging purposes we track the keys entered for this command
165 keys.add(key);
166 logger.debug("keys now " + keys);
167
168 // Ask the key/action tree if this is an appropriate key at this point in the command and if so,
169 // return the node matching this keystroke
170 Node node = currentNode.getChild(key);
171 // If this is a branch node we have entered only part of a multikey command
172 if (node instanceof BranchNode)
173 {
174 // Flag that we aren't allowing any more count digits
175 mode = STATE_COMMAND;
176 currentNode = (BranchNode)node;
177 if (CommandState.getInstance().isRecording())
178 {
179 ArgumentNode arg = (ArgumentNode)((BranchNode)currentNode).getArgumentNode();
180 if (arg != null && (arg.getFlags() & Command.FLAG_NO_ARG_RECORDING) != 0)
181 {
182 handleKey(editor, KeyStroke.getKeyStroke(' '), context);
183 }
184 }
185 }
186 // If this is a command node the user has entered a valid key sequence of a know command
187 else if (node instanceof CommandNode)
188 {
189 // If all does well we are ready to process this command
190 mode = STATE_READY;
191 CommandNode cmdNode = (CommandNode)node;
192 // Did we just get the completed sequence for a motion command argument?
193 if (currentArg == Argument.MOTION)
194 {
195 // We have been expecting a motion argument - is this one?
196 if (cmdNode.getCmdType() == Command.MOTION)
197 {
198 // Create the motion command and add it to the stack
199 Command cmd = new Command(count, cmdNode.getAction(), cmdNode.getCmdType(), cmdNode.getFlags());
200 cmd.setKeys(keys);
201 currentCmd.push(cmd);
202 }
203 else if (cmdNode.getCmdType() == Command.RESET)
204 {
205 currentCmd.clear();
206 Command cmd = new Command(1, cmdNode.getAction(), cmdNode.getCmdType(), cmdNode.getFlags());
207 cmd.setKeys(keys);
208 currentCmd.push(cmd);
209 }
210 else
211 {
212 // Oops - this wasn't a motion command. The user goofed and typed something else
213 mode = STATE_ERROR;
214 }
215 }
216 // The user entered a valid command that doesn't take any arguments
217 else
218 {
219 // Create the command and add it to the stack
220 Command cmd = new Command(count, cmdNode.getAction(), cmdNode.getCmdType(), cmdNode.getFlags());
221 cmd.setKeys(keys);
222 currentCmd.push(cmd);
223
224 // This is a sanity check that the command has a valid action. This should only fail if the
225 // programmer made a typo or forgot to add the action to the plugin.xml file
226 if (cmd.getAction() == null)
227 {
228 logger.error("NULL action for keys '" + keys + "'");
229 mode = STATE_ERROR;
230 }
231 }
232 }
233 // If this is an argument node then the last keystroke was not part of the current command but should
234 // be the first keystroke of the current command's argument
235 else if (node instanceof ArgumentNode)
236 {
237 // Create a new command based on what the user has typed so far, excluding this keystroke.
238 ArgumentNode arg = (ArgumentNode)node;
239 Command cmd = new Command(count, arg.getAction(), arg.getCmdType(), arg.getFlags());
240 cmd.setKeys(keys);
241 currentCmd.push(cmd);
242 // What argType of argument does this command expect?
243 switch (arg.getArgType())
244 {
245 case Argument.CHARACTER:
246 case Argument.MOTION:
247 mode = STATE_NEW_COMMAND;
248 currentArg = arg.getArgType();
249 // Is the current command an operator? If so set the state to only accept "operator pending"
250 // commands
251 if ((arg.getFlags() & Command.FLAG_OP_PEND) != 0)
252 {
253 //CommandState.getInstance().setMappingMode(KeyParser.MAPPING_OP_PEND);
254 CommandState state = CommandState.getInstance();
255 CommandState.getInstance().pushState(state.getMode(), state.getSubMode(),
256 KeyParser.MAPPING_OP_PEND);
257 }
258 break;
259 default:
260 // Oops - we aren't expecting any other argType of argument
261 mode = STATE_ERROR;
262 }
263
264 // If the current keystroke is really the first character of an argument the user needs to enter,
265 // recursively go back and handle this keystroke again with all the state properly updated to
266 // handle the argument
267 if (currentArg != Argument.NONE)
268 {
269 partialReset();
270 boolean saveRecording = isRecording;
271 handleKey(editor, key, context);
272 isRecording = saveRecording;
273 }
274 }
275 else
276 {
277 // If we are in insert/replace mode send this key in for processing
278 if (CommandState.getInstance().getMode() == CommandState.MODE_INSERT ||
279 CommandState.getInstance().getMode() == CommandState.MODE_REPLACE)
280 {
281 if (!CommandGroups.getInstance().getChange().processKey(editor, context, key))
282 {
283 shouldRecord = false;
284 }
285 }
286 else if (CommandState.getInstance().getMappingMode() == KeyParser.MAPPING_CMD_LINE)
287 {
288 if (!CommandGroups.getInstance().getProcess().processExKey(editor, context, key, true))
289 {
290 shouldRecord = false;
291 }
292 }
293 // If we get here then the user has entered an unrecognized series of keystrokes
294 else
295 {
296 mode = STATE_ERROR;
297 }
298 partialReset();
299 }
300 }
301
302 // Do we have a fully entere command at this point? If so, lets execute it
303 if (mode == STATE_READY)
304 {
305 // Let's go through the command stack and merge it all into one command. At this time there should never
306 // be more than two commands on the stack - one is the actual command and the other would be a motion
307 // command argument needed by the first command
308 Command cmd = (Command)currentCmd.pop();
309 while (currentCmd.size() > 0)
310 {
311 Command top = (Command)currentCmd.pop();
312 top.setArgument(new Argument(cmd));
313 cmd = top;
314 }
315
316 // If we have a command and a motion command argument, both could possibly have their own counts. We
317 // need to adjust the counts so the motion gets the product of both counts and the command's count gets
318 // reset. Example 3c2w (change 2 words, three times) becomes c6w (change 6 words)
319 Argument arg = cmd.getArgument();
320 if (arg != null && arg.getType() == Argument.MOTION)
321 {
322 Command mot = arg.getMotion();
323 // If no count was entered for either command then nothing changes. If either had a count then
324 // the motion gets the product of both.
325 int cnt = cmd.getRawCount() == 0 && mot.getRawCount() == 0 ? 0 : cmd.getCount() * mot.getCount();
326 cmd.setCount(0);
327 mot.setCount(cnt);
328 }
329
330 // If we were in "operator pending" mode, reset back to normal mode.
331 if (CommandState.getInstance().getMappingMode() == KeyParser.MAPPING_OP_PEND)
332 {
333 //CommandState.getInstance().setMappingMode(KeyParser.MAPPING_NORMAL);
334 CommandState.getInstance().popState();
335 }
336
337 // Save off the command we are about to execute
338 CommandState.getInstance().setCommand(cmd);
339
340 if (!editor.getDocument().isWritable() && !Command.isReadOnlyType(cmd.getType()))
341 {
342 VimPlugin.indicateError();
343 reset();
344 }
345 else
346 {
347 Runnable action = new ActionRunner(editor, context, cmd, key);
348 if (Command.isReadOnlyType(cmd.getType()))
349 {
350 RunnableHelper.runReadCommand(action);
351 }
352 else
353 {
354 RunnableHelper.runWriteCommand(action);
355 }
356 }
357 }
358 // We had some sort of error so reset the handler and let the user know (beep)
359 else if (mode == STATE_ERROR)
360 {
361 VimPlugin.indicateError();
362 fullReset();
363 }
364 else if (isRecording && shouldRecord)
365 {
366 CommandGroups.getInstance().getRegister().addKeyStroke(key);
367 }
368 }
369
370 /**
371 * Execute an action by name
372 * @param name The name of the action to execute
373 * @param context The context to run it in
374 */
375 public static void executeAction(String name, DataContext context)
376 {
377 logger.debug("executing action " + name);
378 ActionManager aMgr = ActionManager.getInstance();
379 AnAction action = aMgr.getAction(name);
380 if (action != null)
381 {
382 executeAction(action, context);
383 }
384 else
385 {
386 logger.debug("Unknown action");
387 }
388 }
389
390 /**
391 * Execute an action
392 * @param action The action to execute
393 * @param context The context to run it in
394 */
395 public static void executeAction(AnAction action, DataContext context)
396 {
397 logger.debug("executing action " + action);
398
399 // Hopefully all the arguments are sufficient. So far they all seem to work OK.
400 // We don't have a specific InputEvent so that is null
401 // What is "place"? Leave it the empty string for now.
402 // Is the template presentation sufficient?
403 // What are the modifiers? Is zero OK?
404 action.actionPerformed(new AnActionEvent(null, context, "", action.getTemplatePresentation(), 0));
405 }
406
407 /**
408 * Partially resets the state of this handler. Resets the command count, clears the key list, resets the
409 * key tree node to the root for the current mode we are in.
410 */
411 private void partialReset()
412 {
413 count = 0;
414 keys = new ArrayList();
415 currentNode = KeyParser.getInstance().getKeyRoot(CommandState.getInstance().getMappingMode());
416 logger.debug("partialReset");
417 }
418
419 /**
420 * Resets the state of this handler. Does a partial reset then resets the mode, the command, and the argument
421 */
422 public void reset()
423 {
424 partialReset();
425 mode = STATE_NEW_COMMAND;
426 currentCmd.clear();
427 currentArg = Argument.NONE;
428 logger.debug("reset");
429 }
430
431 /**
432 * Completely resets the state of this handler. Resets the command mode to normal, resets, and clears the selected
433 * register.
434 */
435 public void fullReset()
436 {
437 CommandState.getInstance().reset();
438 reset();
439 CommandGroups.getInstance().getRegister().resetRegister();
440 }
441
442 /**
443 * This was used as an experiment to execute actions as a runnable.
444 */
445 static class ActionRunner implements Runnable
446 {
447 public ActionRunner(Editor editor, DataContext context, Command cmd, KeyStroke key)
448 {
449 this.editor = editor;
450 this.context = context;
451 this.cmd = cmd;
452 this.key = key;
453 }
454
455 public void run()
456 {
457 boolean wasRecording = CommandState.getInstance().isRecording();
458
459 executeAction(cmd.getAction(), context);
460 if (CommandState.getInstance().getMode() == CommandState.MODE_INSERT ||
461 CommandState.getInstance().getMode() == CommandState.MODE_REPLACE)
462 {
463 CommandGroups.getInstance().getChange().processCommand(editor, context, cmd);
464 }
465
466 // Now that the command has been executed let's clean up a few things.
467
468 // By default the "empty" register is used by all commands so we want to reset whatever the last register
469 // selected by the user was to the empty register - unless we just executed the "select register" command.
470 if (cmd.getType() != Command.SELECT_REGISTER)
471 {
472 CommandGroups.getInstance().getRegister().resetRegister();
473 }
474
475 // If, at this point, we are not in insert, replace, or visual modes, we need to restore the previous
476 // mode we were in. This handles commands in those modes that temporarily allow us to execute normal
477 // mode commands. An exception is if this command should leave us in the temporary mode such as
478 // "select register"
479 if (CommandState.getInstance().getSubMode() == CommandState.SUBMODE_SINGLE_COMMAND &&
480 (cmd.getFlags() & Command.FLAG_EXPECT_MORE) == 0)
481 {
482 CommandState.getInstance().popState();
483 }
484
485 KeyHandler.getInstance().reset();
486
487 if (wasRecording && CommandState.getInstance().isRecording())
488 {
489 CommandGroups.getInstance().getRegister().addKeyStroke(key);
490 }
491 }
492
493 private Editor editor;
494 private DataContext context;
495 private Command cmd;
496 private KeyStroke key;
497 }
498
499 private int count;
500 private ArrayList keys;
501 private int mode;
502 private ParentNode currentNode;
503 private Stack currentCmd = new Stack();
504 private int currentArg;
505 private TypedActionHandler origHandler;
506
507 private static KeyHandler instance;
508
509 private static final int STATE_NEW_COMMAND = 1;
510 private static final int STATE_COMMAND = 2;
511 private static final int STATE_READY = 3;
512 private static final int STATE_ERROR = 4;
513
514 private static Logger logger = Logger.getInstance(KeyHandler.class.getName());
515 }