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 }