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 java.io.IOException;
023    import java.util.ArrayList;
024    import java.util.Iterator;
025    import java.util.LinkedList;
026    import java.util.List;
027    import java.util.NoSuchElementException;
028    
029    final class EditorBuffer implements Appendable, Iterator<String> {
030    
031      /** . */
032      private StringBuilder current;
033    
034      /** Cursor position. */
035      private int cursor;
036    
037      /** Previous lines. */
038      private LinkedList<String> lines;
039    
040      /** The output. */
041      private final ConsoleDriver driver;
042    
043      /** True if flush is needed. */
044      private boolean needFlush;
045    
046      EditorBuffer(ConsoleDriver driver) {
047        this.current = new StringBuilder();
048        this.cursor = 0;
049        this.lines = new LinkedList<String>();
050        this.driver = driver;
051        this.needFlush = false;
052      }
053    
054      void flush() throws IOException {
055        flush(false);
056      }
057    
058      void flush(boolean force) throws IOException {
059        if (needFlush || force) {
060          driver.flush();
061          needFlush = false;
062        }
063      }
064    
065      /**
066       * Reset the buffer state.
067       */
068      void reset() {
069        this.lines.clear();
070        this.cursor = 0;
071        this.current.setLength(0);
072      }
073    
074      /**
075       * Returns the total number of chars in the buffer, independently of the cursor position.
076       *
077       * @return the number of chars
078       */
079      int getSize() {
080        return current.length();
081      }
082    
083      /**
084       * Returns the current cursor position.
085       *
086       * @return the cursor position
087       */
088      int getCursor() {
089        return cursor;
090      }
091    
092      /**
093       * Returns a character at a specified index in the buffer.
094       *
095       * @param index the index
096       * @return the char
097       * @throws StringIndexOutOfBoundsException if the index is negative or larget than the size
098       */
099      char charAt(int index) throws StringIndexOutOfBoundsException {
100        return current.charAt(index);
101      }
102    
103      /**
104       * @return the current line
105       */
106      public String getLine() {
107        return current.toString();
108      }
109    
110      /**
111       * @return the lines
112       */
113      public List<String> getLines() {
114        ArrayList<String> tmp = new ArrayList<String>(lines.size() + 1);
115        tmp.addAll(lines);
116        tmp.add(getLine());
117        return tmp;
118      }
119    
120      // Iterator<String> implementation ***********************************************************************************
121    
122      @Override
123      public boolean hasNext() {
124        return lines.size() > 0;
125      }
126    
127      @Override
128      public String next() {
129        if (lines.size() == 0) {
130          throw new NoSuchElementException();
131        }
132        return lines.removeFirst();
133      }
134    
135      @Override
136      public void remove() {
137        throw new UnsupportedOperationException();
138      }
139    
140      // Appendable implementation *****************************************************************************************
141    
142      public EditorBuffer append(char c) throws IOException {
143        appendData(Character.toString(c), 0, 1);
144        return this;
145      }
146    
147      public EditorBuffer append(CharSequence s) throws IOException {
148        return append(s, 0, s.length());
149      }
150    
151      public EditorBuffer append(CharSequence csq, int start, int end) throws IOException {
152        appendData(csq, start, end);
153        return this;
154      }
155    
156      // Protected methods *************************************************************************************************
157    
158      /**
159       * Replace all the characters before the cursor by the provided char sequence.
160       *
161       * @param s the new char sequence
162       * @return the l
163       * @throws java.io.IOException any IOException
164       */
165      String replace(CharSequence s) throws IOException {
166        StringBuilder builder = new StringBuilder();
167        for (int i = appendDel();i != -1;i = appendDel()) {
168          builder.append((char)i);
169          needFlush = true;
170        }
171        appendData(s, 0, s.length());
172        return builder.reverse().toString();
173      }
174    
175      /**
176       * Move the cursor right by one char with the provided char.
177       *
178       * @param c the char to overwrite
179       * @return true if it happended
180       * @throws IOException
181       */
182      boolean moveRight(char c) throws IOException {
183        if (cursor < current.length()) {
184          if (driver.moveRight(c)) {
185            current.setCharAt(cursor++, c);
186            return true;
187          }
188        }
189        return false;
190      }
191    
192      boolean moveRight() throws IOException {
193        return moveRightBy(1) == 1;
194      }
195    
196      boolean moveLeft() throws IOException {
197        return moveLeftBy(1) == 1;
198      }
199    
200      int moveRightBy(int count) throws IOException, IllegalArgumentException {
201        if (count < 0) {
202          throw new IllegalArgumentException("Cannot move with negative count " + count);
203        }
204        int delta = 0;
205        while (delta < count) {
206          if (cursor + delta < current.length() && driver.moveRight(current.charAt(cursor + delta))) {
207            delta++;
208          } else {
209            break;
210          }
211        }
212        if (delta > 0) {
213          needFlush = true;
214          cursor += delta;
215        }
216        return delta;
217      }
218    
219      int moveLeftBy(int count) throws IOException, IllegalArgumentException {
220        if (count < 0) {
221          throw new IllegalArgumentException("Cannot move with negative count " + count);
222        }
223        int delta = 0;
224        while (delta < count) {
225          if (delta < cursor && driver.moveLeft()) {
226            delta++;
227          } else {
228            break;
229          }
230        }
231        if (delta > 0) {
232          needFlush = true;
233          cursor -= delta;
234        }
235        return delta;
236      }
237    
238      /**
239       * Delete the char under the cursor or return -1 if no char was deleted.
240       *
241       * @return the deleted char
242       * @throws java.io.IOException any IOException
243       */
244      int del() throws IOException {
245        int ret = appendDel();
246        if (ret != -1) {
247          needFlush = true;
248        }
249        return ret;
250      }
251    
252      private void appendData(CharSequence s, int start, int end) throws IOException {
253        if (start < 0) {
254          throw new IndexOutOfBoundsException("No negative start");
255        }
256        if (end < 0) {
257          throw new IndexOutOfBoundsException("No negative end");
258        }
259        if (end > s.length()) {
260          throw new IndexOutOfBoundsException("End cannot be greater than sequence length");
261        }
262        if (end < start) {
263          throw new IndexOutOfBoundsException("Start cannot be greater than end");
264        }
265    
266        // Break into lines
267        int pos = start;
268        while (pos < end) {
269          char c = s.charAt(pos);
270          if (c == '\n') {
271            newAppendNoLF(s, start, pos);
272            String line = current.toString();
273            lines.add(line);
274            cursor = 0;
275            current.setLength(0);
276            echoCRLF();
277            start = ++pos;
278          } else {
279            pos++;
280          }
281        }
282    
283        // Append the rest if any
284        newAppendNoLF(s, start, pos);
285      }
286    
287      private void newAppendNoLF(CharSequence s, int start, int end) throws IOException {
288    
289        // Count the number of chars
290        // at the moment we ignore \r
291        // since this behavior is erratic and not well defined
292        // not sure we need to handle this here... since we kind of handle it too in the ConsoleDriver.write(int)
293        int len = 0;
294        for (int i = start;i < end;i++) {
295          if (s.charAt(i) != '\r') {
296            len++;
297          }
298        }
299    
300        //
301        if (len > 0) {
302    
303          // Now insert our data
304          int count = cursor;
305          int size = current.length();
306          for (int i = start;i < end;i++) {
307            char c = s.charAt(i);
308            if (c != '\r') {
309              current.insert(count++, c);
310              driver.write(c);
311            }
312          }
313    
314          // Now redraw what is missing and put the cursor back at the correct place
315          for (int i = cursor;i < size;i++) {
316            driver.write(current.charAt(len + i));
317          }
318          for (int i = cursor;i < size;i++) {
319            driver.moveLeft();
320          }
321    
322          // Update state
323          size += len;
324          cursor += len;
325          needFlush = true;
326        }
327      }
328    
329    
330      /**
331       * Delete the char before the cursor.
332       *
333       * @return the removed char value or -1 if no char was removed
334       * @throws java.io.IOException any IOException
335       */
336      private int appendDel() throws IOException {
337    
338        // If the cursor is at the most right position (i.e no more chars after)
339        if (cursor == current.length()){
340          int popped = pop();
341    
342          //
343          if (popped != -1) {
344            echoDel();
345            // We do not care about the return value of echoDel, but we will return a value that indcates
346            // that a flush is required although it may not
347            // to properly carry out the status we should have two things to return
348            // 1/ the popped char
349            // 2/ the boolean indicating if flush is required
350          }
351    
352          //
353          return popped;
354        } else {
355          // We are editing the line
356    
357          // Shift all the chars after the cursor
358          int popped = pop();
359    
360          //
361          if (popped != -1) {
362    
363            // We move the cursor to left
364            if (driver.moveLeft()) {
365              StringBuilder disp = new StringBuilder();
366              disp.append(current, cursor, current.length());
367              disp.append(' ');
368              driver.write(disp);
369              int amount = current.length() - cursor + 1;
370              while (amount > 0) {
371                driver.moveLeft();
372                amount--;
373              }
374            } else {
375              throw new UnsupportedOperationException("not implemented");
376            }
377          }
378    
379          //
380          return popped;
381        }
382      }
383    
384      private void echoDel() throws IOException {
385        driver.writeDel();
386        needFlush = true;
387      }
388    
389      private void echoCRLF() throws IOException {
390        driver.writeCRLF();
391        needFlush = true;
392      }
393    
394      /**
395       * Popup one char from buffer at the current cursor position.
396       *
397       * @return the popped char or -1 if none was removed
398       */
399      private int pop() {
400        if (cursor > 0) {
401          char popped = current.charAt(cursor - 1);
402          current.deleteCharAt(cursor - 1);
403          cursor--;
404         return popped;
405        } else {
406          return -1;
407        }
408      }
409    }