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