mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
repl: add possibility to edit multiline commands while adding them
PR-URL: https://github.com/nodejs/node/pull/58003 Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
This commit is contained in:
parent
5fb879c458
commit
96be7836d7
7 changed files with 507 additions and 54 deletions
|
@ -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,12 @@ 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 kPreviousPrevRows = Symbol('_previousPrevRows');
|
||||
const kAddNewLineOnTTY = Symbol('_addNewLineOnTTY');
|
||||
|
||||
function InterfaceConstructor(input, output, completer, terminal) {
|
||||
this[kSawReturnAt] = 0;
|
||||
|
@ -430,7 +436,7 @@ class Interface extends InterfaceConstructor {
|
|||
}
|
||||
}
|
||||
|
||||
[kSetLine](line) {
|
||||
[kSetLine](line = '') {
|
||||
this.line = line;
|
||||
this[kIsMultiline] = StringPrototypeIncludes(line, '\n');
|
||||
}
|
||||
|
@ -477,10 +483,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 +497,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 +530,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 +947,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,6 +968,7 @@ class Interface extends InterfaceConstructor {
|
|||
}
|
||||
|
||||
[kLine]() {
|
||||
this[kSavePreviousState]();
|
||||
const line = this[kAddHistory]();
|
||||
this[kUndoStack] = [];
|
||||
this[kRedoStack] = [];
|
||||
|
@ -954,6 +976,107 @@ class Interface extends InterfaceConstructor {
|
|||
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.
|
||||
// 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) {
|
||||
|
@ -1525,6 +1648,7 @@ module.exports = {
|
|||
kWordRight,
|
||||
kWriteToOutput,
|
||||
kMultilinePrompt,
|
||||
kRestorePreviousState,
|
||||
kAddNewLineOnTTY,
|
||||
kLastCommandErrored,
|
||||
kNormalizeHistoryLineEndings,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue