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 020package org.crsh.console; 021 022import org.crsh.cli.impl.Delimiter; 023import org.crsh.cli.impl.completion.CompletionMatch; 024import org.crsh.cli.impl.line.LineParser; 025import org.crsh.cli.impl.line.MultiLineVisitor; 026import org.crsh.cli.spi.Completion; 027import org.crsh.util.Utils; 028 029import java.io.IOException; 030import java.util.Iterator; 031import java.util.List; 032import java.util.Map; 033 034/** 035 * An action on the editor. 036 */ 037class 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}