1 /* 2 * Copyright (c) 2002-2018, the original author or authors. 3 * 4 * This software is distributable under the BSD license. See the terms of the 5 * BSD license in the documentation provided with this software. 6 * 7 * http://www.opensource.org/licenses/bsd-license.php 8 */ 9 package jdk.internal.org.jline.utils; 10 11 import java.util.Collections; 12 import java.util.HashMap; 13 import java.util.List; 14 import java.util.Map; 15 import java.util.Objects; 16 import java.util.stream.Collectors; 17 18 import jdk.internal.org.jline.terminal.Terminal; 19 import jdk.internal.org.jline.utils.InfoCmp.Capability; 20 21 /** 22 * Handle display and visual cursor. 23 * 24 * @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a> 25 */ 26 public class Display { 27 28 public static boolean DISABLE_CR = false; 29 30 protected final Terminal terminal; 31 protected final boolean fullScreen; 32 protected List<AttributedString> oldLines = Collections.emptyList(); 33 protected int cursorPos; 34 private int columns; 35 private int columns1; // columns+1 36 protected int rows; 37 protected boolean reset; 38 protected boolean delayLineWrap; 39 40 protected final Map<Capability, Integer> cost = new HashMap<>(); 41 protected final boolean canScroll; 42 protected final boolean wrapAtEol; 43 protected final boolean delayedWrapAtEol; 44 protected final boolean cursorDownIsNewLine; 45 46 public Display(Terminal terminal, boolean fullscreen) { 47 this.terminal = terminal; 48 this.fullScreen = fullscreen; 49 50 this.canScroll = can(Capability.insert_line, Capability.parm_insert_line) 51 && can(Capability.delete_line, Capability.parm_delete_line); 52 this.wrapAtEol = terminal.getBooleanCapability(Capability.auto_right_margin); 53 this.delayedWrapAtEol = this.wrapAtEol 54 && terminal.getBooleanCapability(Capability.eat_newline_glitch); 55 this.cursorDownIsNewLine = "\n".equals(Curses.tputs(terminal.getStringCapability(Capability.cursor_down))); 56 } 57 58 /** 59 * If cursor is at right margin, don't wrap immediately. 60 * See <code>org.jline.reader.LineReader.Option#DELAY_LINE_WRAP</code>. 61 * @return <code>true</code> if line wrap is delayed, <code>false</code> otherwise 62 */ 63 public boolean delayLineWrap() { 64 return delayLineWrap; 65 } 66 public void setDelayLineWrap(boolean v) { delayLineWrap = v; } 67 68 public void resize(int rows, int columns) { 69 if (this.rows != rows || this.columns != columns) { 70 this.rows = rows; 71 this.columns = columns; 72 this.columns1 = columns + 1; 73 oldLines = AttributedString.join(AttributedString.EMPTY, oldLines).columnSplitLength(columns, true, delayLineWrap()); 74 } 75 } 76 77 public void reset() { 78 oldLines = Collections.emptyList(); 79 } 80 81 /** 82 * Clears the whole screen. 83 * Use this method only when using full-screen / application mode. 84 */ 85 public void clear() { 86 if (fullScreen) { 87 reset = true; 88 } 89 } 90 91 public void updateAnsi(List<String> newLines, int targetCursorPos) { 92 update(newLines.stream().map(AttributedString::fromAnsi).collect(Collectors.toList()), targetCursorPos); 93 } 94 95 /** 96 * Update the display according to the new lines and flushes the output. 97 * @param newLines the lines to display 98 * @param targetCursorPos desired cursor position - see Size.cursorPos. 99 */ 100 public void update(List<AttributedString> newLines, int targetCursorPos) { 101 update(newLines, targetCursorPos, true); 102 } 103 104 /** 105 * Update the display according to the new lines. 106 * @param newLines the lines to display 107 * @param targetCursorPos desired cursor position - see Size.cursorPos. 108 * @param flush whether the output should be flushed or not 109 */ 110 public void update(List<AttributedString> newLines, int targetCursorPos, boolean flush) { 111 if (reset) { 112 terminal.puts(Capability.clear_screen); 113 oldLines.clear(); 114 cursorPos = 0; 115 reset = false; 116 } 117 118 // If dumb display, get rid of ansi sequences now 119 Integer cols = terminal.getNumericCapability(Capability.max_colors); 120 if (cols == null || cols < 8) { 121 newLines = newLines.stream().map(s -> new AttributedString(s.toString())) 122 .collect(Collectors.toList()); 123 } 124 125 // Detect scrolling 126 if ((fullScreen || newLines.size() >= rows) && newLines.size() == oldLines.size() && canScroll) { 127 int nbHeaders = 0; 128 int nbFooters = 0; 129 // Find common headers and footers 130 int l = newLines.size(); 131 while (nbHeaders < l 132 && Objects.equals(newLines.get(nbHeaders), oldLines.get(nbHeaders))) { 133 nbHeaders++; 134 } 135 while (nbFooters < l - nbHeaders - 1 136 && Objects.equals(newLines.get(newLines.size() - nbFooters - 1), oldLines.get(oldLines.size() - nbFooters - 1))) { 137 nbFooters++; 138 } 139 List<AttributedString> o1 = newLines.subList(nbHeaders, newLines.size() - nbFooters); 140 List<AttributedString> o2 = oldLines.subList(nbHeaders, oldLines.size() - nbFooters); 141 int[] common = longestCommon(o1, o2); 142 if (common != null) { 143 int s1 = common[0]; 144 int s2 = common[1]; 145 int sl = common[2]; 146 if (sl > 1 && s1 < s2) { 147 moveVisualCursorTo((nbHeaders + s1) * columns1); 148 int nb = s2 - s1; 149 deleteLines(nb); 150 for (int i = 0; i < nb; i++) { 151 oldLines.remove(nbHeaders + s1); 152 } 153 if (nbFooters > 0) { 154 moveVisualCursorTo((nbHeaders + s1 + sl) * columns1); 155 insertLines(nb); 156 for (int i = 0; i < nb; i++) { 157 oldLines.add(nbHeaders + s1 + sl, new AttributedString("")); 158 } 159 } 160 } else if (sl > 1 && s1 > s2) { 161 int nb = s1 - s2; 162 if (nbFooters > 0) { 163 moveVisualCursorTo((nbHeaders + s2 + sl) * columns1); 164 deleteLines(nb); 165 for (int i = 0; i < nb; i++) { 166 oldLines.remove(nbHeaders + s2 + sl); 167 } 168 } 169 moveVisualCursorTo((nbHeaders + s2) * columns1); 170 insertLines(nb); 171 for (int i = 0; i < nb; i++) { 172 oldLines.add(nbHeaders + s2, new AttributedString("")); 173 } 174 } 175 } 176 } 177 178 int lineIndex = 0; 179 int currentPos = 0; 180 int numLines = Math.max(oldLines.size(), newLines.size()); 181 boolean wrapNeeded = false; 182 while (lineIndex < numLines) { 183 AttributedString oldLine = 184 lineIndex < oldLines.size() ? oldLines.get(lineIndex) 185 : AttributedString.NEWLINE; 186 AttributedString newLine = 187 lineIndex < newLines.size() ? newLines.get(lineIndex) 188 : AttributedString.NEWLINE; 189 currentPos = lineIndex * columns1; 190 int curCol = currentPos; 191 int oldLength = oldLine.length(); 192 int newLength = newLine.length(); 193 boolean oldNL = oldLength > 0 && oldLine.charAt(oldLength-1)=='\n'; 194 boolean newNL = newLength > 0 && newLine.charAt(newLength-1)=='\n'; 195 if (oldNL) { 196 oldLength--; 197 oldLine = oldLine.substring(0, oldLength); 198 } 199 if (newNL) { 200 newLength--; 201 newLine = newLine.substring(0, newLength); 202 } 203 if (wrapNeeded 204 && lineIndex == (cursorPos + 1) / columns1 205 && lineIndex < newLines.size()) { 206 // move from right margin to next line's left margin 207 cursorPos++; 208 if (newLength == 0 || newLine.isHidden(0)) { 209 // go to next line column zero 210 rawPrint(new AttributedString(" \b")); 211 } else { 212 AttributedString firstChar = 213 newLine.columnSubSequence(0, 1); 214 // go to next line column one 215 rawPrint(firstChar); 216 cursorPos++; 217 int firstLength = firstChar.length(); // normally 1 218 newLine = newLine.substring(firstLength, newLength); 219 newLength -= firstLength; 220 if (oldLength >= firstLength) { 221 oldLine = oldLine.substring(firstLength, oldLength); 222 oldLength -= firstLength; 223 } 224 currentPos = cursorPos; 225 } 226 } 227 List<DiffHelper.Diff> diffs = DiffHelper.diff(oldLine, newLine); 228 boolean ident = true; 229 boolean cleared = false; 230 for (int i = 0; i < diffs.size(); i++) { 231 DiffHelper.Diff diff = diffs.get(i); 232 int width = diff.text.columnLength(); 233 switch (diff.operation) { 234 case EQUAL: 235 if (!ident) { 236 cursorPos = moveVisualCursorTo(currentPos); 237 rawPrint(diff.text); 238 cursorPos += width; 239 currentPos = cursorPos; 240 } else { 241 currentPos += width; 242 } 243 break; 244 case INSERT: 245 if (i <= diffs.size() - 2 246 && diffs.get(i + 1).operation == DiffHelper.Operation.EQUAL) { 247 cursorPos = moveVisualCursorTo(currentPos); 248 if (insertChars(width)) { 249 rawPrint(diff.text); 250 cursorPos += width; 251 currentPos = cursorPos; 252 break; 253 } 254 } else if (i <= diffs.size() - 2 255 && diffs.get(i + 1).operation == DiffHelper.Operation.DELETE 256 && width == diffs.get(i + 1).text.columnLength()) { 257 moveVisualCursorTo(currentPos); 258 rawPrint(diff.text); 259 cursorPos += width; 260 currentPos = cursorPos; 261 i++; // skip delete 262 break; 263 } 264 moveVisualCursorTo(currentPos); 265 rawPrint(diff.text); 266 cursorPos += width; 267 currentPos = cursorPos; 268 ident = false; 269 break; 270 case DELETE: 271 if (cleared) { 272 continue; 273 } 274 if (currentPos - curCol >= columns) { 275 continue; 276 } 277 if (i <= diffs.size() - 2 278 && diffs.get(i + 1).operation == DiffHelper.Operation.EQUAL) { 279 if (currentPos + diffs.get(i + 1).text.columnLength() < columns) { 280 moveVisualCursorTo(currentPos); 281 if (deleteChars(width)) { 282 break; 283 } 284 } 285 } 286 int oldLen = oldLine.columnLength(); 287 int newLen = newLine.columnLength(); 288 int nb = Math.max(oldLen, newLen) - (currentPos - curCol); 289 moveVisualCursorTo(currentPos); 290 if (!terminal.puts(Capability.clr_eol)) { 291 rawPrint(' ', nb); 292 cursorPos += nb; 293 } 294 cleared = true; 295 ident = false; 296 break; 297 } 298 } 299 lineIndex++; 300 boolean newWrap = ! newNL && lineIndex < newLines.size(); 301 if (targetCursorPos + 1 == lineIndex * columns1 302 && (newWrap || ! delayLineWrap)) 303 targetCursorPos++; 304 boolean atRight = (cursorPos - curCol) % columns1 == columns; 305 wrapNeeded = false; 306 if (this.delayedWrapAtEol) { 307 boolean oldWrap = ! oldNL && lineIndex < oldLines.size(); 308 if (newWrap != oldWrap && ! (oldWrap && cleared)) { 309 moveVisualCursorTo(lineIndex*columns1-1, newLines); 310 if (newWrap) 311 wrapNeeded = true; 312 else 313 terminal.puts(Capability.clr_eol); 314 } 315 } else if (atRight) { 316 if (this.wrapAtEol) { 317 terminal.writer().write(" \b"); 318 cursorPos++; 319 } else { 320 terminal.puts(Capability.carriage_return); // CR / not newline. 321 cursorPos = curCol; 322 } 323 currentPos = cursorPos; 324 } 325 } 326 int was = cursorPos; 327 if (cursorPos != targetCursorPos) { 328 moveVisualCursorTo(targetCursorPos < 0 ? currentPos : targetCursorPos, newLines); 329 } 330 oldLines = newLines; 331 332 if (flush) { 333 terminal.flush(); 334 } 335 } 336 337 protected boolean deleteLines(int nb) { 338 return perform(Capability.delete_line, Capability.parm_delete_line, nb); 339 } 340 341 protected boolean insertLines(int nb) { 342 return perform(Capability.insert_line, Capability.parm_insert_line, nb); 343 } 344 345 protected boolean insertChars(int nb) { 346 return perform(Capability.insert_character, Capability.parm_ich, nb); 347 } 348 349 protected boolean deleteChars(int nb) { 350 return perform(Capability.delete_character, Capability.parm_dch, nb); 351 } 352 353 protected boolean can(Capability single, Capability multi) { 354 return terminal.getStringCapability(single) != null 355 || terminal.getStringCapability(multi) != null; 356 } 357 358 protected boolean perform(Capability single, Capability multi, int nb) { 359 boolean hasMulti = terminal.getStringCapability(multi) != null; 360 boolean hasSingle = terminal.getStringCapability(single) != null; 361 if (hasMulti && (!hasSingle || cost(single) * nb > cost(multi))) { 362 terminal.puts(multi, nb); 363 return true; 364 } else if (hasSingle) { 365 for (int i = 0; i < nb; i++) { 366 terminal.puts(single); 367 } 368 return true; 369 } else { 370 return false; 371 } 372 } 373 374 private int cost(Capability cap) { 375 return cost.computeIfAbsent(cap, this::computeCost); 376 } 377 378 private int computeCost(Capability cap) { 379 String s = Curses.tputs(terminal.getStringCapability(cap), 0); 380 return s != null ? s.length() : Integer.MAX_VALUE; 381 } 382 383 private static int[] longestCommon(List<AttributedString> l1, List<AttributedString> l2) { 384 int start1 = 0; 385 int start2 = 0; 386 int max = 0; 387 for (int i = 0; i < l1.size(); i++) { 388 for (int j = 0; j < l2.size(); j++) { 389 int x = 0; 390 while (Objects.equals(l1.get(i + x), l2.get(j + x))) { 391 x++; 392 if (((i + x) >= l1.size()) || ((j + x) >= l2.size())) break; 393 } 394 if (x > max) { 395 max = x; 396 start1 = i; 397 start2 = j; 398 } 399 } 400 } 401 return max != 0 ? new int[] { start1, start2, max } : null; 402 } 403 404 /* 405 * Move cursor from cursorPos to argument, updating cursorPos 406 * We're at the right margin if {@code (cursorPos % columns1) == columns}. 407 * This method knows how to move both *from* and *to* the right margin. 408 */ 409 protected void moveVisualCursorTo(int targetPos, 410 List<AttributedString> newLines) { 411 if (cursorPos != targetPos) { 412 boolean atRight = (targetPos % columns1) == columns; 413 moveVisualCursorTo(targetPos - (atRight ? 1 : 0)); 414 if (atRight) { 415 // There is no portable way to move to the right margin 416 // except by writing a character in the right-most column. 417 int row = targetPos / columns1; 418 AttributedString lastChar = row >= newLines.size() ? AttributedString.EMPTY 419 : newLines.get(row).columnSubSequence(columns-1, columns); 420 if (lastChar.length() == 0) 421 rawPrint((int) ' '); 422 else 423 rawPrint(lastChar); 424 cursorPos++; 425 } 426 } 427 } 428 429 /* 430 * Move cursor from cursorPos to argument, updating cursorPos 431 * We're at the right margin if {@code (cursorPos % columns1) == columns}. 432 * This method knows how to move *from* the right margin, 433 * but does not know how to move *to* the right margin. 434 * I.e. {@code (i1 % columns1) == column} is not allowed. 435 */ 436 protected int moveVisualCursorTo(int i1) { 437 int i0 = cursorPos; 438 if (i0 == i1) return i1; 439 int width = columns1; 440 int l0 = i0 / width; 441 int c0 = i0 % width; 442 int l1 = i1 / width; 443 int c1 = i1 % width; 444 if (c0 == columns) { // at right margin 445 terminal.puts(Capability.carriage_return); 446 c0 = 0; 447 } 448 if (l0 > l1) { 449 perform(Capability.cursor_up, Capability.parm_up_cursor, l0 - l1); 450 } else if (l0 < l1) { 451 // TODO: clean the following 452 if (fullScreen) { 453 if (!terminal.puts(Capability.parm_down_cursor, l1 - l0)) { 454 for (int i = l0; i < l1; i++) { 455 terminal.puts(Capability.cursor_down); 456 } 457 if (cursorDownIsNewLine) { 458 c0 = 0; 459 } 460 } 461 } else { 462 terminal.puts(Capability.carriage_return); 463 rawPrint('\n', l1 - l0); 464 c0 = 0; 465 } 466 } 467 if (c0 != 0 && c1 == 0 && !DISABLE_CR) { 468 terminal.puts(Capability.carriage_return); 469 } else if (c0 < c1) { 470 perform(Capability.cursor_right, Capability.parm_right_cursor, c1 - c0); 471 } else if (c0 > c1) { 472 perform(Capability.cursor_left, Capability.parm_left_cursor, c0 - c1); 473 } 474 cursorPos = i1; 475 return i1; 476 } 477 478 void rawPrint(char c, int num) { 479 for (int i = 0; i < num; i++) { 480 rawPrint(c); 481 } 482 } 483 484 void rawPrint(int c) { 485 terminal.writer().write(c); 486 } 487 488 void rawPrint(AttributedString str) { 489 terminal.writer().write(str.toAnsi(terminal)); 490 } 491 492 public int wcwidth(String str) { 493 return AttributedString.fromAnsi(str).columnLength(); 494 } 495 496 }