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 }