int InputBuffer::getInputLine()

in ext/linenoise-ng/src/linenoise.cpp [2776:3409]


int InputBuffer::getInputLine(PromptBase &pi) {
  keyType = 0;

  // The latest history entry is always our current buffer
  if (len > 0) {
    size_t bufferSize = sizeof(char32_t) * len + 1;
    unique_ptr<char[]> tempBuffer(new char[bufferSize]);
    copyString32to8(tempBuffer.get(), bufferSize, buf32);
    // line has to be added, even if it's a duplicate, as the latest history
    // entry is assumed to contain the current buffer
    linenoiseHistoryAdd(tempBuffer.get(), true);
  } else {
    linenoiseHistoryAdd("", true);
  }
  historyIndex = historyLen - 1;
  historyRecallMostRecent = false;

  // display the prompt
  if (!pi.write()) return -1;

#ifndef _WIN32
  // we have to generate our own newline on line wrap on Linux
  if (pi.promptIndentation == 0 && pi.promptExtraLines > 0)
    if (write(1, "\n", 1) == -1) return -1;
#endif

  // the cursor starts out at the end of the prompt
  pi.promptCursorRowOffset = pi.promptExtraLines;

  // kill and yank start in "other" mode
  killRing.lastAction = KillRing::actionOther;

  // when history search returns control to us, we execute its terminating
  // keystroke
  int terminatingKeystroke = -1;

  // if there is already text in the buffer, display it first
  if (len > 0) {
    refreshLine(pi);
  }

  // loop collecting characters, respond to line editing characters
  while (true) {
    int c;
    if (terminatingKeystroke == -1) {
      c = linenoiseReadChar();  // get a new keystroke
      // BUG27894642: on Windows, the character may have flags set to indicate
      // that i.e. CTRL key was pressed; need to remove them to correctly
      // identify the key type
      c = cleanupCtrl(c);  // convert CTRL + <char> into normal ctrl

      keyType = 0;
      if (c != 0) {
        // set flag that we got some input
        if (c == ctrlChar('C')) {
          keyType = 1;
        } else if (c == ctrlChar('D')) {
          keyType = 2;
        }
      } else {
        // reading was interrupted (i.e. stdin was closed)
        keyType = 3;
      }

#ifndef _WIN32
      if (c == 0 && gotResize) {
        // caught a window resize event
        // now redraw the prompt and line
        gotResize = false;
        pi.promptScreenColumns = getScreenColumns();
        dynamicRefresh(pi, buf32, len,
                       pos);  // redraw the original prompt with current input
        continue;
      }
#endif
    } else {
      c = terminatingKeystroke;   // use the terminating keystroke from search
      terminatingKeystroke = -1;  // clear it once we've used it
    }

    if (c == 0) {
      return len;
    }

    if (c == -1) {
      refreshLine(pi);
      continue;
    }

    if (c == -2) {
      if (!pi.write()) return -1;
      refreshLine(pi);
      continue;
    }

    if (g_custom_commands.begin(c)) {
      while (!g_custom_commands.is_finished()) {
        if (!g_custom_commands.next(cleanupCtrl(linenoiseReadChar()))) {
          beep();
          break;
        }
      }

      if (g_custom_commands.is_finished()) {
        const auto command = g_custom_commands.get();
        char *output = nullptr;
        Utf8String input(Utf32String(buf32, len));

        terminatingKeystroke = command.call(input.get(), &output);

        if (nullptr != output) {
          size_t count = 0;
          copyString8to32(buf32, buflen, count, output);
          len = pos = static_cast<int>(count);
          free(output);

          refreshLine(pi);
        }
      }

      continue;
    }

    // ctrl-I/tab, command completion, needs to be before switch statement
    if (c == ctrlChar('I') && completionCallback) {
      if (pos == 0)  // SERVER-4967 -- in earlier versions, you could paste
                     // previous output
        continue;    //  back into the shell ... this output may have leading
                     //  tabs.
      // This hack (i.e. what the old code did) prevents command completion
      //  on an empty line but lets users paste text with leading tabs.

      killRing.lastAction = KillRing::actionOther;
      historyRecallMostRecent = false;

      // completeLine does the actual completion and replacement
      c = completeLine(pi);

      if (c < 0)  // return on error
        return len;

      if (c == 0)  // read next character when 0
        continue;

      // deliberate fall-through here, so we use the terminating character
    }

    switch (c) {
      case ctrlChar('A'):  // ctrl-A, move cursor to start of line
      case HOME_KEY:
        killRing.lastAction = KillRing::actionOther;
        pos = 0;
        refreshLine(pi);
        break;

      case ctrlChar('B'):  // ctrl-B, move cursor left by one character
      case LEFT_ARROW_KEY:
        killRing.lastAction = KillRing::actionOther;
        if (pos > 0) {
          --pos;
          refreshLine(pi);
        }
        break;

      case META + 'b':  // meta-B, move cursor left by one word
      case META + 'B':
      case CTRL + LEFT_ARROW_KEY:
      case META + LEFT_ARROW_KEY:  // Emacs allows Meta, bash & readline don't
        killRing.lastAction = KillRing::actionOther;
        if (pos > 0) {
          while (pos > 0 && !isCharacterAlphanumeric(buf32[pos - 1])) {
            --pos;
          }
          while (pos > 0 && isCharacterAlphanumeric(buf32[pos - 1])) {
            --pos;
          }
          refreshLine(pi);
        }
        break;

      case ctrlChar('C'):  // ctrl-C, abort this line
        killRing.lastAction = KillRing::actionOther;
        historyRecallMostRecent = false;
        errno = EAGAIN;
        --historyLen;
        free(history[historyLen]);
        // we need one last refresh with the cursor at the end of the line
        // so we don't display the next prompt over the previous input line
        pos = len;  // pass len as pos for EOL
        refreshLine(pi);
        if (write(1, "^C", 2) == -1) return -1;  // Display the ^C we got
        return -1;

      case META + 'c':  // meta-C, give word initial Cap
      case META + 'C':
        killRing.lastAction = KillRing::actionOther;
        historyRecallMostRecent = false;
        if (pos < len) {
          while (pos < len && !isCharacterAlphanumeric(buf32[pos])) {
            ++pos;
          }
          if (pos < len && isCharacterAlphanumeric(buf32[pos])) {
            if (buf32[pos] >= 'a' && buf32[pos] <= 'z') {
              buf32[pos] += 'A' - 'a';
            }
            ++pos;
          }
          while (pos < len && isCharacterAlphanumeric(buf32[pos])) {
            if (buf32[pos] >= 'A' && buf32[pos] <= 'Z') {
              buf32[pos] += 'a' - 'A';
            }
            ++pos;
          }
          refreshLine(pi);
        }
        break;

      // ctrl-D, delete the character under the cursor
      // on an empty line, exit the shell
      case ctrlChar('D'):
        killRing.lastAction = KillRing::actionOther;
        if (len > 0 && pos < len) {
          historyRecallMostRecent = false;
          memmove(buf32 + pos, buf32 + pos + 1, sizeof(char32_t) * (len - pos));
          --len;
          refreshLine(pi);
        } else if (len == 0) {
          --historyLen;
          free(history[historyLen]);
          return -1;
        }
        break;

      case META + 'd':  // meta-D, kill word to right of cursor
      case META + 'D':
        if (pos < len) {
          historyRecallMostRecent = false;
          int endingPos = pos;
          while (endingPos < len &&
                 !isCharacterAlphanumeric(buf32[endingPos])) {
            ++endingPos;
          }
          while (endingPos < len && isCharacterAlphanumeric(buf32[endingPos])) {
            ++endingPos;
          }
          killRing.kill(&buf32[pos], endingPos - pos, true);
          memmove(buf32 + pos, buf32 + endingPos,
                  sizeof(char32_t) * (len - endingPos + 1));
          len -= endingPos - pos;
          refreshLine(pi);
        }
        killRing.lastAction = KillRing::actionKill;
        break;

      case ctrlChar('E'):  // ctrl-E, move cursor to end of line
      case END_KEY:
        killRing.lastAction = KillRing::actionOther;
        pos = len;
        refreshLine(pi);
        break;

      case ctrlChar('F'):  // ctrl-F, move cursor right by one character
      case RIGHT_ARROW_KEY:
        killRing.lastAction = KillRing::actionOther;
        if (pos < len) {
          ++pos;
          refreshLine(pi);
        }
        break;

      case META + 'f':  // meta-F, move cursor right by one word
      case META + 'F':
      case CTRL + RIGHT_ARROW_KEY:
      case META + RIGHT_ARROW_KEY:  // Emacs allows Meta, bash & readline don't
        killRing.lastAction = KillRing::actionOther;
        if (pos < len) {
          while (pos < len && !isCharacterAlphanumeric(buf32[pos])) {
            ++pos;
          }
          while (pos < len && isCharacterAlphanumeric(buf32[pos])) {
            ++pos;
          }
          refreshLine(pi);
        }
        break;

      case ctrlChar('H'):  // backspace/ctrl-H, delete char to left of cursor
        killRing.lastAction = KillRing::actionOther;
        if (pos > 0) {
          historyRecallMostRecent = false;
          memmove(buf32 + pos - 1, buf32 + pos,
                  sizeof(char32_t) * (1 + len - pos));
          --pos;
          --len;
          refreshLine(pi);
        }
        break;

      // meta-Backspace, kill word to left of cursor
      case META + ctrlChar('H'):
        if (pos > 0) {
          historyRecallMostRecent = false;
          int startingPos = pos;
          while (pos > 0 && !isCharacterAlphanumeric(buf32[pos - 1])) {
            --pos;
          }
          while (pos > 0 && isCharacterAlphanumeric(buf32[pos - 1])) {
            --pos;
          }
          killRing.kill(&buf32[pos], startingPos - pos, false);
          memmove(buf32 + pos, buf32 + startingPos,
                  sizeof(char32_t) * (len - startingPos + 1));
          len -= startingPos - pos;
          refreshLine(pi);
        }
        killRing.lastAction = KillRing::actionKill;
        break;

      case ctrlChar('J'):  // ctrl-J/linefeed/newline, accept line
      case ctrlChar('M'):  // ctrl-M/return/enter
        killRing.lastAction = KillRing::actionOther;
        // we need one last refresh with the cursor at the end of the line
        // so we don't display the next prompt over the previous input line
        pos = len;  // pass len as pos for EOL
        refreshLine(pi);
        historyPreviousIndex = historyRecallMostRecent ? historyIndex : -2;
        --historyLen;
        free(history[historyLen]);
        return len;

      case ctrlChar('K'):  // ctrl-K, kill from cursor to end of line
        killRing.kill(&buf32[pos], len - pos, true);
        buf32[pos] = '\0';
        len = pos;
        refreshLine(pi);
        killRing.lastAction = KillRing::actionKill;
        historyRecallMostRecent = false;
        break;

      case ctrlChar('L'):  // ctrl-L, clear screen and redisplay line
        clearScreen(pi);
        break;

      case META + 'l':  // meta-L, lowercase word
      case META + 'L':
        killRing.lastAction = KillRing::actionOther;
        if (pos < len) {
          historyRecallMostRecent = false;
          while (pos < len && !isCharacterAlphanumeric(buf32[pos])) {
            ++pos;
          }
          while (pos < len && isCharacterAlphanumeric(buf32[pos])) {
            if (buf32[pos] >= 'A' && buf32[pos] <= 'Z') {
              buf32[pos] += 'a' - 'A';
            }
            ++pos;
          }
          refreshLine(pi);
        }
        break;

      case ctrlChar('N'):  // ctrl-N, recall next line in history
      case ctrlChar('P'):  // ctrl-P, recall previous line in history
      case DOWN_ARROW_KEY:
      case UP_ARROW_KEY:
        killRing.lastAction = KillRing::actionOther;
        // if not already recalling, add the current line to the history list so
        // we don't
        // have to special case it
        if (historyIndex == historyLen - 1) {
          free(history[historyLen - 1]);
          size_t tempBufferSize = sizeof(char32_t) * len + 1;
          unique_ptr<char[]> tempBuffer(new char[tempBufferSize]);
          copyString32to8(tempBuffer.get(), tempBufferSize, buf32);
          history[historyLen - 1] = strdup8(tempBuffer.get());
        }
        if (historyLen > 1) {
          if (c == UP_ARROW_KEY) {
            c = ctrlChar('P');
          }
          if (historyPreviousIndex != -2 && c != ctrlChar('P')) {
            historyIndex =
                1 + historyPreviousIndex;  // emulate Windows down-arrow
          } else {
            historyIndex += (c == ctrlChar('P')) ? -1 : 1;
          }
          historyPreviousIndex = -2;
          if (historyIndex < 0) {
            historyIndex = 0;
            break;
          } else if (historyIndex >= historyLen) {
            historyIndex = historyLen - 1;
            break;
          }
          historyRecallMostRecent = true;
          size_t ucharCount = 0;
          copyString8to32(buf32, buflen, ucharCount, history[historyIndex]);
          len = pos = static_cast<int>(ucharCount);
          refreshLine(pi);
        }
        break;

      case ctrlChar('R'):  // ctrl-R, reverse history search
      case ctrlChar('S'):  // ctrl-S, forward history search
        terminatingKeystroke = incrementalHistorySearch(pi, c);
        break;

      case ctrlChar('T'):  // ctrl-T, transpose characters
        killRing.lastAction = KillRing::actionOther;
        if (pos > 0 && len > 1) {
          historyRecallMostRecent = false;
          size_t leftCharPos = (pos == len) ? pos - 2 : pos - 1;
          char32_t aux = buf32[leftCharPos];
          buf32[leftCharPos] = buf32[leftCharPos + 1];
          buf32[leftCharPos + 1] = aux;
          if (pos != len) ++pos;
          refreshLine(pi);
        }
        break;

      case ctrlChar(
          'U'):  // ctrl-U, kill all characters to the left of the cursor
        if (pos > 0) {
          historyRecallMostRecent = false;
          killRing.kill(&buf32[0], pos, false);
          len -= pos;
          memmove(buf32, buf32 + pos, sizeof(char32_t) * (len + 1));
          pos = 0;
          refreshLine(pi);
        }
        killRing.lastAction = KillRing::actionKill;
        break;

      case META + 'u':  // meta-U, uppercase word
      case META + 'U':
        killRing.lastAction = KillRing::actionOther;
        if (pos < len) {
          historyRecallMostRecent = false;
          while (pos < len && !isCharacterAlphanumeric(buf32[pos])) {
            ++pos;
          }
          while (pos < len && isCharacterAlphanumeric(buf32[pos])) {
            if (buf32[pos] >= 'a' && buf32[pos] <= 'z') {
              buf32[pos] += 'A' - 'a';
            }
            ++pos;
          }
          refreshLine(pi);
        }
        break;

      // ctrl-W, kill to whitespace (not word) to left of cursor
      case ctrlChar('W'):
        if (pos > 0) {
          historyRecallMostRecent = false;
          int startingPos = pos;
          while (pos > 0 && buf32[pos - 1] == ' ') {
            --pos;
          }
          while (pos > 0 && buf32[pos - 1] != ' ') {
            --pos;
          }
          killRing.kill(&buf32[pos], startingPos - pos, false);
          memmove(buf32 + pos, buf32 + startingPos,
                  sizeof(char32_t) * (len - startingPos + 1));
          len -= startingPos - pos;
          refreshLine(pi);
        }
        killRing.lastAction = KillRing::actionKill;
        break;

      case ctrlChar('Y'):  // ctrl-Y, yank killed text
        historyRecallMostRecent = false;
        {
          Utf32String *restoredText = killRing.yank();
          if (restoredText) {
            bool truncated = false;
            size_t ucharCount = restoredText->length();
            if (ucharCount > static_cast<size_t>(buflen - len)) {
              ucharCount = buflen - len;
              truncated = true;
            }
            memmove(buf32 + pos + ucharCount, buf32 + pos,
                    sizeof(char32_t) * (len - pos + 1));
            memmove(buf32 + pos, restoredText->get(),
                    sizeof(char32_t) * ucharCount);
            pos += static_cast<int>(ucharCount);
            len += static_cast<int>(ucharCount);
            refreshLine(pi);
            killRing.lastAction = KillRing::actionYank;
            killRing.lastYankSize = ucharCount;
            if (truncated) {
              beep();
            }
          } else {
            beep();
          }
        }
        break;

      case META + 'y':  // meta-Y, "yank-pop", rotate popped text
      case META + 'Y':
        if (killRing.lastAction == KillRing::actionYank) {
          historyRecallMostRecent = false;
          Utf32String *restoredText = killRing.yankPop();
          if (restoredText) {
            bool truncated = false;
            size_t ucharCount = restoredText->length();
            if (ucharCount >
                static_cast<size_t>(killRing.lastYankSize + buflen - len)) {
              ucharCount = killRing.lastYankSize + buflen - len;
              truncated = true;
            }
            if (ucharCount > killRing.lastYankSize) {
              memmove(buf32 + pos + ucharCount - killRing.lastYankSize,
                      buf32 + pos, sizeof(char32_t) * (len - pos + 1));
              memmove(buf32 + pos - killRing.lastYankSize, restoredText->get(),
                      sizeof(char32_t) * ucharCount);
            } else {
              memmove(buf32 + pos - killRing.lastYankSize, restoredText->get(),
                      sizeof(char32_t) * ucharCount);
              memmove(buf32 + pos + ucharCount - killRing.lastYankSize,
                      buf32 + pos, sizeof(char32_t) * (len - pos + 1));
            }
            pos += static_cast<int>(ucharCount - killRing.lastYankSize);
            len += static_cast<int>(ucharCount - killRing.lastYankSize);
            killRing.lastYankSize = ucharCount;
            refreshLine(pi);
            if (truncated) {
              beep();
            }
            break;
          }
        }
        beep();
        break;

#ifndef _WIN32
      case ctrlChar('Z'):  // ctrl-Z, job control
        disableRawMode();  // Returning to Linux (whatever) shell, leave raw
                           // mode
        raise(SIGSTOP);    // Break out in mid-line
        enableRawMode();   // Back from Linux shell, re-enter raw mode
        if (!pi.write()) break;  // Redraw prompt
        refreshLine(pi);         // Refresh the line
        break;
#endif

      // DEL, delete the character under the cursor
      case 127:
      case DELETE_KEY:
        killRing.lastAction = KillRing::actionOther;
        if (len > 0 && pos < len) {
          historyRecallMostRecent = false;
          memmove(buf32 + pos, buf32 + pos + 1, sizeof(char32_t) * (len - pos));
          --len;
          refreshLine(pi);
        }
        break;

      case META + '<':     // meta-<, beginning of history
      case PAGE_UP_KEY:    // Page Up, beginning of history
      case META + '>':     // meta->, end of history
      case PAGE_DOWN_KEY:  // Page Down, end of history
        killRing.lastAction = KillRing::actionOther;
        // if not already recalling, add the current line to the history list so
        // we don't
        // have to special case it
        if (historyIndex == historyLen - 1) {
          free(history[historyLen - 1]);
          size_t tempBufferSize = sizeof(char32_t) * len + 1;
          unique_ptr<char[]> tempBuffer(new char[tempBufferSize]);
          copyString32to8(tempBuffer.get(), tempBufferSize, buf32);
          history[historyLen - 1] = strdup8(tempBuffer.get());
        }
        if (historyLen > 1) {
          historyIndex =
              (c == META + '<' || c == PAGE_UP_KEY) ? 0 : historyLen - 1;
          historyPreviousIndex = -2;
          historyRecallMostRecent = true;
          size_t ucharCount = 0;
          copyString8to32(buf32, buflen, ucharCount, history[historyIndex]);
          len = pos = static_cast<int>(ucharCount);
          refreshLine(pi);
        }
        break;

      // not one of our special characters, maybe insert it in the buffer
      default:
        killRing.lastAction = KillRing::actionOther;
        historyRecallMostRecent = false;
        if (c & (META | CTRL)) {  // beep on unknown Ctrl and/or Meta keys
          beep();
          break;
        }
        if (len < buflen) {
          if (isControlChar(c)) {  // don't insert control characters
            beep();
            break;
          }
          if (len == pos) {  // at end of buffer
            buf32[pos] = c;
            ++pos;
            ++len;
            buf32[len] = '\0';
            int inputLen = calculateColumnPosition(buf32, len);
            if (pi.promptIndentation + inputLen < pi.promptScreenColumns) {
              if (inputLen > pi.promptPreviousInputLen)
                pi.promptPreviousInputLen = inputLen;
              /* Avoid a full update of the line in the
               * trivial case. */
              if (write32(1, reinterpret_cast<char32_t *>(&c), 1) == -1)
                return -1;
            } else {
              refreshLine(pi);
            }
          } else {  // not at end of buffer, have to move characters to our
                    // right
            memmove(buf32 + pos + 1, buf32 + pos,
                    sizeof(char32_t) * (len - pos));
            buf32[pos] = c;
            ++len;
            ++pos;
            buf32[len] = '\0';
            refreshLine(pi);
          }
        } else {
          beep();  // buffer is full, beep on new characters
        }
        break;
    }
  }
  return len;
}