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      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}