@@ -155,6 +155,8 @@ const kSavePreviousState = Symbol('_savePreviousState'); |
155 | 155 | const kRestorePreviousState = Symbol('_restorePreviousState'); |
156 | 156 | const kPreviousLine = Symbol('_previousLine'); |
157 | 157 | const kPreviousCursor = Symbol('_previousCursor'); |
| 158 | +const kPreviousCursorCols = Symbol('_previousCursorCols'); |
| 159 | +const kMultilineMove = Symbol('_multilineMove'); |
158 | 160 | const kPreviousPrevRows = Symbol('_previousPrevRows'); |
159 | 161 | const kAddNewLineOnTTY = Symbol('_addNewLineOnTTY'); |
160 | 162 |
|
@@ -245,6 +247,7 @@ function InterfaceConstructor(input, output, completer, terminal) { |
245 | 247 | this[kRedoStack] = []; |
246 | 248 | this.history = history; |
247 | 249 | this.historySize = historySize; |
| 250 | + this[kPreviousCursorCols] = -1; |
248 | 251 |
|
249 | 252 | // The kill ring is a global list of blocks of text that were previously |
250 | 253 | // killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest |
@@ -1114,27 +1117,50 @@ class Interface extends InterfaceConstructor { |
1114 | 1117 | this[kRefreshLine](); |
1115 | 1118 | } |
1116 | 1119 |
|
1117 | | - [kMoveDownOrHistoryNext]() { |
1118 | | - const { cols, rows } = this.getCursorPos(); |
1119 | | - const splitLine = StringPrototypeSplit(this.line, '\n'); |
1120 | | - if (!this.historyIndex && rows === splitLine.length) { |
1121 | | - return; |
| 1120 | + [kMultilineMove](direction, splitLines, { rows, cols }) { |
| 1121 | + const curr = splitLines[rows]; |
| 1122 | + const down = direction === 1; |
| 1123 | + const adj = splitLines[rows + direction]; |
| 1124 | + const promptLen = kMultilinePrompt.description.length; |
| 1125 | + let amountToMove; |
| 1126 | + // Clamp distance to end of current + prompt + next/prev line + newline |
| 1127 | + const clamp = down ? |
| 1128 | + curr.length - cols + promptLen + adj.length + 1 : |
| 1129 | + -cols + 1; |
| 1130 | + const shouldClamp = cols > adj.length + 1; |
| 1131 | + |
| 1132 | + if (shouldClamp) { |
| 1133 | + if (this[kPreviousCursorCols] === -1) { |
| 1134 | + this[kPreviousCursorCols] = cols; |
| 1135 | + } |
| 1136 | + amountToMove = clamp; |
| 1137 | + } else { |
| 1138 | + if (down) { |
| 1139 | + amountToMove = curr.length + 1; |
| 1140 | + } else { |
| 1141 | + amountToMove = -adj.length - 1; |
| 1142 | + } |
| 1143 | + if (this[kPreviousCursorCols] !== -1) { |
| 1144 | + if (this[kPreviousCursorCols] <= adj.length) { |
| 1145 | + amountToMove += this[kPreviousCursorCols] - cols; |
| 1146 | + this[kPreviousCursorCols] = -1; |
| 1147 | + } else { |
| 1148 | + amountToMove = clamp; |
| 1149 | + } |
| 1150 | + } |
1122 | 1151 | } |
1123 | | - // Go to the next history only if the cursor is in the first line of the multiline input. |
1124 | | - // Otherwise treat the "arrow down" as a movement to the next row. |
1125 | | - if (this[kIsMultiline] && rows < splitLine.length - 1) { |
1126 | | - const currentLine = splitLine[rows]; |
1127 | | - const nextLine = splitLine[rows + 1]; |
1128 | | - // If I am moving down and the current line is longer than the next line |
1129 | | - const amountToMove = (cols > nextLine.length + 1) ? |
1130 | | - currentLine.length - cols + nextLine.length + |
1131 | | - kMultilinePrompt.description.length + 1 : // Move to the end of the current line |
1132 | | - // + chars to account for the kMultilinePrompt prefix, + 1 to go to the first char |
1133 | | - currentLine.length + 1; // Otherwise just move to the next line, in the same position |
1134 | | - this[kMoveCursor](amountToMove); |
| 1152 | + |
| 1153 | + this[kMoveCursor](amountToMove); |
| 1154 | + } |
| 1155 | + |
| 1156 | + [kMoveDownOrHistoryNext]() { |
| 1157 | + const cursorPos = this.getCursorPos(); |
| 1158 | + const splitLines = StringPrototypeSplit(this.line, '\n'); |
| 1159 | + if (this[kIsMultiline] && cursorPos.rows < splitLines.length - 1) { |
| 1160 | + this[kMultilineMove](1, splitLines, cursorPos); |
1135 | 1161 | return; |
1136 | 1162 | } |
1137 | | - |
| 1163 | + this[kPreviousCursorCols] = -1; |
1138 | 1164 | this[kHistoryNext](); |
1139 | 1165 | } |
1140 | 1166 |
|
@@ -1169,23 +1195,13 @@ class Interface extends InterfaceConstructor { |
1169 | 1195 | } |
1170 | 1196 |
|
1171 | 1197 | [kMoveUpOrHistoryPrev]() { |
1172 | | - const { cols, rows } = this.getCursorPos(); |
1173 | | - if (this.historyIndex === this.history.length && rows) { |
1174 | | - return; |
1175 | | - } |
1176 | | - // Go to the previous history only if the cursor is in the first line of the multiline input. |
1177 | | - // Otherwise treat the "arrow up" as a movement to the previous row. |
1178 | | - if (this[kIsMultiline] && rows > 0) { |
1179 | | - const splitLine = StringPrototypeSplit(this.line, '\n'); |
1180 | | - const previousLine = splitLine[rows - 1]; |
1181 | | - // If I am moving up and the current line is longer than the previous line |
1182 | | - const amountToMove = (cols > previousLine.length + 1) ? |
1183 | | - -cols + 1 : // Move to the beginning of the current line + 1 char to go to the end of the previous line |
1184 | | - -previousLine.length - 1; // Otherwise just move to the previous line, in the same position |
1185 | | - this[kMoveCursor](amountToMove); |
| 1198 | + const cursorPos = this.getCursorPos(); |
| 1199 | + if (this[kIsMultiline] && cursorPos.rows > 0) { |
| 1200 | + const splitLines = StringPrototypeSplit(this.line, '\n'); |
| 1201 | + this[kMultilineMove](-1, splitLines, cursorPos); |
1186 | 1202 | return; |
1187 | 1203 | } |
1188 | | - |
| 1204 | + this[kPreviousCursorCols] = -1; |
1189 | 1205 | this[kHistoryPrev](); |
1190 | 1206 | } |
1191 | 1207 |
|
@@ -1296,6 +1312,7 @@ class Interface extends InterfaceConstructor { |
1296 | 1312 | const previousKey = this[kPreviousKey]; |
1297 | 1313 | key ||= kEmptyObject; |
1298 | 1314 | this[kPreviousKey] = key; |
| 1315 | + let shouldResetPreviousCursorCols = true; |
1299 | 1316 |
|
1300 | 1317 | if (!key.meta || key.name !== 'y') { |
1301 | 1318 | // Reset yanking state unless we are doing yank pop. |
@@ -1543,10 +1560,12 @@ class Interface extends InterfaceConstructor { |
1543 | 1560 | break; |
1544 | 1561 |
|
1545 | 1562 | case 'up': |
| 1563 | + shouldResetPreviousCursorCols = false; |
1546 | 1564 | this[kMoveUpOrHistoryPrev](); |
1547 | 1565 | break; |
1548 | 1566 |
|
1549 | 1567 | case 'down': |
| 1568 | + shouldResetPreviousCursorCols = false; |
1550 | 1569 | this[kMoveDownOrHistoryNext](); |
1551 | 1570 | break; |
1552 | 1571 |
|
@@ -1582,6 +1601,9 @@ class Interface extends InterfaceConstructor { |
1582 | 1601 | } |
1583 | 1602 | } |
1584 | 1603 | } |
| 1604 | + if (shouldResetPreviousCursorCols) { |
| 1605 | + this[kPreviousCursorCols] = -1; |
| 1606 | + } |
1585 | 1607 | } |
1586 | 1608 |
|
1587 | 1609 | /** |
|