-
Notifications
You must be signed in to change notification settings - Fork 30.3k
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
readline: add reverse search #24335
readline: add reverse search #24335
Conversation
5d0498e
to
10579f5
Compare
Not sure I understand what this does, could you elaborate? |
@Fishrock123 Oh, I'm sorry, should have given it in PR description, but didn't. Modified now. |
@nodejs/repl |
This sounds fine to me if it is fully implemented. @antsmartian Did you need anything to progress here? |
@Fishrock123 Not really. I just wanted repl team & community confirmation on this idea, before I can go ahead with few changes and add test cases to land this. Thanks for your time on this. |
5a61791
to
35ab75c
Compare
@nodejs/repl @Fishrock123 PTAL.. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great that you implement this! It is something I am definitely looking forward to!
I personally would change the output to the same as it's used in bash:
(reverse-i-search)'$input': $match
and (failed reverse-i-search)'input': $lastMatch
.
That way the $match is not duplicated.
this.reverseSearchPrompt = "(reverse-i-search)`' "; | ||
this.inReverseSearch = false; | ||
this.reverseSearchResults = []; | ||
this.reverseSearchIndex = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be wrong but I believe it is possible to combine reverseSearchIndex
and inReverseSearch
by setting reverseSearchIndex
to -2
when not searching and -1 when nothing is found in reverseSearch.
Thanks @BridgeAR. Will take a look at your comments. One quick question, do you think this will be valuable for the user:
I guess it can slightly confuse the user if we show |
@antsmartian I just suggested to mirror the way it's known to me. Showing $lastMatch is definitely not really helpful and just showing |
35ab75c
to
e7f7afd
Compare
@BridgeAR @nodejs/repl PTAL.. |
Gentle remainder.. cc @nodejs/repl @BridgeAR |
@BridgeAR Thanks for your review. Will have a look at it and close this. Regarding the color check, are there any existing function that we use for repl? I tried searching the codebase, but no luck till now. |
e7f7afd
to
bdb624c
Compare
bdb624c
to
75d46ab
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for following up on the comments! It's on a good way but it needs some further polishing.
Since this feature is mainly a readline feature and not a repl feature, we should likely also add a couple readline tests. Please also add tests for more special cases.
@@ -474,8 +491,60 @@ Interface.prototype._insertString = function(c) { | |||
// a hack to get the line refreshed if it's needed | |||
this._moveCursor(0); | |||
} | |||
|
|||
searchHistory.call(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be obsolete as _refreshLine()
has just been called before and that calls searchHistory()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess searchHistory
is required, but we should move up to the else block (where the refresh line is not called in that case.
const historySet = new Set(this.history); | ||
for (;this.reverseSearchIndex < [...historySet].length; | ||
this.reverseSearchIndex++) { | ||
if (this.line.trim() !== '' && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The trimming could be done once upfront instead of for each history entry but I would rather not do this at all. It should be fine to search for e.g., multiple whitespace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, searching for whitespaces might be useful. Let me check on this.
this.line = this.history[this.reverseSearchIndex - 1]; | ||
this.inReverseSearch = false; | ||
this.reverseSearchIndex = 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you test all other "special" keys like left
, right
, up
, down
, tab
, etc? I personally would just mimic the behavior in the bash which ends the reverse search but if it has no side effects here, we could keep this. I just expect some possible input to have unintended side effects.
E.g. ctrl + a
: where does the cursor go in that case? The beginning of the line should not be zero in this case. We could keep the functionality working but in that case we would have to move the cursor to the correct position and we have to make sure this works with all input (and that's not that easy due to lots of cases).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, yes I did test on this. Actually, all the keys like left
, right
, ctrl + a
are working as expected (as the whole reverse search is on the same stream and cursor set correctly). In this case, I guess it's good enough to leave these as is, rather than writing a layer which prevents all these key combinations when search is on. What say? Thoughts?
if (this.inReverseSearch) { | ||
let result = searchText.call(this); | ||
const historySet = new Set(this.history); | ||
if (this.reverseSearchIndex >= [...historySet].length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand this code part, can you please elaborate? How could the found index be bigger than the set length? It could at max be equal but that should be legit, no?
cursorTo(this.output, this.cursor + kReverseSearchPrompt.length - 2); | ||
} else { | ||
this.output.write(buildReverseSearchPrompt(this.line, '')); | ||
cursorTo(this.output, this.cursor + kFailedReverseSearchPrompt.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The this.cursor
part confuses me in all of these. Are you certain it is the correct position?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. But can you explain me a bit, why this is confusing you?
cursorTo(this.output, this.cursor + kReverseSearchPrompt.length - 2); | ||
} else if (this.line === '') { | ||
this.output.write(buildReverseSearchPrompt()); | ||
cursorTo(this.output, this.cursor + kReverseSearchPrompt.length - 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could move the cursor to the position where we matched the entry. That way we also use the same behavior as the bash and it's helpful for the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1. I guess this is very good idea!
@@ -383,6 +398,8 @@ Interface.prototype._refreshLine = function() { | |||
} | |||
|
|||
this.prevRows = cursorPos.rows; | |||
|
|||
searchHistory.call(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This calls the searchHistory more often than it's necessary. Right now it would trigger this with each e.g. cursor move but we should only trigger the search if the input letters changed.
function searchText() { | ||
let result = ''; | ||
const historySet = new Set(this.history); | ||
for (;this.reverseSearchIndex < [...historySet].length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no need to convert the set back to an array. Just use .size
instead. The same below.
However, this approach does not work in all cases. The unique set could have less entries than the original history and the entry to look for might be at the end of the history which would not be reached here anymore.
E.g.: The set of [1, 1, 1, 2]
is { 1, 2 }
and if we look for 2
it would only check history entry 0 and 1 and would give up afterwards.
I personally would just iterate directly over the array.
// At the top of the file:
const kReverseMatches = Symbol('kReverseMatches');
const kRreverseSearchIndex = Symbol('kRreverseSearchIndex');
// In the constructor:
Object.defineProperty(this, kRreverseSearchIndex, {
value: -2,
writable: true,
configurable: true,
enumerable: false
});
Object.defineProperty(this, kReverseMatches, {
value: null,
writable: true,
configurable: true,
enumerable: false
});
// Actual logic:
function reverseSearch(self) {
while (++self[kRreverseSearchIndex] < self.history.length) {
const entry = self.history[self[kRreverseSearchIndex]];
if (self[kReverseMatches].has(entry)) continue;
self[kReverseMatches].add(entry);
const start = entry.indexOf(self.line)
if (start !== -1) {
const output = `(reverse-i-search)\`${self.line}': `;
self.output.write(`${output}${entry}`);
cursorTo(self.output, output.length + start);
return;
}
}
self[kRreverseSearchIndex] = -1;
const output = `(failed-reverse-i-search)\`${text}':`;
self.output.write(output);
cursorTo(self.output, output.length);
}
// Further below:
case 'r':
if (this[kRreverseSearchIndex] === -2) {
this[kRreverseSearchIndex] = -1;
this[kReverseMatches] = new Set();
}
reverseSearch(this);
// Further below:
case 'return': // carriage return, i.e. \r
this._sawReturnAt = Date.now();
if (this[kRreverseSearchIndex] !== -2) {
if (this[kRreverseSearchIndex] !== -1)
this.line = this.history[this[kRreverseSearchIndex]];
this[kRreverseSearchIndex] = -2;
this[kReverseMatches] = null;
}
The code is untested but it should roughly work like that.
Btw: the bash would change the historyIndex
which this implementation does not but I think it's fine as it is.
@BridgeAR Thanks for your time on this, sorry was quite busy hence late reply from my side. Will try to update the PR with comments addressed. |
Taken care by: #31006 |
Hello Team,
I wanted to add reverse search to repl:
ctrl+r
to search their historynode_repl_history
and show off the matching results.repl
.ctrl+r
again will navigate the search results one by one in a circular fashion (if we reach end of the search result).It's a work in progress need tests, doc etc.
Initial mode of working is ready.
Need thoughts from community on the idea. But I would love to land this on repl 😍
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes