001    /*
002     * Copyright (C) 2012 eXo Platform SAS.
003     *
004     * This is free software; you can redistribute it and/or modify it
005     * under the terms of the GNU Lesser General Public License as
006     * published by the Free Software Foundation; either version 2.1 of
007     * the License, or (at your option) any later version.
008     *
009     * This software is distributed in the hope that it will be useful,
010     * but WITHOUT ANY WARRANTY; without even the implied warranty of
011     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012     * Lesser General Public License for more details.
013     *
014     * You should have received a copy of the GNU Lesser General Public
015     * License along with this software; if not, write to the Free
016     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
017     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
018     */
019    
020    package org.crsh.console;
021    
022    import org.crsh.cli.impl.Delimiter;
023    import org.crsh.cli.impl.completion.CompletionMatch;
024    import org.crsh.cli.impl.line.LineParser;
025    import org.crsh.cli.impl.line.MultiLineVisitor;
026    import org.crsh.cli.spi.Completion;
027    import org.crsh.util.Utils;
028    
029    import java.io.IOException;
030    import java.util.Iterator;
031    import java.util.List;
032    import java.util.Map;
033    
034    /**
035     * An action on the editor.
036     */
037    class EditorAction {
038    
039      static class InsertKey extends EditorAction {
040    
041        private final int[] sequence;
042    
043        public InsertKey(int[] sequence) {
044          this.sequence = sequence;
045        }
046    
047        void perform(Editor editor, EditorBuffer buffer) throws IOException {
048          for (int c : sequence) {
049            buffer.append((char)c);
050          }
051        }
052      }
053    
054      static EditorAction COMPLETE = new EditorAction() {
055        @Override
056        String execute(Editor editor, EditorBuffer buffer, int[] sequence, boolean flush) throws IOException {
057    
058          // Compute prefix
059          MultiLineVisitor visitor = new MultiLineVisitor();
060          LineParser parser = new LineParser(visitor);
061          List<String> lines = buffer.getLines();
062          for (int i = 0;i < lines.size();i++) {
063            if (i > 0) {
064              parser.crlf();
065            }
066            parser.append(lines.get(i));
067          }
068          String prefix = visitor.getRaw();
069    
070          // log.log(Level.FINE, "About to get completions for " + prefix);
071          CompletionMatch completion = editor.console.shell.complete(prefix);
072          // log.log(Level.FINE, "Completions for " + prefix + " are " + completions);
073    
074          //
075          if (completion != null) {
076            Completion completions = completion.getValue();
077    
078            //
079            Delimiter delimiter = completion.getDelimiter();
080    
081            try {
082              // Try to find the greatest prefix among all the results
083              if (completions.getSize() == 0) {
084                // Do nothing
085              } else if (completions.getSize() == 1) {
086                Map.Entry<String, Boolean> entry = completions.iterator().next();
087                String insert = entry.getKey();
088                StringBuilder sb = new StringBuilder();
089                sb.append(delimiter.escape(insert));
090                if (entry.getValue()) {
091                  sb.append(completion.getDelimiter().getValue());
092                }
093                buffer.append(sb);
094                editor.console.driver.flush();
095              } else {
096                String commonCompletion = Utils.findLongestCommonPrefix(completions.getValues());
097    
098                // Format stuff
099                int width = editor.console.driver.getWidth();
100    
101                //
102                String completionPrefix = completions.getPrefix();
103    
104                // Get the max length
105                int max = 0;
106                for (String suffix : completions.getValues()) {
107                  max = Math.max(max, completionPrefix.length() + suffix.length());
108                }
109    
110                // Separator : use two whitespace like in BASH
111                max += 2;
112    
113                //
114                StringBuilder sb = new StringBuilder().append('\n');
115                if (max < width) {
116                  int columns = width / max;
117                  int index = 0;
118                  for (String suffix : completions.getValues()) {
119                    sb.append(completionPrefix).append(suffix);
120                    for (int l = completionPrefix.length() + suffix.length();l < max;l++) {
121                      sb.append(' ');
122                    }
123                    if (++index >= columns) {
124                      index = 0;
125                      sb.append('\n');
126                    }
127                  }
128                  if (index > 0) {
129                    sb.append('\n');
130                  }
131                } else {
132                  for (Iterator<String> i = completions.getValues().iterator();i.hasNext();) {
133                    String suffix = i.next();
134                    sb.append(commonCompletion).append(suffix);
135                    if (i.hasNext()) {
136                      sb.append('\n');
137                    }
138                  }
139                  sb.append('\n');
140                }
141    
142                // Add current buffer
143                int index = 0;
144                for (String line : lines) {
145                  if (index == 0) {
146                    String prompt = editor.console.shell.getPrompt();
147                    sb.append(prompt == null ? "" : prompt);
148                  } else {
149                    sb.append("\n> ");
150                  }
151                  sb.append(line);
152                  index++;
153                }
154    
155                // Redraw everything
156                editor.console.driver.write(sb.toString());
157    
158                // If we have common completion we append it now in the buffer
159                if (commonCompletion.length() > 0) {
160                  buffer.append(delimiter.escape(commonCompletion));
161                }
162    
163                // Flush
164                buffer.flush(true);
165              }
166            }
167            catch (IOException e) {
168              // log.log(Level.SEVERE, "Could not write completion", e);
169            }
170          }
171    
172          //
173          return null;
174        }
175      };
176    
177      static EditorAction INTERRUPT = new EditorAction() {
178        @Override
179        String execute(Editor editor, EditorBuffer buffer, int[] sequence, boolean flush) throws IOException {
180          editor.lineParser.reset();
181          buffer.reset();
182          editor.console.driver.writeCRLF();
183          String prompt = editor.console.shell.getPrompt();
184          if (prompt != null) {
185            editor.console.driver.write(prompt);
186          }
187          if (flush) {
188            editor.console.driver.flush();
189          }
190          return null;
191        }
192      };
193    
194      static EditorAction EOF_MAYBE = new EditorAction() {
195        @Override
196        String execute(Editor editor, EditorBuffer buffer, int[] sequence, boolean flush) throws IOException {
197          if (editor.isEmpty()) {
198            editor.console.running = false;
199            return null;
200          } else {
201            if (editor.console.getMode() == Mode.EMACS) {
202              return EditorAction.DELETE_PREV_CHAR.execute(editor, buffer, sequence, true);
203            } else {
204              return EditorAction.ENTER.execute(editor, buffer, sequence, true);
205            }
206          }
207        }
208      };
209    
210      public abstract static class History extends EditorAction {
211    
212        protected abstract int getNext(Editor editor);
213    
214        @Override
215        void perform(Editor editor, EditorBuffer buffer) throws IOException {
216          int nextHistoryCursor = getNext(editor);
217          if (nextHistoryCursor >= -1 && nextHistoryCursor < editor.history.size()) {
218            String s = nextHistoryCursor == -1 ? editor.historyBuffer : editor.history.get(nextHistoryCursor);
219            while (buffer.moveRight()) {
220              // Do nothing
221            }
222            String t = buffer.replace(s);
223            if (editor.historyCursor == -1) {
224              editor.historyBuffer = t;
225            } else {
226              editor.history.set(editor.historyCursor, t);
227            }
228            editor.historyCursor = nextHistoryCursor;
229          }
230        }
231      }
232    
233      static EditorAction HISTORY_FIRST = new History() {
234        @Override
235        protected int getNext(Editor editor) {
236          return editor.history.size() - 1;
237        }
238      };
239    
240      static EditorAction HISTORY_LAST = new History() {
241        @Override
242        protected int getNext(Editor editor) {
243          return 0;
244        }
245      };
246    
247      static EditorAction HISTORY_PREV = new History() {
248        @Override
249        protected int getNext(Editor editor) {
250          return editor.historyCursor + 1;
251        }
252      };
253    
254      static EditorAction HISTORY_NEXT = new History() {
255        @Override
256        protected int getNext(Editor editor) {
257          return editor.historyCursor - 1;
258        }
259      };
260    
261      static EditorAction LEFT = new EditorAction() {
262        @Override
263        void perform(Editor editor, EditorBuffer buffer) throws IOException {
264          buffer.moveLeft();
265        }
266      };
267    
268      static EditorAction RIGHT = new EditorAction() {
269        @Override
270        void perform(Editor editor, EditorBuffer buffer) throws IOException {
271          if (buffer.getCursor() < editor.getCursorBound()) {
272            buffer.moveRight();
273          }
274        }
275      };
276    
277      static EditorAction MOVE_BEGINNING = new EditorAction() {
278        @Override
279        void perform(Editor editor, EditorBuffer buffer) throws IOException {
280          int cursor = buffer.getCursor();
281          if (cursor > 0) {
282            buffer.moveLeftBy(cursor);
283          }
284        }
285      };
286    
287      static class MovePrevWord extends EditorAction {
288    
289        final boolean atBeginning /* otherwise at end */;
290    
291        public MovePrevWord(boolean atBeginning) {
292          this.atBeginning = atBeginning;
293        }
294    
295        @Override
296        void perform(Editor editor, EditorBuffer buffer) throws IOException {
297          int cursor = buffer.getCursor();
298          int pos = cursor;
299          while (pos > 0) {
300            char c = buffer.charAt(pos - 1);
301            if ((atBeginning && Character.isLetterOrDigit(c)) || (!atBeginning && !Character.isLetterOrDigit(c))) {
302              break;
303            } else {
304              pos--;
305            }
306          }
307          while (pos > 0) {
308            char c = buffer.charAt(pos - 1);
309            if ((atBeginning && !Character.isLetterOrDigit(c)) || (!atBeginning && Character.isLetterOrDigit(c))) {
310              break;
311            } else {
312              pos--;
313            }
314          }
315          if (pos < cursor) {
316            buffer.moveLeftBy(cursor - pos);
317          }
318        }
319      }
320    
321      static EditorAction MOVE_PREV_WORD_AT_BEGINNING = new MovePrevWord(true);
322    
323      static EditorAction MOVE_PREV_WORD_AT_END = new MovePrevWord(false);
324    
325      static class MoveNextWord extends EditorAction {
326    
327        final At at;
328    
329        public MoveNextWord(At at) {
330          this.at = at;
331        }
332    
333        @Override
334        void perform(Editor editor, EditorBuffer buffer) throws IOException {
335          int to = editor.getCursorBound();
336          int from = buffer.getCursor();
337          int pos = from;
338          while (true) {
339            int look = at == At.BEFORE_END ? pos + 1 : pos;
340            if (look < to) {
341              char c = buffer.charAt(look);
342              if ((at != At.BEGINNING && Character.isLetterOrDigit(c)) || (at == At.BEGINNING && !Character.isLetterOrDigit(c))) {
343                break;
344              } else {
345                pos++;
346              }
347            } else {
348              break;
349            }
350          }
351          while (true) {
352            int look = at == At.BEFORE_END ? pos + 1 : pos;
353            if (look < to) {
354              char c = buffer.charAt(look);
355              if ((at != At.BEGINNING && !Character.isLetterOrDigit(c)) || (at == At.BEGINNING && Character.isLetterOrDigit(c))) {
356                break;
357              } else {
358                pos++;
359              }
360            } else {
361              break;
362            }
363          }
364          if (pos > from) {
365            buffer.moveRightBy(pos - from);
366          }
367        }
368      }
369    
370      static EditorAction MOVE_NEXT_WORD_AT_BEGINNING = new MoveNextWord(At.BEGINNING);
371    
372      static EditorAction MOVE_NEXT_WORD_AFTER_END = new MoveNextWord(At.AFTER_END);
373    
374      static EditorAction MOVE_NEXT_WORD_BEFORE_END = new MoveNextWord(At.BEFORE_END);
375    
376      static EditorAction DELETE_PREV_WORD = new EditorAction() {
377        @Override
378        void perform(Editor editor, EditorBuffer buffer) throws IOException {
379          editor.killBuffer.setLength(0);
380          boolean chars = false;
381          while (true) {
382            int cursor = buffer.getCursor();
383            if (cursor > 0) {
384              if (buffer.charAt(cursor - 1) == ' ') {
385                if (!chars) {
386                  editor.killBuffer.appendCodePoint(buffer.del());
387                } else {
388                  break;
389                }
390              } else {
391                editor.killBuffer.appendCodePoint(buffer.del());
392                chars = true;
393              }
394            } else {
395              break;
396            }
397          }
398          editor.killBuffer.reverse();
399        }
400      };
401    
402      static EditorAction DELETE_NEXT_WORD = new EditorAction() {
403        @Override
404        void perform(Editor editor, EditorBuffer buffer) throws IOException {
405          int count = 0;
406          boolean chars = false;
407          while (true) {
408            if (buffer.getCursor() < buffer.getSize()) {
409              char c = buffer.charAt(buffer.getCursor());
410              if (!Character.isLetterOrDigit(c)) {
411                if (!chars) {
412                  count++;
413                  buffer.moveRight();
414                } else {
415                  break;
416                }
417              } else {
418                chars = true;
419                count++;
420                buffer.moveRight();
421              }
422            } else {
423              break;
424            }
425          }
426          editor.killBuffer.setLength(0);
427          while (count-- > 0) {
428            editor.killBuffer.appendCodePoint(buffer.del());
429          }
430          editor.killBuffer.reverse();
431        }
432      };
433    
434      static EditorAction DELETE_UNTIL_NEXT_WORD = new EditorAction() {
435        @Override
436        void perform(Editor editor, EditorBuffer buffer) throws IOException {
437          int pos = buffer.getCursor();
438          EditorAction.MOVE_NEXT_WORD_AT_BEGINNING.perform(editor, buffer);
439          while (buffer.getCursor() > pos) {
440            buffer.del();
441          }
442        }
443      };
444    
445      static EditorAction DELETE_END = new EditorAction() {
446        @Override
447        void perform(Editor editor, EditorBuffer buffer) throws IOException {
448          int count = 0;
449          while (buffer.moveRight()) {
450            count++;
451          }
452          editor.killBuffer.setLength(0);
453          while (count-- > 0) {
454            editor.killBuffer.appendCodePoint(buffer.del());
455          }
456          editor.killBuffer.reverse();
457          if (buffer.getCursor() > editor.getCursorBound()) {
458            buffer.moveLeft();
459          }
460        }
461      };
462    
463      static EditorAction DELETE_BEGINNING = new EditorAction() {
464        @Override
465        void perform(Editor editor, EditorBuffer buffer) throws IOException {
466          editor.killBuffer.setLength(0);
467          while (editor.buffer.getCursor() > 0) {
468            editor.killBuffer.appendCodePoint(buffer.del());
469          }
470          editor.killBuffer.reverse();
471        }
472      };
473    
474      static EditorAction UNIX_LINE_DISCARD = new EditorAction() {
475        @Override
476        void perform(Editor editor, EditorBuffer buffer) throws IOException {
477          // Not really efficient
478          if (buffer.getCursor()  > 0) {
479            editor.killBuffer.setLength(0);
480            while (buffer.getCursor() > 0) {
481              int c = buffer.del();
482              editor.killBuffer.appendCodePoint(c);
483            }
484            editor.killBuffer.reverse();
485          }
486        }
487      };
488    
489      static EditorAction DELETE_LINE = new EditorAction() {
490        @Override
491        String execute(Editor editor, EditorBuffer buffer, int[] sequence, boolean flush) throws IOException {
492          buffer.moveRightBy(buffer.getSize() - buffer.getCursor());
493          buffer.replace("");
494          return null;
495        }
496      };
497    
498      static EditorAction PASTE_AFTER = new EditorAction() {
499        @Override
500        void perform(Editor editor, EditorBuffer buffer) throws IOException {
501          if (editor.killBuffer.length() > 0) {
502            for (int i = 0;i < editor.killBuffer.length();i++) {
503              char c = editor.killBuffer.charAt(i);
504              buffer.append(c);
505            }
506          }
507        }
508      };
509    
510      static EditorAction MOVE_END = new EditorAction() {
511        @Override
512        void perform(Editor editor, EditorBuffer buffer) throws IOException {
513          int cursor = editor.getCursorBound() - buffer.getCursor();
514          if (cursor > 0) {
515            buffer.moveRightBy(cursor);
516          }
517        }
518      };
519    
520      static abstract class Copy extends EditorAction {
521    
522        protected abstract int getFrom(EditorBuffer buffer);
523    
524        protected abstract int getTo(EditorBuffer buffer);
525    
526        @Override
527        void perform(Editor editor, EditorBuffer buffer) throws IOException {
528          int from = getFrom(buffer);
529          int to = getTo(buffer);
530          editor.killBuffer.setLength(0);
531          for (int i = from;i < to;i++) {
532            editor.killBuffer.append(editor.buffer.charAt(i));
533          }
534        }
535      }
536    
537      static EditorAction COPY = new Copy() {
538        @Override
539        protected int getFrom(EditorBuffer buffer) {
540          return 0;
541        }
542        @Override
543        protected int getTo(EditorBuffer buffer) {
544          return buffer.getSize();
545        }
546      };
547    
548      static EditorAction COPY_END_OF_LINE = new Copy() {
549        @Override
550        protected int getFrom(EditorBuffer buffer) {
551          return buffer.getCursor();
552        }
553        @Override
554        protected int getTo(EditorBuffer buffer) {
555          return buffer.getSize();
556        }
557      };
558    
559      static EditorAction COPY_BEGINNING_OF_LINE = new Copy() {
560        @Override
561        protected int getFrom(EditorBuffer buffer) {
562          return 0;
563        }
564        @Override
565        protected int getTo(EditorBuffer buffer) {
566          return buffer.getCursor();
567        }
568      };
569    
570      static EditorAction COPY_NEXT_WORD = new EditorAction() {
571        @Override
572        void perform(Editor editor, EditorBuffer buffer) throws IOException {
573          int size = editor.buffer.getSize();
574          int cursor = editor.buffer.getCursor();
575          editor.killBuffer.setLength(0);
576          while (cursor < size && editor.buffer.charAt(cursor) != ' ') {
577            editor.killBuffer.append(editor.buffer.charAt(cursor++));
578          }
579          while (cursor < size && editor.buffer.charAt(cursor) == ' ') {
580            editor.killBuffer.append(editor.buffer.charAt(cursor++));
581          }
582        }
583      };
584    
585      static EditorAction COPY_PREV_WORD = new EditorAction() {
586        @Override
587        void perform(Editor editor, EditorBuffer buffer) throws IOException {
588          int cursor = buffer.getCursor() - 1;
589          editor.killBuffer.setLength(0);
590          while (cursor > 0 && buffer.charAt(cursor) != ' ') {
591            editor.killBuffer.append(buffer.charAt(cursor--));
592          }
593          while (cursor > 0 && editor.buffer.charAt(cursor) == ' ') {
594            editor.killBuffer.append(buffer.charAt(cursor--));
595          }
596          editor.killBuffer.reverse();
597        }
598      };
599    
600      static class ChangeChars extends EditorAction {
601    
602        /** . */
603        public final int count;
604    
605        /** . */
606        public final int c;
607    
608        public ChangeChars(int count, int c) {
609          this.count = count;
610          this.c = c;
611        }
612    
613        @Override
614        void perform(Editor editor, EditorBuffer buffer) throws IOException {
615          int a = Math.min(count, buffer.getSize() - buffer.getCursor());
616          while (a-- > 0) {
617            buffer.moveRight((char)c);
618          }
619          buffer.moveLeft();
620        }
621      }
622    
623      static EditorAction DELETE_PREV_CHAR = new EditorAction() {
624        @Override
625        void perform(Editor editor, EditorBuffer buffer) throws IOException {
626          buffer.del();
627        }
628      };
629    
630      static class DeleteNextChars extends EditorAction {
631    
632        /** . */
633        public final int count;
634    
635        public DeleteNextChars(int count) {
636          this.count = count;
637        }
638    
639        @Override
640        void perform(Editor editor, EditorBuffer buffer) throws IOException {
641          int tmp = count;
642          while (tmp > 0 && buffer.moveRight()) {
643            tmp--;
644          }
645          while (tmp++ < count) {
646            buffer.del();
647          }
648          if (buffer.getCursor() > editor.getCursorBound()) {
649            buffer.moveLeft();
650          }
651        }
652      }
653    
654      static EditorAction DELETE_NEXT_CHAR = ((EditorAction)new DeleteNextChars(1));
655    
656      static EditorAction CHANGE_CASE = new EditorAction() {
657        @Override
658        void perform(Editor editor, EditorBuffer buffer) throws IOException {
659          if (buffer.getCursor() < buffer.getSize()) {
660            char c = buffer.charAt(buffer.getCursor());
661            if (Character.isUpperCase(c)) {
662              c = Character.toLowerCase(c);
663            }
664            else if (Character.isLowerCase(c)) {
665              c = Character.toUpperCase(c);
666            }
667            buffer.moveRight(c);
668            if (buffer.getCursor() > editor.getCursorBound()) {
669              buffer.moveLeft();
670            }
671          }
672        }
673      };
674    
675      static EditorAction TRANSPOSE_CHARS = new EditorAction() {
676        @Override
677        void perform(Editor editor, EditorBuffer buffer) throws IOException {
678          if (buffer.getSize() > 2) {
679            int pos = buffer.getCursor();
680            if (pos > 0) {
681              if (pos < buffer.getSize()) {
682                if (buffer.moveLeft()) {
683                  char a = buffer.charAt(pos - 1);
684                  char b = buffer.charAt(pos);
685                  buffer.moveRight(b); // Should be assertion
686                  buffer.moveRight(a); // Should be assertion
687                  // A bit not great : need to find a better way to do that...
688                  if (editor.console.getMode() == Mode.VI_MOVE && buffer.getCursor() > editor.getCursorBound()) {
689                    buffer.moveLeft();
690                  }
691                }
692              } else {
693                if (buffer.moveLeft() && buffer.moveLeft()) {
694                  char a = buffer.charAt(pos - 2);
695                  char b = buffer.charAt(pos - 1);
696                  buffer.moveRight(b); // Should be assertion
697                  buffer.moveRight(a); // Should be assertion
698                }
699              }
700            }
701          }
702        }
703      };
704    
705      static EditorAction INSERT_COMMENT = new EditorAction() {
706        @Override
707        String execute(Editor editor, EditorBuffer buffer, int[] sequence, boolean flush) throws IOException {
708          EditorAction.MOVE_BEGINNING.perform(editor, buffer);
709          buffer.append("#");
710          return EditorAction.ENTER.execute(editor, buffer, sequence, flush);
711        }
712      };
713    
714      static EditorAction CLS = new EditorAction() {
715        @Override
716        void perform(Editor editor, EditorBuffer buffer) throws IOException {
717          editor.console.driver.cls();
718          StringBuilder sb = new StringBuilder();
719          int index = 0;
720          List<String> lines = buffer.getLines();
721          for (String line : lines) {
722            if (index == 0) {
723              String prompt = editor.console.shell.getPrompt();
724              sb.append(prompt == null ? "" : prompt);
725            } else {
726              sb.append("\n> ");
727            }
728            sb.append(line);
729            index++;
730          }
731          editor.console.driver.write(sb.toString());
732          editor.console.driver.flush();
733        }
734      };
735    
736      static EditorAction ENTER = new EditorAction() {
737        @Override
738        String execute(Editor editor, EditorBuffer buffer, int[] sequence, boolean flush) throws IOException {
739          editor.historyCursor = -1;
740          editor.historyBuffer = null;
741          String line = buffer.getLine();
742          editor.lineParser.append(line);
743          if (editor.console.getMode() == Mode.VI_MOVE) {
744            editor.console.setMode(Mode.VI_INSERT);
745          }
746          if (editor.lineParser.crlf()) {
747            editor.console.driver.writeCRLF();
748            editor.console.driver.flush();
749            String request = editor.visitor.getRaw();
750            editor.addToHistory(request);
751            return request;
752          } else {
753            buffer.append('\n');
754            editor.console.driver.write("> ");
755            if (flush) {
756              buffer.flush();
757            }
758            return null;
759          }
760        }
761      };
762    
763      String execute(Editor editor, EditorBuffer buffer, int[] sequence, boolean flush) throws IOException {
764        perform(editor, buffer);
765        if (flush) {
766          buffer.flush();
767        }
768        return null;
769      }
770    
771      void perform(Editor editor, EditorBuffer buffer) throws IOException {
772        throw new UnsupportedOperationException("Implement the edition logic");
773      }
774    
775      public EditorAction then(final EditorAction action) {
776        return new EditorAction() {
777          @Override
778          String execute(Editor editor, EditorBuffer buffer, int[] sequence, boolean flush) throws IOException {
779            EditorAction.this.execute(editor, buffer, sequence, flush);
780            return action.execute(editor, buffer, sequence, flush);
781          }
782        };
783      }
784    
785      public EditorAction repeat(final int count) {
786        return new EditorAction() {
787          @Override
788          void perform(Editor editor, EditorBuffer buffer) throws IOException {
789            for (int i = 0;i < count;i++) {
790              EditorAction.this.perform(editor, buffer);
791            }
792          }
793        };
794      }
795    }