@@ -17,9 +17,10 @@ const { |
17 | 17 | MathMax, |
18 | 18 | MathMaxApply, |
19 | 19 | NumberIsFinite, |
20 | | - ObjectDefineProperty, |
| 20 | + ObjectDefineProperties, |
21 | 21 | ObjectSetPrototypeOf, |
22 | 22 | RegExpPrototypeExec, |
| 23 | + RegExpPrototypeSymbolSplit, |
23 | 24 | SafeStringIterator, |
24 | 25 | StringPrototypeCodePointAt, |
25 | 26 | StringPrototypeEndsWith, |
@@ -172,6 +173,7 @@ function InterfaceConstructor(input, output, completer, terminal) { |
172 | 173 | let crlfDelay; |
173 | 174 | let prompt = '> '; |
174 | 175 | let signal; |
| 176 | + let historyOptions; |
175 | 177 |
|
176 | 178 | if (input?.input) { |
177 | 179 | // An options object was given |
@@ -210,12 +212,15 @@ function InterfaceConstructor(input, output, completer, terminal) { |
210 | 212 | crlfDelay = input.crlfDelay; |
211 | 213 | input = input.input; |
212 | 214 |
|
213 | | - input.size = historySize; |
214 | | - input.history = history; |
215 | | - input.removeHistoryDuplicates = removeHistoryDuplicates; |
| 215 | + historyOptions = { |
| 216 | + __proto__: null, |
| 217 | + size: historySize, |
| 218 | + history, |
| 219 | + removeHistoryDuplicates, |
| 220 | + }; |
216 | 221 | } |
217 | 222 |
|
218 | | - this.setupHistoryManager(input); |
| 223 | + this.setupHistoryManager(historyOptions ?? input); |
219 | 224 |
|
220 | 225 | if (completer !== undefined && typeof completer !== 'function') { |
221 | 226 | throw new ERR_INVALID_ARG_VALUE('completer', completer); |
@@ -358,6 +363,30 @@ function InterfaceConstructor(input, output, completer, terminal) { |
358 | 363 | ObjectSetPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype); |
359 | 364 | ObjectSetPrototypeOf(InterfaceConstructor, EventEmitter); |
360 | 365 |
|
| 366 | +// Shared descriptors for the history accessors defined on each instance. |
| 367 | +// Hoisted to avoid allocating fresh closures on every construction. |
| 368 | +const kHistoryAccessorDescriptors = { |
| 369 | + __proto__: null, |
| 370 | + history: { |
| 371 | + __proto__: null, configurable: true, enumerable: true, |
| 372 | + get() { return this.historyManager.history; }, |
| 373 | + set(newHistory) { return this.historyManager.history = newHistory; }, |
| 374 | + }, |
| 375 | + historyIndex: { |
| 376 | + __proto__: null, configurable: true, enumerable: true, |
| 377 | + get() { return this.historyManager.index; }, |
| 378 | + set(historyIndex) { return this.historyManager.index = historyIndex; }, |
| 379 | + }, |
| 380 | + historySize: { |
| 381 | + __proto__: null, configurable: true, enumerable: true, |
| 382 | + get() { return this.historyManager.size; }, |
| 383 | + }, |
| 384 | + isFlushing: { |
| 385 | + __proto__: null, configurable: true, enumerable: true, |
| 386 | + get() { return this.historyManager.isFlushing; }, |
| 387 | + }, |
| 388 | +}; |
| 389 | + |
361 | 390 | class Interface extends InterfaceConstructor { |
362 | 391 | get columns() { |
363 | 392 | if (this.output?.columns) return this.output.columns; |
@@ -388,27 +417,7 @@ class Interface extends InterfaceConstructor { |
388 | 417 | this.historyManager.initialize(options.onHistoryFileLoaded); |
389 | 418 | } |
390 | 419 |
|
391 | | - ObjectDefineProperty(this, 'history', { |
392 | | - __proto__: null, configurable: true, enumerable: true, |
393 | | - get() { return this.historyManager.history; }, |
394 | | - set(newHistory) { return this.historyManager.history = newHistory; }, |
395 | | - }); |
396 | | - |
397 | | - ObjectDefineProperty(this, 'historyIndex', { |
398 | | - __proto__: null, configurable: true, enumerable: true, |
399 | | - get() { return this.historyManager.index; }, |
400 | | - set(historyIndex) { return this.historyManager.index = historyIndex; }, |
401 | | - }); |
402 | | - |
403 | | - ObjectDefineProperty(this, 'historySize', { |
404 | | - __proto__: null, configurable: true, enumerable: true, |
405 | | - get() { return this.historyManager.size; }, |
406 | | - }); |
407 | | - |
408 | | - ObjectDefineProperty(this, 'isFlushing', { |
409 | | - __proto__: null, configurable: true, enumerable: true, |
410 | | - get() { return this.historyManager.isFlushing; }, |
411 | | - }); |
| 420 | + ObjectDefineProperties(this, kHistoryAccessorDescriptors); |
412 | 421 | } |
413 | 422 |
|
414 | 423 | [kSetRawMode](mode) { |
@@ -622,37 +631,44 @@ class Interface extends InterfaceConstructor { |
622 | 631 | this[kSawReturnAt] = 0; |
623 | 632 | } |
624 | 633 |
|
625 | | - // Run test() on the new string chunk, not on the entire line buffer. |
626 | | - let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string); |
627 | | - if (newPartContainsEnding !== null) { |
628 | | - if (this[kLine_buffer]) { |
629 | | - string = this[kLine_buffer] + string; |
630 | | - this[kLine_buffer] = null; |
631 | | - lineEnding.lastIndex = 0; // Start the search from the beginning of the string. |
632 | | - newPartContainsEnding = RegExpPrototypeExec(lineEnding, string); |
633 | | - } |
634 | | - this[kSawReturnAt] = StringPrototypeEndsWith(string, '\r') ? |
635 | | - DateNow() : |
636 | | - 0; |
637 | | - |
638 | | - const indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex]; |
639 | | - let nextMatch; |
640 | | - while ((nextMatch = RegExpPrototypeExec(lineEnding, string)) !== null) { |
641 | | - ArrayPrototypePush(indexes, nextMatch.index, lineEnding.lastIndex); |
642 | | - } |
643 | | - const lastIndex = indexes.length - 1; |
644 | | - // Either '' or (conceivably) the unfinished portion of the next line |
645 | | - this[kLine_buffer] = StringPrototypeSlice(string, indexes[lastIndex]); |
646 | | - for (let i = 1; i < lastIndex; i += 2) { |
647 | | - this[kOnLine](StringPrototypeSlice(string, indexes[i - 1], indexes[i])); |
648 | | - } |
649 | | - } else if (string) { |
650 | | - // No newlines this time, save what we have for next time |
| 634 | + if (!string) { |
| 635 | + return; |
| 636 | + } |
| 637 | + |
| 638 | + // Split the new string chunk, not the entire line buffer: a single |
| 639 | + // split pass avoids allocating a match object per line ending. |
| 640 | + // When the chunk contains none of the rare line endings, a plain |
| 641 | + // string split is much cheaper than the regular expression. |
| 642 | + const lines = |
| 643 | + StringPrototypeIncludes(string, '\r') || |
| 644 | + StringPrototypeIncludes(string, '\u2028') || |
| 645 | + StringPrototypeIncludes(string, '\u2029') ? |
| 646 | + RegExpPrototypeSymbolSplit(lineEnding, string) : |
| 647 | + StringPrototypeSplit(string, '\n'); |
| 648 | + const lastIndex = lines.length - 1; |
| 649 | + if (lastIndex === 0) { |
| 650 | + // No line endings this time, save what we have for next time. |
651 | 651 | if (this[kLine_buffer]) { |
652 | 652 | this[kLine_buffer] += string; |
653 | 653 | } else { |
654 | 654 | this[kLine_buffer] = string; |
655 | 655 | } |
| 656 | + return; |
| 657 | + } |
| 658 | + |
| 659 | + this[kSawReturnAt] = StringPrototypeEndsWith(string, '\r') ? |
| 660 | + DateNow() : |
| 661 | + 0; |
| 662 | + |
| 663 | + let first = lines[0]; |
| 664 | + if (this[kLine_buffer]) { |
| 665 | + first = this[kLine_buffer] + first; |
| 666 | + } |
| 667 | + // Either '' or (conceivably) the unfinished portion of the next line |
| 668 | + this[kLine_buffer] = lines[lastIndex]; |
| 669 | + this[kOnLine](first); |
| 670 | + for (let i = 1; i < lastIndex; i++) { |
| 671 | + this[kOnLine](lines[i]); |
656 | 672 | } |
657 | 673 | } |
658 | 674 |
|
|