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 java.io.IOException;
023import java.util.ArrayList;
024import java.util.Iterator;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.NoSuchElementException;
028
029final 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}