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

Quick Search    Search Deep

Source code: com/maddyhome/idea/vim/group/ChangeGroup.java


1   package com.maddyhome.idea.vim.group;
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.DataConstants;
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.EditorFactory;
29  import com.intellij.openapi.editor.LogicalPosition;
30  import com.intellij.openapi.editor.VisualPosition;
31  import com.intellij.openapi.editor.event.EditorFactoryAdapter;
32  import com.intellij.openapi.editor.event.EditorFactoryEvent;
33  import com.intellij.openapi.editor.event.EditorMouseAdapter;
34  import com.intellij.openapi.editor.event.EditorMouseEvent;
35  import com.intellij.openapi.fileEditor.FileEditorManagerAdapter;
36  import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
37  import com.intellij.openapi.project.Project;
38  import com.intellij.openapi.util.TextRange;
39  import com.maddyhome.idea.vim.KeyHandler;
40  import com.maddyhome.idea.vim.VimPlugin;
41  import com.maddyhome.idea.vim.command.Argument;
42  import com.maddyhome.idea.vim.command.Command;
43  import com.maddyhome.idea.vim.command.CommandState;
44  import com.maddyhome.idea.vim.common.Register;
45  import com.maddyhome.idea.vim.helper.CharacterHelper;
46  import com.maddyhome.idea.vim.helper.EditorData;
47  import com.maddyhome.idea.vim.helper.EditorHelper;
48  import com.maddyhome.idea.vim.helper.SearchHelper;
49  import com.maddyhome.idea.vim.key.KeyParser;
50  import com.maddyhome.idea.vim.undo.UndoManager;
51  import java.awt.event.KeyEvent;
52  import java.util.ArrayList;
53  import javax.swing.KeyStroke;
54  
55  /**
56   * Provides all the insert/replace related functionality
57   * TODO - change cursor for the different modes
58   */
59  public class ChangeGroup extends AbstractActionGroup
60  {
61      /**
62       * Creates the group
63       */
64      public ChangeGroup()
65      {
66          // We want to know when a user clicks the mouse somewhere in the editor so we can clear any
67          // saved text for the current insert mode.
68          EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryAdapter() {
69              public void editorCreated(EditorFactoryEvent event)
70              {
71                  Editor editor = event.getEditor();
72                  editor.addEditorMouseListener(new EditorMouseAdapter() {
73                      public void mouseClicked(EditorMouseEvent event)
74                      {
75                          if (!VimPlugin.isEnabled()) return;
76  
77                          if (CommandState.getInstance().getMode() == CommandState.MODE_INSERT ||
78                              CommandState.getInstance().getMode() == CommandState.MODE_REPLACE)
79                          {
80                              clearStrokes(event.getEditor());
81                          }
82                      }
83                  });
84              }
85  
86         });
87      }
88  
89      /**
90       * Begin insert before the cursor position
91       * @param editor The editor to insert into
92       * @param context The data context
93       */
94      public void insertBeforeCursor(Editor editor, DataContext context)
95      {
96          initInsert(editor, context, CommandState.MODE_INSERT);
97      }
98  
99      /**
100      * Begin insert before the first non-blank on the current line
101      * @param editor The editor to insert into
102      * @param context The data context
103      */
104     public void insertBeforeFirstNonBlank(Editor editor, DataContext context)
105     {
106         MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretToLineStartSkipLeading(editor));
107         initInsert(editor, context, CommandState.MODE_INSERT);
108     }
109 
110     /**
111      * Begin insert before the start of the current line
112      * @param editor The editor to insert into
113      * @param context The data context
114      */
115     public void insertLineStart(Editor editor, DataContext context)
116     {
117         MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretToLineStart(editor));
118         initInsert(editor, context, CommandState.MODE_INSERT);
119     }
120 
121     /**
122      * Begin insert after the cursor position
123      * @param editor The editor to insert into
124      * @param context The data context
125      */
126     public void insertAfterCursor(Editor editor, DataContext context)
127     {
128         MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretHorizontal(editor, 1, true));
129         initInsert(editor, context, CommandState.MODE_INSERT);
130     }
131 
132     /**
133      * Begin insert after the end of the current line
134      * @param editor The editor to insert into
135      * @param context The data context
136      */
137     public void insertAfterLineEnd(Editor editor, DataContext context)
138     {
139         MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretToLineEnd(editor, true));
140         initInsert(editor, context, CommandState.MODE_INSERT);
141     }
142 
143     /**
144      * Begin insert before the current line by creating a new blank line above the current line
145      * @param editor The editor to insert into
146      * @param context The data context
147      */
148     public void insertNewLineAbove(Editor editor, DataContext context)
149     {
150         if (EditorHelper.getCurrentVisualLine(editor) == 0)
151         {
152             MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretToLineStart(editor));
153             initInsert(editor, context, CommandState.MODE_INSERT);
154             KeyHandler.executeAction("VimEditorEnter", context);
155             MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretVertical(editor, -1));
156         }
157         else
158         {
159             MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretVertical(editor, -1));
160             insertNewLineBelow(editor, context);
161         }
162     }
163 
164     /**
165      * Begin insert after the current line by creating a new blank line below the current line
166      * @param editor The editor to insert into
167      * @param context The data context
168      */
169     public void insertNewLineBelow(Editor editor, DataContext context)
170     {
171         MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretToLineEnd(editor, true));
172         initInsert(editor, context, CommandState.MODE_INSERT);
173         KeyHandler.executeAction("VimEditorEnter", context);
174     }
175 
176     /**
177      * Begin insert at the location of the previous insert
178      * @param editor The editor to insert into
179      * @param context The data context
180      */
181     public void insertAtPreviousInsert(Editor editor, DataContext context)
182     {
183         int offset = CommandGroups.getInstance().getMotion().moveCaretToFileMarkLine(editor, context, '^');
184         if (offset != -1)
185         {
186             MotionGroup.moveCaret(editor, context, offset);
187         }
188 
189         insertBeforeCursor(editor, context);
190     }
191 
192     /**
193      * Inserts the previously inserted text
194      * @param editor The editor to insert into
195      * @param context The data context
196      * @param exit true if insert mode should be exited after the insert, false should stay in insert mode
197      */
198     public void insertPreviousInsert(Editor editor, DataContext context, boolean exit)
199     {
200         repeatInsertText(editor, context, 1);
201         if (exit)
202         {
203             processEscape(editor, context);
204         }
205     }
206 
207     /**
208      * Exits insert mode and brings up the help system
209      * @param editor The editor to exit insert mode in
210      * @param context The data context
211      */
212     public void insertHelp(Editor editor, DataContext context)
213     {
214         processEscape(editor, context);
215         KeyHandler.executeAction("HelpTopics", context);
216     }
217 
218     /**
219      * Inserts the contents of the specified register
220      * @param editor The editor to insert the text into
221      * @param context The data context
222      * @param key The register name
223      * @return true if able to insert the register contents, false if not
224      */
225     public boolean insertRegister(Editor editor, DataContext context, char key)
226     {
227         Register register = CommandGroups.getInstance().getRegister().getRegister(key);
228         if (register != null)
229         {
230             String text = register.getText();
231             for (int i = 0; i < text.length(); i++)
232             {
233                 processKey(editor, context, KeyStroke.getKeyStroke(text.charAt(i)));
234             }
235 
236             return true;
237         }
238 
239         return false;
240     }
241 
242     /**
243      * Inserts the character above/below the cursor at the cursor location
244      * @param editor The editor to insert into
245      * @param context The data context
246      * @param dir 1 for getting from line below cursor, -1 for getting from line aboe cursor
247      * @return true if able to get the character and insert it, false if not
248      */
249     public boolean insertCharacterAroundCursor(Editor editor, DataContext context, int dir)
250     {
251         boolean res = false;
252 
253         VisualPosition vp = editor.getCaretModel().getVisualPosition();
254         vp = new VisualPosition(vp.line + dir, vp.column);
255         int len = EditorHelper.getLineLength(editor, EditorHelper.visualLineToLogicalLine(editor, vp.line));
256         if (vp.column < len)
257         {
258             int offset = EditorHelper.visualPostionToOffset(editor, vp);
259             char ch = editor.getDocument().getChars()[offset];
260             processKey(editor, context, KeyStroke.getKeyStroke(ch));
261             res = true;
262         }
263 
264         return res;
265     }
266 
267     /**
268      * If the cursor is currently after the start of the current insert this deletes all the newly inserted text.
269      * Otherwise it deletes all text from the cursor back to the first non-blank in the line.
270      * @param editor The editor to delete the text from
271      * @param context The data context
272      * @return true if able to delete the text, false if not
273      */
274     public boolean insertDeleteInsertedText(Editor editor, DataContext context)
275     {
276         int deleteTo = insertStart;
277         int offset = editor.getCaretModel().getOffset();
278         if (offset == insertStart)
279         {
280             deleteTo = CommandGroups.getInstance().getMotion().moveCaretToLineStartSkipLeading(editor);
281         }
282 
283         if (deleteTo != -1)
284         {
285             deleteRange(editor, context, new TextRange(deleteTo, offset), Command.FLAG_MOT_EXCLUSIVE);
286 
287             return true;
288         }
289 
290         return false;
291     }
292 
293     /**
294      * Deletes the text from the cursor to the start of the previous word
295      * @param editor The editor to delete the text from
296      * @param context The data context
297      * @return true if able to delete text, false if not
298      */
299     public boolean insertDeletePreviousWord(Editor editor, DataContext context)
300     {
301         int deleteTo = insertStart;
302         int offset = editor.getCaretModel().getOffset();
303         if (offset == insertStart)
304         {
305             deleteTo = CommandGroups.getInstance().getMotion().moveCaretToNextWord(editor, -1, false);
306         }
307 
308         if (deleteTo != -1)
309         {
310             deleteRange(editor, context, new TextRange(deleteTo, offset), Command.FLAG_MOT_EXCLUSIVE);
311 
312             return true;
313         }
314 
315         return false;
316     }
317 
318     /**
319      * Begin insert/replace mode
320      * @param editor The editor to insert into
321      * @param context The data context
322      * @param mode The mode - inidicate insert or replace
323      */
324     private void initInsert(Editor editor, DataContext context, int mode)
325     {
326         CommandState state = CommandState.getInstance();
327 
328         insertStart = editor.getCaretModel().getOffset();
329         CommandGroups.getInstance().getMark().setMark(editor, context, '[', insertStart);
330 
331         // If we are repeating the last insert/replace
332         if (state.getMode() == CommandState.MODE_REPEAT)
333         {
334             if (mode == CommandState.MODE_REPLACE)
335             {
336                 processInsert(editor, context);
337             }
338             // If this command doesn't allow repeating, set the count to 1
339             if ((state.getCommand().getFlags() & Command.FLAG_NO_REPEAT) != 0)
340             {
341                 repeatInsert(editor, context, 1);
342             }
343             else
344             {
345                 repeatInsert(editor, context, state.getCommand().getCount());
346             }
347             if (mode == CommandState.MODE_REPLACE)
348             {
349                 processInsert(editor, context);
350             }
351         }
352         // Here we begin insert/replace mode
353         else
354         {
355             lastInsert = state.getCommand();
356             strokes.clear();
357             inInsert = true;
358             if (mode == CommandState.MODE_REPLACE)
359             {
360                 processInsert(editor, context);
361             }
362             state.pushState(mode, 0, KeyParser.MAPPING_INSERT);
363         }
364     }
365 
366     /**
367      * This repeats the previous insert count times
368      * @param editor The editor to insert into
369      * @param context The data context
370      * @param count The number of times to repeat the previous insert
371      */
372     private void repeatInsert(Editor editor, DataContext context, int count)
373     {
374         repeatInsertText(editor, context, count);
375 
376         MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretHorizontal(editor, -1, false));
377     }
378 
379     /**
380      * This repeats the previous insert count times
381      * @param editor The editor to insert into
382      * @param context The data context
383      * @param count The number of times to repeat the previous insert
384      */
385     private void repeatInsertText(Editor editor, DataContext context, int count)
386     {
387         for (int i = 0; i < count; i++)
388         {
389             // Treat other keys special by performing the appropriate action they represent in insert/replace mode
390             for (int k = 0; k < lastStrokes.size(); k++)
391             {
392                 Object obj = lastStrokes.get(k);
393                 if (obj instanceof AnAction)
394                 {
395                     KeyHandler.executeAction((AnAction)obj, context);
396                     strokes.add(obj);
397                 }
398                 else if (obj instanceof Character)
399                 {
400                     processKey(editor, context, KeyStroke.getKeyStroke(((Character)obj).charValue()));
401                 }
402             }
403         }
404     }
405 
406     /**
407      * Terminate insert/replace mode after the user presses Escape or Ctrl-C
408      * @param editor The editor that was being edited
409      * @param context The data context
410      */
411     public void processEscape(Editor editor, DataContext context)
412     {
413         logger.debug("processing escape");
414         int cnt = lastInsert.getCount();
415         // Turn off overwrite mode if we were in replace mode
416         if (CommandState.getInstance().getMode() == CommandState.MODE_REPLACE)
417         {
418             KeyHandler.executeAction("VimInsertReplaceToggle", context);
419         }
420         // If this command doesn't allow repeats, set count to 1
421         if ((lastInsert.getFlags() & Command.FLAG_NO_REPEAT) != 0)
422         {
423             cnt = 1;
424         }
425 
426         // Save off current list of keystrokes
427         lastStrokes = new ArrayList(strokes);
428 
429         // TODO - support . register
430         //CommandGroups.getInstance().getRegister().storeKeys(lastStrokes, Command.FLAG_MOT_CHARACTERWISE, '.');
431 
432         // If the insert/replace command was preceded by a count, repeat again N - 1 times
433         repeatInsert(editor, context, cnt - 1);
434 
435         CommandGroups.getInstance().getMark().setMark(editor, context, '^', editor.getCaretModel().getOffset());
436         CommandGroups.getInstance().getMark().setMark(editor, context, ']', editor.getCaretModel().getOffset());
437         CommandState.getInstance().popState();
438         UndoManager.getInstance().endCommand(editor);
439         UndoManager.getInstance().beginCommand(editor);
440     }
441 
442     /**
443      * Processes the user pressing the Enter key. If this is REPLACE mode we need to turn off OVERWRITE before and
444      * then turn OVERWRITE back on after sending the "Enter" key.
445      * @param editor The editor to press "Enter" in
446      * @param context The data context
447      */
448     public void processEnter(Editor editor, DataContext context)
449     {
450         if (CommandState.getInstance().getMode() == CommandState.MODE_REPLACE)
451         {
452             KeyHandler.executeAction("VimEditorToggleInsertState", context);
453         }
454         KeyHandler.executeAction("VimEditorEnter", context);
455         if (CommandState.getInstance().getMode() == CommandState.MODE_REPLACE)
456         {
457             KeyHandler.executeAction("VimEditorToggleInsertState", context);
458         }
459     }
460 
461     /**
462      * Processes the user pressing the Insert key while in INSERT or REPLACE mode. This simply toggles the
463      * Insert/Overwrite state which updates the status bar.
464      * @param editor The editor to toggle the state in
465      * @param context The data context
466      */
467     public void processInsert(Editor editor, DataContext context)
468     {
469         KeyHandler.executeAction("VimEditorToggleInsertState", context);
470         CommandState.getInstance().toggleInsertOverwrite();
471         inInsert = !inInsert;
472     }
473 
474     /**
475      * While in INSERT or REPLACE mode the user can enter a single NORMAL mode command and then automatically
476      * return to INSERT or REPLACE mode.
477      * @param editor The editor to put into NORMAL mode for one command
478      * @param context The data context
479      */
480     public void processSingleCommand(Editor editor, DataContext context)
481     {
482         CommandState.getInstance().pushState(CommandState.MODE_COMMAND, CommandState.SUBMODE_SINGLE_COMMAND,
483             KeyParser.MAPPING_NORMAL);
484         clearStrokes(editor);
485     }
486 
487     /**
488      * This processes all "regular" keystrokes entered while in insert/replace mode
489      * @param editor The editor the character was typed into
490      * @param context The data context
491      * @param key The user entered keystroke
492      * @return true if this was a regular character, false if not
493      */
494     public boolean processKey(Editor editor, DataContext context, KeyStroke key)
495     {
496         logger.debug("processKey(" + key + ")");
497 
498         if (key.getKeyChar() != KeyEvent.CHAR_UNDEFINED)
499         {
500             // Regular characters are not handled by us, pass them back to Idea. We just keep track of the keystroke
501             // for repeating later.
502             strokes.add(new Character(key.getKeyChar()));
503 
504             KeyHandler.getInstance().getOriginalHandler().execute(editor, key.getKeyChar(), context);
505 
506             return true;
507         }
508 
509         return false;
510     }
511 
512     /**
513      * This processes all keystrokes in Insert/Replace mode that were converted into Commands. Some of these
514      * commands need to be saved off so the inserted/replaced text can be repeated properly later if needed.
515      * @param editor The editor the command was executed in
516      * @param context The data context
517      * @param cmd The command that was executed
518      * @return true if the command was stored for later repeat, false if not
519      */
520     public boolean processCommand(Editor editor, DataContext context, Command cmd)
521     {
522         if ((cmd.getFlags() & Command.FLAG_SAVE_STROKE) != 0)
523         {
524             strokes.add(cmd.getAction());
525 
526             return true;
527         }
528         else if ((cmd.getFlags() & Command.FLAG_CLEAR_STROKES) != 0)
529         {
530             clearStrokes(editor);
531             return false;
532         }
533         else
534         {
535             return false;
536         }
537     }
538 
539     /**
540      * Clears all the keystrokes from the current insert command
541      * @param editor
542      */
543     private void clearStrokes(Editor editor)
544     {
545         strokes.clear();
546         insertStart = editor.getCaretModel().getOffset();
547     }
548 
549     /**
550      * Deletes count characters from the editor
551      * @param editor The editor to remove the characters from
552      * @param context The data context
553      * @param count The number of characters to delete
554      * @return true if able to delete, false if not
555      */
556     public boolean deleteCharacter(Editor editor, DataContext context, int count)
557     {
558         int offset = CommandGroups.getInstance().getMotion().moveCaretHorizontal(editor, count, true);
559         if (offset != -1)
560         {
561             boolean res = deleteText(editor, context, editor.getCaretModel().getOffset(), offset, Command.FLAG_MOT_INCLUSIVE);
562             int pos = editor.getCaretModel().getOffset();
563             int norm = EditorHelper.normalizeOffset(editor, EditorHelper.getCurrentLogicalLine(editor), pos, false);
564             if (norm != pos)
565             {
566                 MotionGroup.moveCaret(editor, context, norm);
567             }
568 
569             return res;
570         }
571 
572         return false;
573     }
574 
575     /**
576      * Deletes count lines including the current line
577      * @param editor The editor to remove the lines from
578      * @param context The data context
579      * @param count The number of lines to delete
580      * @return true if able to delete the lines, false if not
581      */
582     public boolean deleteLine(Editor editor, DataContext context, int count)
583     {
584         int start = CommandGroups.getInstance().getMotion().moveCaretToLineStart(editor);
585         int offset = Math.min(CommandGroups.getInstance().getMotion().moveCaretToLineEndOffset(editor,
586             count - 1, true) + 1, EditorHelper.getFileSize(editor));
587         if (offset != -1)
588         {
589             boolean res = deleteText(editor, context, start, offset, Command.FLAG_MOT_LINEWISE);
590             if (res && editor.getCaretModel().getOffset() >= EditorHelper.getFileSize(editor) &&
591                 editor.getCaretModel().getOffset() != 0)
592             {
593                 MotionGroup.moveCaret(editor, context,
594                     CommandGroups.getInstance().getMotion().moveCaretToLineStartSkipLeadingOffset(editor, -1));
595             }
596 
597             return res;
598         }
599 
600         return false;
601     }
602 
603     /**
604      * Delete from the cursor to the end of count - 1 lines down
605      * @param editor The editor to delete from
606      * @param context The data context
607      * @param count The number of lines affected
608      * @return true if able to delete the text, false if not
609      */
610     public boolean deleteEndOfLine(Editor editor, DataContext context, int count)
611     {
612         int offset = CommandGroups.getInstance().getMotion().moveCaretToLineEndOffset(editor, count - 1, true);
613         if (offset != -1)
614         {
615             boolean res = deleteText(editor, context, editor.getCaretModel().getOffset(), offset, Command.FLAG_MOT_INCLUSIVE);
616             int pos = CommandGroups.getInstance().getMotion().moveCaretHorizontal(editor, -1, false);
617             if (pos != -1)
618             {
619                 MotionGroup.moveCaret(editor, context, pos);
620             }
621 
622             return res;
623         }
624 
625         return false;
626     }
627 
628     /**
629      * Joins count lines togetheri starting at the cursor. No count or a count of one still joins two lines.
630      * @param editor The editor to join the lines in
631      * @param context The data context
632      * @param count The number of lines to join
633      * @param spaces If true the joined lines will have one space between them and any leading space on the second line
634      *        will be removed. If false, only the newline is removed to join the lines.
635      * @return true if able to join the lines, false if not
636      */
637     public boolean deleteJoinLines(Editor editor, DataContext context, int count, boolean spaces)
638     {
639         if (count < 2) count = 2;
640         int lline = EditorHelper.getCurrentLogicalLine(editor);
641         int total = EditorHelper.getLineCount(editor);
642         if (lline + count > total)
643         {
644             return false;
645         }
646 
647         return deleteJoinNLines(editor, context, lline, count, spaces);
648     }
649 
650     /**
651      * Joins all the lines selected by the current visual selection.
652      * @param editor The editor to join the lines in
653      * @param context The data context
654      * @param range The range of the visual selection
655      * @param spaces If true the joined lines will have one space between them and any leading space on the second line
656      *        will be removed. If false, only the newline is removed to join the lines.
657      * @return true if able to join the lines, false if not
658      */
659     public boolean deleteJoinRange(Editor editor, DataContext context, TextRange range, boolean spaces)
660     {
661         int startLine = editor.offsetToLogicalPosition(range.getStartOffset()).line;
662         int endLine = editor.offsetToLogicalPosition(range.getEndOffset()).line;
663         int count = endLine - startLine + 1;
664         if (count < 2) count = 2;
665 
666         return deleteJoinNLines(editor, context, startLine, count, spaces);
667     }
668 
669     /**
670      * This does the actual joining of the lines
671      * @param editor The editor to join the lines in
672      * @param context The data context
673      * @param startLine The starting logical line
674      * @param count The number of lines to join including startLine
675      * @param spaces If true the joined lines will have one space between them and any leading space on the second line
676      *        will be removed. If false, only the newline is removed to join the lines.
677      * @return true if able to join the lines, false if not
678      */
679     private boolean deleteJoinNLines(Editor editor, DataContext context, int startLine, int count, boolean spaces)
680     {
681         // start my moving the cursor to the very end of the first line
682         MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretToLineEnd(editor, startLine, true));
683         for (int i = 1; i < count; i++)
684         {
685             int start = CommandGroups.getInstance().getMotion().moveCaretToLineEnd(editor, true);
686             MotionGroup.moveCaret(editor, context, start);
687             int offset;
688             if (spaces)
689             {
690                 offset = CommandGroups.getInstance().getMotion().moveCaretToLineStartSkipLeadingOffset(editor, 1);
691             }
692             else
693             {
694                 offset = CommandGroups.getInstance().getMotion().moveCaretToLineStartOffset(editor, 1);
695             }
696             deleteText(editor, context, editor.getCaretModel().getOffset(), offset, Command.FLAG_MOT_INCLUSIVE);
697             if (spaces)
698             {
699                 insertText(editor, context, start, " ");
700                 MotionGroup.moveCaret(editor, context, CommandGroups.getInstance().getMotion().moveCaretHorizontal(editor, -1, false));
701             }
702         }
703 
704         return true;
705     }
706 
707     /**
708      * Delete all text moved over by the supplied motion command argument.
709      * @param editor The editor to delete the text from
710      * @param context The data context
711      * @param count The number of times to repear the deletion
712      * @param rawCount The actual count entered by the user
713      * @param argument The motion command
714      * @return true if able to delete the text, false if not
715      */
716     public boolean deleteMotion(Editor editor, DataContext context, int count, int rawCount, Argument argument, boolean isChange)
717     {
718         TextRange range = MotionGroup.getMotionRange(editor, context, count, rawCount, argument, true, false);
719         if (range == null && EditorHelper.getFileSize(editor) == 0)
720         {
721             return true;
722         }
723 
724         // Delete motion commands that are not linewise become linewise if all the following are true:
725         // 1) The range is across multiple lines
726         // 2) There is only whitespace before the start of the range
727         // 3) There is only whitespace after the end of the range
728         if (!isChange && (argument.getMotion().getFlags() & Command.FLAG_MOT_LINEWISE) == 0)
729         {
730             LogicalPosition start = editor.offsetToLogicalPosition(range.getStartOffset());
731             LogicalPosition end = editor.offsetToLogicalPosition(range.getEndOffset());
732             if (start.line != end.line)
733             {
734                 if (!SearchHelper.anyNonWhitespace(editor, range.getStartOffset(), -1) &&
735                     !SearchHelper.anyNonWhitespace(editor, range.getEndOffset(), 1))
736                 {
737                     int flags = argument.getMotion().getFlags();
738                     flags &= ~Command.FLAG_MOT_EXCLUSIVE;
739                     flags &= ~Command.FLAG_MOT_INCLUSIVE;
740                     flags |= Command.FLAG_MOT_LINEWISE;
741                     argument.getMotion().setFlags(flags);
742                 }
743             }
744         }
745         return deleteRange(editor, context, range, argument.getMotion().getFlags());
746     }
747 
748     /**
749      * Delete the range of text.
750      * @param editor The editor to delete the text from
751      * @param context The data context
752      * @param range The range to delete
753      * @param type The type of deletion (FLAG_MOT_LINEWISE, FLAG_MOT_EXCLUSIVE, or FLAG_MOT_INCLUSIVE)
754      * @return true if able to delete the text, false if not
755      */
756     public boolean deleteRange(Editor editor, DataContext context, TextRange range, int type)
757     {
758         if (range == null)
759         {
760             return false;
761         }
762         else
763         {
764             boolean res = deleteText(editor, context, range.getStartOffset(), range.getEndOffset(), type);
765             if (res && editor.getCaretModel().getOffset() >= EditorHelper.getFileSize(editor) &&
766                 editor.getCaretModel().getOffset() != 0)
767             {
768                 MotionGroup.moveCaret(editor, context,
769                     CommandGroups.getInstance().getMotion().moveCaretToLineStartSkipLeadingOffset(editor, -1));
770             }
771 
772             return res;
773         }
774     }
775 
776     /**
777      * Begin Replace mode
778      * @param editor The editor to replace in
779      * @param context The data context
780      * @return true
781      */
782     public boolean changeReplace(Editor editor, DataContext context)
783     {
784         initInsert(editor, context, CommandState.MODE_REPLACE);
785 
786         return true;
787     }
788 
789     /**
790      * Replace each of the next count characters with the charcter ch
791      * @param editor The editor to chage
792      * @param context The data context
793      * @param count The number of characters to change
794      * @param ch The character to change to
795      * @return true if able to change count characters, false if not
796      */
797     public boolean changeCharacter(Editor editor, DataContext context, int count, char ch)
798     {
799         int col = EditorHelper.getCurrentLogicalColumn(editor);
800         int len = EditorHelper.getLineLength(editor);
801         int offset = editor.getCaretModel().getOffset();
802         if (len - col < count)
803         {
804             return false;
805         }
806 
807         StringBuffer repl = new StringBuffer(count);
808         for (int i = 0; i < count; i++)
809         {
810             repl.append(ch);
811         }
812 
813         replaceText(editor, context, offset, offset + count, repl.toString());
814 
815         return true;
816     }
817 
818     /**
819      * Each character in the supplied range gets replaced with the character ch
820      * @param editor The editor to change
821      * @param context The data context
822      * @param range The range to change
823      * @param ch The replacing character
824      * @return true if able to change the range, false if not
825      */
826     public boolean changeCharacterRange(Editor editor, DataContext context, TextRange range, char ch)
827     {
828         char[] chars = editor.getDocument().getChars();
829         for (int i = range.getStartOffset(); i < range.getEndOffset(); i++)
830         {
831             if (i < chars.length && '\n' != chars[i])
832             {
833                 replaceText(editor, context, i, i + 1, Character.toString(ch));
834             }
835         }
836         return true;
837     }
838 
839     /**
840      * Delete count characters and then enter insert mode
841      * @param editor The editor to change
842      * @param context The data context
843      * @param count The number of characters to change
844      * @return true if able to delete count characters, false if not
845      */
846     public boolean changeCharacters(Editor editor, DataContext context, int count)
847     {
848         boolean res = deleteCharacter(editor, context, count);
849         if (res)
850         {
851             initInsert(editor, context, CommandState.MODE_INSERT);
852         }
853 
854         return res;
855     }
856 
857     /**
858      * Delete count lines and then enter insert mode
859      * @param editor The editor to change
860      * @param context The data context
861      * @param count The number of lines to change
862      * @return true if able to delete count lines, false if not
863      */
864     public boolean changeLine(Editor editor, DataContext context, int count)
865     {
866         boolean res = deleteLine(editor, context, count);
867         if (res)
868         {
869             insertNewLineAbove(editor, context);
870         }
871 
872         return res;
873     }
874 
875     /**
876      * Delete from the cursor to the end of count - 1 lines down and enter insert mode
877      * @param editor The editor to change
878      * @param context The data context
879      * @param count The number of lines to change
880      * @return true if able to delete count lines, false if not
881      */
882     public boolean changeEndOfLine(Editor editor, DataContext context, int count)
883     {
884         boolean res = deleteEndOfLine(editor, context, count);
885         if (res)
886         {
887             insertAfterLineEnd(editor, context);
888         }
889 
890         return res;
891     }
892 
893     /**
894      * Delete the text covered by the motion command argument and enter insert mode
895      * @param editor The editor to change
896      * @param context The data context
897      * @param count The number of time to repeat the change
898      * @param rawCount The actual count entered by the user
899      * @param argument The motion command
900      * @return true if able to delete the text, false if not
901      */
902     public boolean changeMotion(Editor editor, DataContext context, int count, int rawCount, Argument argument)
903     {
904         // TODO: Hack - find better way to do this exceptional case - at least make constants out of these strings
905 
906         // Vim treats cw as ce and cW as cE if cursor is on a non-blank character
907         String id = ActionManager.getInstance().getId(argument.getMotion().getAction());
908         if (id.equals("VimMotionWordRight"))
909         {
910             if (EditorHelper.getFileSize(editor) > 0 &&
911                 !Character.isWhitespace(editor.getDocument().getChars()[editor.getCaretModel().getOffset()]))
912             {
913                 argument.getMotion().setAction(ActionManager.getInstance().getAction("VimMotionWordEndRight"));
914                 argument.getMotion().setFlags(Command.FLAG_MOT_INCLUSIVE);
915             }
916         }
917         else if (id.equals("VimMotionWORDRight"))
918         {
919             if (EditorHelper.getFileSize(editor) > 0 &&
920                 !Character.isWhitespace(editor.getDocument().getChars()[editor.getCaretModel().getOffset()]))
921             {
922                 argument.getMotion().setAction(ActionManager.getInstance().getAction("VimMotionWORDEndRight"));
923                 argument.getMotion().setFlags(Command.FLAG_MOT_INCLUSIVE);
924             }
925         }
926         else if (id.equals("VimMotionCamelRight"))
927         {
928             if (EditorHelper.getFileSize(editor) > 0 &&
929                 !Character.isWhitespace(editor.getDocument().getChars()[editor.getCaretModel().getOffset()]))
930             {
931                 argument.getMotion().setAction(ActionManager.getInstance().getAction("VimMotionCamelEndRight"));
932                 argument.getMotion().setFlags(Command.FLAG_MOT_INCLUSIVE);
933             }
934         }
935 
936         boolean res = deleteMotion(editor, context, count, rawCount, argument, true);
937         if (res)
938         {
939             insertBeforeCursor(editor, context);
940         }
941 
942         return res;
943     }
944 
945     /**
946      * Deletes the range of text and enters insert mode
947      * @param editor The editor to change
948      * @param context The data context
949      * @param range The range to change
950      * @param type The type of the range (FLAG_MOT_LINEWISE, FLAG_MOT_CHARACTERWISE)
951      * @return true if able to delete the range, false if not
952      */
953     public boolean changeRange(Editor editor, DataContext context, TextRange range, int type)
954     {
955         boolean after = range.getEndOffset() >= EditorHelper.getFileSize(editor);
956         boolean res = deleteRange(editor, context, range, type);
957         if (res)
958         {
959             if (type == Command.FLAG_MOT_LINEWISE)
960             {
961                 if (after)
962                 {
963                     insertNewLineBelow(editor, context);
964                 }
965                 else
966                 {
967                     insertNewLineAbove(editor, context);
968                 }
969             }
970             else
971             {
972                 insertBeforeCursor(editor, context);
973             }
974         }
975 
976         return res;
977     }
978 
979     /**
980      * Toggles the case of count characters
981      * @param editor The editor to change
982      * @param context The data context
983      * @param count The number of characters to change
984      * @return true if able to change count characters
985      */
986     public boolean changeCaseToggleCharacter(Editor editor, DataContext context, int count)
987     {
988         int offset = CommandGroups.getInstance().getMotion().moveCaretHorizontal(editor, count, true);
989         if (offset == -1)
990         {
991             return false;
992         }
993         else
994         {
995             changeCase(editor, context, editor.getCaretModel().getOffset(), offset, CharacterHelper.CASE_TOGGLE);
996 
997             offset = EditorHelper.normalizeOffset(editor, offset, false);
998             MotionGroup.moveCaret(editor, context, offset);
999 
1000            return true;
1001        }
1002    }
1003
1004    /**
1005     * Changes the case of all the character moved over by the motion argument.
1006     * @param editor The editor to change
1007     * @param context The data context
1008     * @param count The number of times to repeat the change
1009     * @param rawCount The actual count entered by the user
1010     * @param type The case change type (TOGGLE, UPPER, LOWER)
1011     * @param argument The motion command
1012     * @return true if able to delete the text, false if not
1013     */
1014    public boolean changeCaseMotion(Editor editor, DataContext context, int count, int rawCount, char type, Argument argument)
1015    {
1016        TextRange range = MotionGroup.getMotionRange(editor, context, count, rawCount, argument, true, false);
1017
1018        return changeCaseRange(editor, context, range, type);
1019    }
1020
1021    /**
1022     * Changes the case of all the characters in the range
1023     * @param editor The editor to change
1024     * @param context The data context
1025     * @param range The range to change
1026     * @param type The case change type (TOGGLE, UPPER, LOWER)
1027     * @return true if able to delete the text, false if not
1028     */
1029    public boolean changeCaseRange(Editor editor, DataContext context, TextRange range, char type)
1030    {
1031        if (range == null)
1032        {
1033            return false;
1034        }
1035        else
1036        {
1037            changeCase(editor, context, range.getStartOffset(), range.getEndOffset(), type);
1038            MotionGroup.moveCaret(editor, context, range.getStartOffset());
1039
1040            return true;
1041        }
1042    }
1043
1044    /**
1045     * This performs the actual case change.
1046     * @param editor The editor to change
1047     * @param context The data context
1048     * @param start The start offset to change
1049     * @param end The end offset to change
1050     * @param type The type of change (TOGGLE, UPPER, LOWER)
1051     */
1052    private void changeCase(Editor editor, DataContext context, int start, int end, char type)
1053    {
1054        if (start > end)
1055        {
1056            int t = end;
1057            end = start;
1058            start = t;
1059        }
1060
1061        char[] chars = editor.getDocument().getChars();
1062        for (int i = start; i < end; i++)
1063        {
1064            if (i >= chars.length)
1065            {
1066                break;
1067            }
1068
1069            char ch = CharacterHelper.changeCase(chars[i], type);
1070            if (ch != chars[i])
1071            {
1072                replaceText(editor, context, i, i + 1, Character.toString(ch));
1073            }
1074        }
1075    }
1076
1077    public void autoIndentLines(Editor editor, DataContext context, int lines)
1078    {
1079        KeyHandler.executeAction("AutoIndentLines", context);
1080    }
1081
1082    public void indentLines(Editor editor, DataContext context, int lines, int dir)
1083    {
1084        int cnt = 1;
1085        if (CommandState.getInstance().getMode() == CommandState.MODE_INSERT ||
1086            CommandState.getInstance().getMode() == CommandState.MODE_REPLACE)
1087        {
1088            if (strokes.size() > 0)
1089            {
1090                Object stroke = strokes.get(strokes.size() - 1);
1091                if (stroke instanceof Character)
1092                {
1093                    Character key = (Character)stroke;
1094                    if (key.charValue() == '0')
1095                    {
1096                        deleteCharacter(editor, context, -1);
1097                        cnt = 99;
1098                    }
1099                }
1100            }
1101        }
1102
1103        int start = editor.getCaretModel().getOffset();
1104        int end = CommandGroups.getInstance().getMotion().moveCaretToLineEndOffset(editor, lines - 1, false);
1105
1106        indentRange(editor, context, new TextRange(start, end), cnt, dir);
1107    }
1108
1109    public void indentMotion(Editor editor, DataContext context, int count, int rawCount, Argument argument, int dir)
1110    {
1111        TextRange range = MotionGroup.getMotionRange(editor, context, count, rawCount, argument, false, false);
1112
1113        indentRange(editor, context, range, 1, dir);
1114    }
1115
1116    public void indentRange(Editor editor, DataContext context, TextRange range, int count, int dir)
1117    {
1118        if (range == null) return;
1119
1120        Project proj = (Project)context.getData(DataConstants.PROJECT);
1121        int tabSize = editor.getSettings().getTabSize(proj);
1122        boolean useTabs = editor.getSettings().isUseTabCharacter(proj);
1123
1124        int sline = editor.offsetToLogicalPosition(range.getStartOffset()).line;
1125        int eline = editor.offsetToLogicalPosition(range.getEndOffset()).line;
1126        int eoff = EditorHelper.getLineStartForOffset(editor, range.getEndOffset());
1127        if (eoff == range.getEndOffset())
1128        {
1129            eline--;
1130        }
1131
1132        for (int l = sline; l <= eline; l++)
1133        {
1134            int soff = EditorHelper.getLineStartOffset(editor, l);
1135            int woff = CommandGroups.getInstance().getMotion().moveCaretToLineStartSkipLeading(editor, l);
1136            int col = editor.offsetToVisualPosition(woff).column;
1137            int newCol = Math.max(0, col + dir * tabSize * count);
1138            if (dir == 1 || col > 0)
1139            {
1140                StringBuffer space = new StringBuffer();
1141                int tabCnt = 0;
1142                int spcCnt = 0;
1143                if (useTabs)
1144                {
1145                    tabCnt = newCol / tabSize;
1146                    spcCnt = newCol % tabSize;
1147                }
1148                else
1149                {
1150                    spcCnt = newCol;
1151                }
1152
1153                for (int i = 0; i < tabCnt; i++)
1154                {
1155                    space.append('\t');
1156                }
1157                for (int i = 0; i < spcCnt; i++)
1158                {
1159                    space.append(' ');
1160                }
1161
1162                replaceText(editor, context, soff, woff, space.toString());
1163            }
1164        }
1165
1166        if (CommandState.getInstance().getMode() != CommandState.MODE_INSERT &&
1167            CommandState.getInstance().getMode() != CommandState.MODE_REPLACE)
1168        {
1169            MotionGroup.moveCaret(editor, context,
1170                CommandGroups.getInstance().getMotion().moveCaretToLineStartSkipLeading(editor, sline));
1171        }
1172
1173        EditorData.setLastColumn(editor, editor.getCaretModel().getVisualPosition().column);
1174    }
1175
1176    /**
1177     * Insert text into the document
1178     * @param editor The editor to insert into
1179     * @param context The data context
1180     * @param start The starting offset to insert at
1181     * @param str The text to insert
1182     */
1183    public void insertText(Editor editor, DataContext context, int start, String str)
1184    {
1185        editor.getDocument().insertString(start, str);
1186        editor.getCaretModel().moveToOffset(start + str.length());
1187
1188        CommandGroups.getInstance().getMark().setMark(editor, context, '.', start);
1189        //CommandGroups.getInstance().getMark().setMark(editor, context, '[', start);
1190        //CommandGroups.getInstance().getMark().setMark(editor, context, ']', start + str.length());
1191    }
1192
1193    /**
1194     * Delete text from the document. This will fail if being asked to store the deleted text into a read-only
1195     * register.
1196     * @param editor The editor to delete from
1197     * @param context The data context
1198     * @param start The start offset to delete
1199     * @param end The end offset to delete
1200     * @param type The type of deletion (FLAG_MOT_LINEWISE, FLAG_MOT_CHARACTERWISE)
1201     * @return true if able to delete the text, false if not
1202     */
1203    private boolean deleteText(Editor editor, DataContext context, int start, int end, int type)
1204    {
1205        if (start > end)
1206        {
1207            int t = start;
1208            start = end;
1209            end = t;
1210        }
1211
1212        start = Math.max(0, Math.min(start, EditorHelper.getFileSize(editor)));
1213        end = Math.max(0, Math.min(end, EditorHelper.getFileSize(editor)));
1214
1215        if (CommandGroups.getInstance().getRegister().storeText(editor, context, start, end, type, true, false))
1216        {
1217            editor.getDocument().deleteString(start, end);
1218
1219            CommandGroups.getInstance().getMark().setMark(editor, context, '.', start);
1220            CommandGroups.getInstance().getMark().setMark(editor, context, '[', start);
1221            CommandGroups.getInstance().getMark().setMark(editor, context, ']', start);
1222
1223            return true;
1224        }
1225
1226        return false;
1227    }
1228
1229    /**
1230     * Replace text in the editor
1231     * @param editor The editor to replace text in
1232     * @param context The data context
1233     * @param start The start offset to change
1234     * @param end The end offset to change
1235     * @param str The new text
1236     */
1237    private void replaceText(Editor editor, DataContext context, int start, int end, String str)
1238    {
1239        editor.getDocument().replaceString(start, end, str);
1240
1241        CommandGroups.getInstance().getMark().setMark(editor, context, '[', start);
1242        CommandGroups.getInstance().getMark().setMark(editor, context, ']', start + str.length());
1243        CommandGroups.getInstance().getMark().setMark(editor, context, '.', start + str.length());
1244    }
1245
1246    /**
1247     * This class listens for editor tab changes so any insert/replace modes that need to be reset can be
1248     */
1249    public static class InsertCheck extends FileEditorManagerAdapter
1250    {
1251        /**
1252         * The user has changed the editor they are working with - exit insert/replace mode, and complete any
1253         * appropriate repeat.
1254         * @param event
1255         */
1256        public void selectionChanged(FileEditorManagerEvent event)
1257        {
1258            if (!VimPlugin.isEnabled()) return;
1259
1260            logger.debug("selected file changed");
1261
1262            CommandState.getInstance().reset();
1263            KeyHandler.getInstance().fullReset();
1264        }
1265    }
1266
1267    private ArrayList strokes = new ArrayList();
1268    private ArrayList lastStrokes;
1269    private int insertStart;
1270    private Command lastInsert;
1271    private boolean inInsert;
1272
1273    private static Logger logger = Logger.getInstance(ChangeGroup.class.getName());
1274}