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 }