Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 36.4k
repl: add possibility to edit multiline commands while adding them#58003
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
puskin
wants to merge
2
commits into
nodejs:main
from
puskin:repl-multiline-refactor-and-while-typing
Uh oh!
There was an error while loading. Please reload this page.
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -64,6 +64,7 @@ const { | ||
| charLengthLeft, | ||
| commonPrefix, | ||
| kSubstringSearch, | ||
| reverseString, | ||
| } = require('internal/readline/utils'); | ||
| let emitKeypressEvents; | ||
| let kFirstEventParam; | ||
| @@ -98,9 +99,7 @@ const ESCAPE_CODE_TIMEOUT = 500; | ||
| // Max length of the kill ring | ||
| const kMaxLengthOfKillRing = 32; | ||
| // TODO(puskin94): make this configurable | ||
| const kMultilinePrompt = Symbol('| '); | ||
| const kLastCommandErrored = Symbol('_lastCommandErrored'); | ||
| const kAddHistory = Symbol('_addHistory'); | ||
| const kBeforeEdit = Symbol('_beforeEdit'); | ||
| @@ -131,6 +130,7 @@ const kPrompt = Symbol('_prompt'); | ||
| const kPushToKillRing = Symbol('_pushToKillRing'); | ||
| const kPushToUndoStack = Symbol('_pushToUndoStack'); | ||
| const kQuestionCallback = Symbol('_questionCallback'); | ||
| const kLastCommandErrored = Symbol('_lastCommandErrored'); | ||
| const kQuestionReject = Symbol('_questionReject'); | ||
| const kRedo = Symbol('_redo'); | ||
| const kRedoStack = Symbol('_redoStack'); | ||
| @@ -151,6 +151,14 @@ const kYank = Symbol('_yank'); | ||
| const kYanking = Symbol('_yanking'); | ||
| const kYankPop = Symbol('_yankPop'); | ||
| const kNormalizeHistoryLineEndings = Symbol('_normalizeHistoryLineEndings'); | ||
| const kSavePreviousState = Symbol('_savePreviousState'); | ||
| const kRestorePreviousState = Symbol('_restorePreviousState'); | ||
| const kPreviousLine = Symbol('_previousLine'); | ||
| const kPreviousCursor = Symbol('_previousCursor'); | ||
| const kPreviousCursorCols = Symbol('_previousCursorCols'); | ||
| const kMultilineMove = Symbol('_multilineMove'); | ||
| const kPreviousPrevRows = Symbol('_previousPrevRows'); | ||
| const kAddNewLineOnTTY = Symbol('_addNewLineOnTTY'); | ||
| function InterfaceConstructor(input, output, completer, terminal) { | ||
| this[kSawReturnAt] = 0; | ||
| @@ -239,6 +247,7 @@ function InterfaceConstructor(input, output, completer, terminal) { | ||
| this[kRedoStack] = []; | ||
| this.history = history; | ||
| this.historySize = historySize; | ||
| this[kPreviousCursorCols] = -1; | ||
| // The kill ring is a global list of blocks of text that were previously | ||
| // killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest | ||
| @@ -430,7 +439,7 @@ class Interface extends InterfaceConstructor { | ||
| } | ||
| } | ||
| [kSetLine](line) { | ||
| [kSetLine](line = '') { | ||
| this.line = line; | ||
| this[kIsMultiline] = StringPrototypeIncludes(line, '\n'); | ||
| } | ||
| @@ -477,10 +486,7 @@ class Interface extends InterfaceConstructor { | ||
| // Reversing the multilines is necessary when adding / editing and displaying them | ||
| if (reverse) { | ||
| // First reverse the lines for proper order, then convert separators | ||
| return ArrayPrototypeJoin( | ||
| ArrayPrototypeReverse(StringPrototypeSplit(line, from)), | ||
| to, | ||
| ); | ||
| return reverseString(line, from, to); | ||
| } | ||
| // For normal cases (saving to history or non-multiline entries) | ||
| return StringPrototypeReplaceAll(line, from, to); | ||
| @@ -494,22 +500,28 @@ class Interface extends InterfaceConstructor { | ||
| // If the trimmed line is empty then return the line | ||
| if (StringPrototypeTrim(this.line).length === 0) return this.line; | ||
| const normalizedLine = this[kNormalizeHistoryLineEndings](this.line, '\n', '\r', false); | ||
| // This is necessary because each line would be saved in the history while creating | ||
| // A new multiline, and we don't want that. | ||
| if (this[kIsMultiline] && this.historyIndex === -1) { | ||
| ArrayPrototypeShift(this.history); | ||
| } else if (this[kLastCommandErrored]) { | ||
| // If the last command errored and we are trying to edit the history to fix it | ||
| // Remove the broken one from the history | ||
| ArrayPrototypeShift(this.history); | ||
| } | ||
| const normalizedLine = this[kNormalizeHistoryLineEndings](this.line, '\n', '\r', true); | ||
| if (this.history.length === 0 || this.history[0] !== normalizedLine) { | ||
| if (this[kLastCommandErrored] && this.historyIndex === 0) { | ||
| // If the last command errored, remove it from history. | ||
| // The user is issuing a new command starting from the errored command, | ||
| // Hopefully with the fix | ||
| ArrayPrototypeShift(this.history); | ||
| } | ||
| if (this.removeHistoryDuplicates) { | ||
| // Remove older history line if identical to new one | ||
| const dupIndex = ArrayPrototypeIndexOf(this.history, this.line); | ||
| if (dupIndex !== -1) ArrayPrototypeSplice(this.history, dupIndex, 1); | ||
| } | ||
| ArrayPrototypeUnshift(this.history, this.line); | ||
| // Add the new line to the history | ||
| ArrayPrototypeUnshift(this.history, normalizedLine); | ||
| // Only store so many | ||
| if (this.history.length > this.historySize) | ||
| @@ -521,7 +533,7 @@ class Interface extends InterfaceConstructor { | ||
| // The listener could change the history object, possibly | ||
| // to remove the last added entry if it is sensitive and should | ||
| // not be persisted in the history, like a password | ||
| const line = this.history[0]; | ||
| const line = this[kIsMultiline] ? reverseString(this.history[0]) : this.history[0]; | ||
| // Emit history event to notify listeners of update | ||
| this.emit('history', this.history); | ||
| @@ -938,6 +950,18 @@ class Interface extends InterfaceConstructor { | ||
| } | ||
| } | ||
| [kSavePreviousState]() { | ||
| this[kPreviousLine] = this.line; | ||
| this[kPreviousCursor] = this.cursor; | ||
| this[kPreviousPrevRows] = this.prevRows; | ||
| } | ||
| [kRestorePreviousState]() { | ||
| this[kSetLine](this[kPreviousLine]); | ||
| this.cursor = this[kPreviousCursor]; | ||
| this.prevRows = this[kPreviousPrevRows]; | ||
| } | ||
| clearLine() { | ||
| this[kMoveCursor](+Infinity); | ||
| this[kWriteToOutput]('\r\n'); | ||
| @@ -947,13 +971,115 @@ class Interface extends InterfaceConstructor { | ||
| } | ||
| [kLine]() { | ||
| this[kSavePreviousState](); | ||
| const line = this[kAddHistory](); | ||
| this[kUndoStack] = []; | ||
| this[kRedoStack] = []; | ||
| this.clearLine(); | ||
| this[kOnLine](line); | ||
| } | ||
| // TODO(puskin94): edit [kTtyWrite] to make call this function on a new key combination | ||
| // to make it add a new line in the middle of a "complete" multiline. | ||
| // I tried with shift + enter but it is not detected. Find a new one. | ||
| // Make sure to call this[kSavePreviousState](); && this.clearLine(); | ||
| // before calling this[kAddNewLineOnTTY] to simulate what [kLine] is doing. | ||
| // When this function is called, the actual cursor is at the very end of the whole string, | ||
| // No matter where the new line was entered. | ||
| // This function should only be used when the output is a TTY | ||
| [kAddNewLineOnTTY]() { | ||
| // Restore terminal state and store current line | ||
| this[kRestorePreviousState](); | ||
| const originalLine = this.line; | ||
| // Split the line at the current cursor position | ||
| const beforeCursor = StringPrototypeSlice(this.line, 0, this.cursor); | ||
| let afterCursor = StringPrototypeSlice(this.line, this.cursor, this.line.length); | ||
| // Add the new line where the cursor is at | ||
| this[kSetLine](`${beforeCursor}\n${afterCursor}`); | ||
| // To account for the new line | ||
| this.cursor += 1; | ||
| const hasContentAfterCursor = afterCursor.length > 0; | ||
| const cursorIsNotOnFirstLine = this.prevRows > 0; | ||
| let needsRewriteFirstLine = false; | ||
| // Handle cursor positioning based on different scenarios | ||
| if (hasContentAfterCursor) { | ||
| const splitBeg = StringPrototypeSplit(beforeCursor, '\n'); | ||
| // Determine if we need to rewrite the first line | ||
| needsRewriteFirstLine = splitBeg.length < 2; | ||
| // If the cursor is not on the first line | ||
| if (cursorIsNotOnFirstLine) { | ||
| const splitEnd = StringPrototypeSplit(afterCursor, '\n'); | ||
| // If the cursor when I pressed enter was at least on the second line | ||
| // I need to completely erase the line where the cursor was pressed because it is possible | ||
| // That it was pressed in the middle of the line, hence I need to write the whole line. | ||
BridgeAR marked this conversation as resolved.
Outdated
Uh oh!There was an error while loading. Please reload this page. | ||
| // To achieve that, I need to reach the line above the current line coming from the end | ||
| const dy = splitEnd.length + 1; | ||
| // Calculate how many Xs we need to move on the right to get to the end of the line | ||
| const dxEndOfLineAbove = (splitBeg[splitBeg.length - 2] || '').length + kMultilinePrompt.description.length; | ||
| moveCursor(this.output, dxEndOfLineAbove, -dy); | ||
| // This is the line that was split in the middle | ||
| // Just add it to the rest of the line that will be printed later | ||
| afterCursor = `${splitBeg[splitBeg.length - 1]}\n${afterCursor}`; | ||
| } else { | ||
| // Otherwise, go to the very beginning of the first line and erase everything | ||
| const dy = StringPrototypeSplit(originalLine, '\n').length; | ||
| moveCursor(this.output, 0, -dy); | ||
| } | ||
| // Erase from the cursor to the end of the line | ||
| clearScreenDown(this.output); | ||
| if (cursorIsNotOnFirstLine) { | ||
| this[kWriteToOutput]('\n'); | ||
| } | ||
| } | ||
| if (needsRewriteFirstLine) { | ||
| this[kWriteToOutput](`${this[kPrompt]}${beforeCursor}\n${kMultilinePrompt.description}`); | ||
| } else { | ||
| this[kWriteToOutput](kMultilinePrompt.description); | ||
| } | ||
| // Write the rest and restore the cursor to where the user left it | ||
| if (hasContentAfterCursor) { | ||
| // Save the cursor pos, we need to come back here | ||
| const oldCursor = this.getCursorPos(); | ||
| // Write everything after the cursor which has been deleted by clearScreenDown | ||
| const formattedEndContent = StringPrototypeReplaceAll( | ||
| afterCursor, | ||
| '\n', | ||
| `\n${kMultilinePrompt.description}`, | ||
| ); | ||
| this[kWriteToOutput](formattedEndContent); | ||
| const newCursor = this[kGetDisplayPos](this.line); | ||
| // Go back to where the cursor was, with relative movement | ||
| moveCursor(this.output, oldCursor.cols - newCursor.cols, oldCursor.rows - newCursor.rows); | ||
| // Setting how many rows we have on top of the cursor | ||
| // Necessary for kRefreshLine | ||
| this.prevRows = oldCursor.rows; | ||
| } else { | ||
| // Setting how many rows we have on top of the cursor | ||
| // Necessary for kRefreshLine | ||
| this.prevRows = StringPrototypeSplit(this.line, '\n').length - 1; | ||
| } | ||
| } | ||
| [kPushToUndoStack](text, cursor) { | ||
| if (ArrayPrototypePush(this[kUndoStack], { text, cursor }) > | ||
| kMaxUndoRedoStackSize) { | ||
| @@ -991,27 +1117,50 @@ class Interface extends InterfaceConstructor { | ||
| this[kRefreshLine](); | ||
| } | ||
| [kMoveDownOrHistoryNext]() { | ||
| const { cols, rows } = this.getCursorPos(); | ||
| const splitLine = StringPrototypeSplit(this.line, '\n'); | ||
| if (!this.historyIndex && rows === splitLine.length) { | ||
| return; | ||
| [kMultilineMove](direction, splitLines, { rows, cols }) { | ||
| const curr = splitLines[rows]; | ||
| const down = direction === 1; | ||
| const adj = splitLines[rows + direction]; | ||
| const promptLen = kMultilinePrompt.description.length; | ||
| let amountToMove; | ||
| // Clamp distance to end of current + prompt + next/prev line + newline | ||
| const clamp = down ? | ||
| curr.length - cols + promptLen + adj.length + 1 : | ||
| -cols + 1; | ||
| const shouldClamp = cols > adj.length + 1; | ||
| if (shouldClamp) { | ||
| if (this[kPreviousCursorCols] === -1) { | ||
| this[kPreviousCursorCols] = cols; | ||
| } | ||
| amountToMove = clamp; | ||
| } else { | ||
| if (down) { | ||
| amountToMove = curr.length + 1; | ||
| } else { | ||
| amountToMove = -adj.length - 1; | ||
| } | ||
| if (this[kPreviousCursorCols] !== -1) { | ||
| if (this[kPreviousCursorCols] <= adj.length) { | ||
| amountToMove += this[kPreviousCursorCols] - cols; | ||
| this[kPreviousCursorCols] = -1; | ||
| } else { | ||
| amountToMove = clamp; | ||
| } | ||
| } | ||
| } | ||
| // Go to the next history only if the cursor is in the first line of the multiline input. | ||
| // Otherwise treat the "arrow down" as a movement to the next row. | ||
| if (this[kIsMultiline] && rows < splitLine.length - 1) { | ||
| const currentLine = splitLine[rows]; | ||
| const nextLine = splitLine[rows + 1]; | ||
| // If I am moving down and the current line is longer than the next line | ||
| const amountToMove = (cols > nextLine.length + 1) ? | ||
| currentLine.length - cols + nextLine.length + | ||
| kMultilinePrompt.description.length + 1 : // Move to the end of the current line | ||
| // + chars to account for the kMultilinePrompt prefix, + 1 to go to the first char | ||
| currentLine.length + 1; // Otherwise just move to the next line, in the same position | ||
| this[kMoveCursor](amountToMove); | ||
| this[kMoveCursor](amountToMove); | ||
| } | ||
| [kMoveDownOrHistoryNext]() { | ||
| const cursorPos = this.getCursorPos(); | ||
| const splitLines = StringPrototypeSplit(this.line, '\n'); | ||
| if (this[kIsMultiline] && cursorPos.rows < splitLines.length - 1) { | ||
| this[kMultilineMove](1, splitLines, cursorPos); | ||
| return; | ||
| } | ||
| this[kPreviousCursorCols] = -1; | ||
| this[kHistoryNext](); | ||
| } | ||
| @@ -1046,23 +1195,13 @@ class Interface extends InterfaceConstructor { | ||
| } | ||
| [kMoveUpOrHistoryPrev]() { | ||
| const { cols, rows } = this.getCursorPos(); | ||
| if (this.historyIndex === this.history.length && rows) { | ||
| return; | ||
| } | ||
| // Go to the previous history only if the cursor is in the first line of the multiline input. | ||
| // Otherwise treat the "arrow up" as a movement to the previous row. | ||
| if (this[kIsMultiline] && rows > 0) { | ||
| const splitLine = StringPrototypeSplit(this.line, '\n'); | ||
| const previousLine = splitLine[rows - 1]; | ||
| // If I am moving up and the current line is longer than the previous line | ||
| const amountToMove = (cols > previousLine.length + 1) ? | ||
| -cols + 1 : // Move to the beginning of the current line + 1 char to go to the end of the previous line | ||
| -previousLine.length - 1; // Otherwise just move to the previous line, in the same position | ||
| this[kMoveCursor](amountToMove); | ||
| const cursorPos = this.getCursorPos(); | ||
| if (this[kIsMultiline] && cursorPos.rows > 0) { | ||
| const splitLines = StringPrototypeSplit(this.line, '\n'); | ||
| this[kMultilineMove](-1, splitLines, cursorPos); | ||
| return; | ||
| } | ||
| this[kPreviousCursorCols] = -1; | ||
| this[kHistoryPrev](); | ||
| } | ||
| @@ -1173,6 +1312,7 @@ class Interface extends InterfaceConstructor { | ||
| const previousKey = this[kPreviousKey]; | ||
| key ||= kEmptyObject; | ||
| this[kPreviousKey] = key; | ||
| let shouldResetPreviousCursorCols = true; | ||
| if (!key.meta || key.name !== 'y') { | ||
| // Reset yanking state unless we are doing yank pop. | ||
| @@ -1420,10 +1560,12 @@ class Interface extends InterfaceConstructor { | ||
| break; | ||
| case 'up': | ||
| shouldResetPreviousCursorCols = false; | ||
| this[kMoveUpOrHistoryPrev](); | ||
| break; | ||
| case 'down': | ||
| shouldResetPreviousCursorCols = false; | ||
| this[kMoveDownOrHistoryNext](); | ||
| break; | ||
| @@ -1459,6 +1601,9 @@ class Interface extends InterfaceConstructor { | ||
| } | ||
| } | ||
| } | ||
| if (shouldResetPreviousCursorCols) { | ||
| this[kPreviousCursorCols] = -1; | ||
| } | ||
| } | ||
| /** | ||
| @@ -1525,6 +1670,7 @@ module.exports = { | ||
| kWordRight, | ||
| kWriteToOutput, | ||
| kMultilinePrompt, | ||
| kRestorePreviousState, | ||
| kAddNewLineOnTTY, | ||
| kLastCommandErrored, | ||
| kNormalizeHistoryLineEndings, | ||
| }; | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.