-
-
Notifications
You must be signed in to change notification settings - Fork 489
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
feat: add support for custom keybindings and editor actions in the REPL #2739
base: develop
Are you sure you want to change the base?
Conversation
Signed-off-by: Snehil Shah <[email protected]>
Another solution to the problem in OP is, we can expect the user to give us the ASCII sequence of the keypress instead. Every keypress will always have a Now this is not user-friendly as the user is expected to know the control sequence of the keypress they are wanting to assign. So, another approach would be to maintain a table mapping all possible keybindings to their corresponding emitted control sequences. Something like: const CONTROL_SEQUENCES = {
'\x01': 'Ctrl+A',
'\x02': 'Ctrl+B',
'\x03': 'Ctrl+C',
'\x04': 'Ctrl+D',
'\x05': 'Ctrl+E',
'\x06': 'Ctrl+F',
'\x07': 'Ctrl+G',
'\x08': 'Ctrl+H',
'\x09': 'Ctrl+I',
'\x0A': 'Ctrl+J',
'\x0B': 'Ctrl+K',
'\x0C': 'Ctrl+L',
'\x0D': 'Ctrl+M',
'\x0E': 'Ctrl+N',
'\x0F': 'Ctrl+O',
'\x10': 'Ctrl+P',
'\x11': 'Ctrl+Q',
'\x12': 'Ctrl+R',
'\x13': 'Ctrl+S',
'\x14': 'Ctrl+T',
'\x15': 'Ctrl+U',
'\x16': 'Ctrl+V',
'\x17': 'Ctrl+W',
'\x18': 'Ctrl+X',
'\x19': 'Ctrl+Y',
'\x1A': 'Ctrl+Z'
}; But not sure if control sequences and their corresponding keys are standardized across emulators..? |
https://github.com/nodejs/node/blob/main/lib/internal/readline/utils.js#L59
|
@Snehil-Shah Not requiring providing a Enforcing xterm standards for key parsing could be feasible. xterm is widely used and AFAIK many terminal emulators aim for compatibility with xterm, so this sounds worth investigating. I also do think that it would be a pragmatic choice to handle the |
Now if we decide to go ahead and enforce xterm control sequences, we also have the flexibility to change the input format the user can give. So we can have something like the string "CTRL-E" as input, and map this to the corresponding xterm control sequence.. |
Signed-off-by: Snehil Shah <[email protected]>
Signed-off-by: Snehil Shah <[email protected]>
Went ahead and added parsing of unrecognized keypresses. Fortunately, there were not many cases to cover, the above problems were mainly around keys involving symbols. @Planeshifter I think it would be better to stick to |
Signed-off-by: Snehil Shah <[email protected]>
Signed-off-by: Snehil Shah <[email protected]>
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.
Looks good to me!
One small comment: I believe we can safely assume that _rli.line
is a string, correct? Hence, we could use the base package equivalents, so e.g. @stdlib/string/base/uppercase
instead of @stdlib/string/uppercase
, to bypass runtime argument validation of these functions.
@Planeshifter Makes sense.. |
We should go ahead and make those changes. Too easy to forget to do them later. |
Signed-off-by: Snehil Shah <[email protected]>
For instance, the right arrow key should also complete the completion. It would be a better idea to clean up this logic in the PR where we add support for configuring keybindings for existing actions. Signed-off-by: Snehil Shah <[email protected]>
Signed-off-by: Snehil Shah <[email protected]>
Signed-off-by: Snehil Shah <[email protected]>
} | ||
for ( j = 0; j < actionKeys.length; j++ ) { | ||
for ( k = 0; k < possibleKeys.length; k++ ) { | ||
if ( deepEqual( possibleKeys[ k ], actionKeys[ j ] ) ) { |
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.
Deep equality could be an expensive check. Is there a potential lighter weight means?
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.
Given that we know the keys, can we not write a small custom function which explicitly compares known keys?
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.
For two keys to be equal, their four properties should be equal (name
, ctrl
, meta
, shift
). Wouldn't deep equality do just that?
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 it is the difference between comparing values having an unknown schema and those having a known schema. For the former, you need to dynamically resolve the list of keys and then iterate. For the latter, you can skip resolution and directly compare without iteration. It is something of a micro-optimization, but my main concern is preventing lag, especially as we continue to add various keystroke listeners.
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.
Makes sense. I'll make a helper function, or better, a module like containsKeys
which will take an array of possible keybindings (as one action has multiple keybindings set) and the user input key.
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.
Seems reasonable.
Signed-off-by: Athan <[email protected]>
Signed-off-by: Athan <[email protected]>
* @param {string} key - readline keypress object | ||
* @returns {(Array<Object>|boolean)} list of possible key objects |
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.
These types seem off. Is key
an object or a string? And it is not obvious that the function can return a boolean
.
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.
Okay. parseMetaSymbolSequence
can return a boolean. Would it make more sense to make the types consistent? For example, return an empty array? Alternatively, rather than a boolean, it is more common for us to return null
.
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 forgot to update the jsdoc, sorry for the confusion. Yes, the main parse_key function doesn't return a boolean.
I updated parseMetaSymbolSequence
to return null
instead. If it returns null
means it's not a meta symbol sequence. So we just return the original key object.
Should parseMetaSymbolSequence
return an empty array? The only problem would be, we won't be able to check directly if it returned a sequence or not (using if ( out )
) but would have to check if the array is empty. null
should be fine(?).
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.
Is there an issue with checking out.length > 0
? I suppose I am not sure why null
would be preferred over checking the length of the returned array.
'indentLineRight': [ | ||
{ | ||
'name': 'right', | ||
'ctrl': false, | ||
'shift': false, | ||
'meta': true | ||
}, | ||
{ | ||
'name': 'i', | ||
'ctrl': true, | ||
'shift': false, | ||
'meta': false | ||
} | ||
], | ||
'indentLineLeft': [ | ||
{ | ||
'name': 'left', | ||
'ctrl': false, | ||
'shift': false, | ||
'meta': true | ||
}, | ||
{ | ||
'name': 'i', | ||
'ctrl': false, | ||
'shift': false, | ||
'meta': true | ||
} |
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.
@Planeshifter and I were not able to get indentation to work.
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.
Just tried and it's highly likely it's clashing with your terminal's internal keybindings (that's the case with me, ALT+RIGHT is vscode terminal's internal binding). Just change the keybinding to something unoccupied and it should work.
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 wonder how we should publicly document these keybindings. As @Planeshifter and I experienced, not all the default keybindings work (e.g., due to a terminal's internal keybindings). I can already imagine getting bug reports for when keybindings don't work, but this is because a user is unaware that some other application has already laid claim to a particular keybinding.
'name': 'backspace', | ||
'ctrl': true, | ||
'shift': false, | ||
'meta': false | ||
}, | ||
{ | ||
'name': 'backspace', | ||
'ctrl': false, | ||
'shift': false, | ||
'meta': true | ||
}, | ||
{ | ||
'name': 'w', | ||
'ctrl': true, | ||
'shift': false, | ||
'meta': false | ||
} |
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.
Should all of these keybindings work or just one of them?
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.
All of them should work. The user can set multiple keybindings to trigger a single action.
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.
It was often the case that only one of the keybindings worked. I suppose it is possible that there are conflicts happening somewhere so that not able to see intended changes.
'meta': true | ||
} | ||
], | ||
'indentLineRight': [ |
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 also doesn't work for me.
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.
Is this comment also for the indentation?
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, meaning same as indentLineLeft
for me, as neither worked.
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, @Snehil-Shah, for working this. I think this PR is pretty close to merge. Just left a few small questions, and @Planeshifter and I were not able to get indentation to work.
@Snehil-Shah I'll defer to your judgement on that. |
Signed-off-by: Snehil Shah <[email protected]>
Signed-off-by: Snehil Shah <[email protected]>
Signed-off-by: Snehil Shah <[email protected]>
Signed-off-by: Snehil Shah <[email protected]>
Towards #2647.
Description
This pull request:
Related Issues
This pull request:
Questions
For the sake of user experience, I decided the key object can have 4 properties:
name
,ctrl
,shift
, andmeta
. In actuality, a readlinekey
object also has properties likesequence
andcode
, which mainly contain the escape sequence of the keypress.In most keypresses, the
name
,ctrl
,shift
, andmeta
properties are correctly emitted and are enough to determine the action we have to trigger. But in some cases, for instance, CTRL+/, instead of emitting an event with{ 'name': '/', 'ctrl': true }
, it emits a sequence{ 'sequence': '\x1f' }
. So if a user has keybindings with CTRL+/ as the key, it won't work, as such a keypress is never emitted.Now such cases might be limited, as readline only uses the
sequence
prop to identify two keypress actions, namely undo and redo.Maybe we can handle these two special cases manually in code? Not sure if a robust solution exists.
Ofcourse, we can also punt the entire responsibility of giving the readline-ready
key
object, onto the user, but that's not user friendly IMO.Other
No.
Checklist
@stdlib-js/reviewers