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}