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}