Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

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 }