Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

RFC: Revamp the REPL Tab completion feature #17826

Closed
wants to merge 5 commits into from

Conversation

Bamieh
Copy link
Contributor

@Bamieh Bamieh commented Dec 22, 2017

This feature reworks the tab complete feature in the repl module.

Rather than using regular expressions to guess the completion options, the tab completion will call a method on the function through a Symbol attached on these methods, which returns an array of possible completion options.

const repl = require('repl');
var fnWithTabComplete = function(someParam) {
   //fn body
}

fnWithTabComplete[repl.replComplete] = function(inputParams, paramsText) {
  // inputParams => [param1, param2, ...]
  // paramsText => "param1, param2 ..."

  return ['tabComplete1', 'tabComplete2'];
}


fnWithTabComplete('...<Tab>'
/*
  tabComplete1    tabComplete2
*/

Notes

  • This tab completion feature works on all methods and functions with the repl.replComplete Symbol attached on them.
  • The completer function gets an array of the parameters the user added. This way the implementers can specify different tab completion based on the parameter the user is trying auto complete.

Benefits

  • more scalable than using regex
  • exposes the tab completion for both external and internal modules
  • move tab completion implementations into the concerned modules rather than bloating the repl module.
  • Implementing tab completion in other modules becomes a breeze rather than trying to craft a usually-morbid regular expression.
  • Additionally, using the current approach for tab completion renders the feature rigid:
    • When I added the tab completion feature for the fs module methods, a concern about inconsistency of having the tab completion work in some places while not in others. This approach makes it the concern of the consuming modules to implement the tab completion as needed rather than having it
    • All the methods supported by tab completion will have to be maintained by the repl module, its test cases, and any updated on the main module will have to be reflected. Basically the repl will have a large dictionary ot method names in other modules.

Context

Recently i submitted a PR to implement tab completion for the fs module, and i was faced with many design issues mainly because of how impractical the regexp approach is for expanding the tab completion feature.

@starkwang @apapirovski @benjamingr can you provide feedback on this please, since you are in context with this issue already. Thanks!

Semver: Major or Minor

I believe this does not introduce any breaking changes so it could be introduced as a minor semver.

RFC

I am requesting comments and feedback on the approach and some general guidance. This PR is far from complete but it contains the initial direction of implementation. Also I have an already opened PR for the fs path autocomplete, if this PR receives positive feedback i will refactor the other one to use this feature.

Final notes

Finally, I will leave you with this piece of wisdom my brain crafted while implementing the feature 😄

I feel that the regular expression approach is similar to the foreign key constrain in SQL, using constrains usually introduces rigidity and impracticality.

Cheers!

@nodejs-github-bot nodejs-github-bot added the repl Issues and PRs related to the REPL subsystem. label Dec 22, 2017
@addaleax addaleax added the semver-minor PRs that contain new features and should be released in the next minor version. label Dec 22, 2017
Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work so far, I like it! 👍

Do you have a specific list of functions inside Node core where this could be added?

Also, can you add documentation for repl.replComplete?

lib/repl.js Outdated
@@ -964,6 +964,9 @@ function complete(line, callback) {
// REPL commands (e.g. ".break").
var filter;
var match = null;

var functionCallRE = /\b(\w+)\([^)]*$/;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there can be arbitrary whitespace before the opening (?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I will add test cases and go through the code again to make sure its production ready. meanwhile i just wanted to submit the PR to get feedback for the general direction of the feature rather than the code details.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does  (\w+) catch all valid function names?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misses $, but otherwise seems OK. % may also be needed for natives support.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this can be const

lib/repl.js Outdated
// console.log("::a, b = 0, /*c,*/ d::", annotate("a, b = 0, /*c,*/ d"))
// console.log("::a, \"bamieh, ahm\", \"incom::", annotate("a, \"bamieh, ahm\", \"incom"))

exports.replComplete = Symbol('REPL Complete');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel like this name is a bit unclear, maybe tabComplete and REPL Tab Complete?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the review! As i said the code still needs some work, but i wanted to put it out ASAP to get feedback in order to know if i should put in more time into this feature

@Bamieh
Copy link
Contributor Author

Bamieh commented Dec 22, 2017

@addaleax Awesome, I wanted to make sure that the feature is aligned with node. I will add test cases, update the docs, rename things and rewrite some parts to make sure its production ready

lib/repl.js Outdated
let started = false;
return params.split(',').reduce((acc, cur, i, array) => {
if(/'|"|`|\/\*|\*\//.test(cur)) {
if(started === true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here can be simplified to if(started) { ... }

lib/repl.js Outdated
try {
var args = parseInputParameteres(paramsPassed);
console.log('paramsPassed::', paramsPassed)
console.log('args::', args)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove these two console.log ? 😉

lib/repl.js Outdated
} else if (line.match(functionCallRE)) {
const fnParenthesesIndex = line.lastIndexOf('(');
const fullMethod = line.slice(0, fnParenthesesIndex);
const paramsPassed = line.slice(fnParenthesesIndex+1) || '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eslint:
line.slice(fnParenthesesIndex+1) -> line.slice(fnParenthesesIndex + 1)

lib/repl.js Outdated

this.eval(evalExpr, this.context, 'repl', (e, completer) => {
var completionGroup = [];
if(typeof completer === 'function') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: be sure to run make lint ... there needs to be a space between the if and the ( here :-)

@Bamieh
Copy link
Contributor Author

Bamieh commented Dec 28, 2017

@Jansell sorry i have been a little busy, but this PR is undergoing major rework. again i submitted the PR to request comments on the concept not the actual code. Hopefully I will submit my final iteration later on tomorrow.

@BridgeAR
Copy link
Member

BridgeAR commented Feb 7, 2018

@BridgeAR
Copy link
Member

BridgeAR commented Feb 9, 2018

@Bamieh it seems like there are some linting issues and a test it constantly failing:

not ok 1364 parallel/test-repl-tab-complete
  ---
  duration_ms: 0.313
  severity: fail
  stack: |-
    [ [ 13, 7 ], undefined ]
    [ [ 13, 7 ], undefined ]
    assert.js:527
        throw newErr;
        ^
    
    AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 0 strictEqual 1
        at Domain.<anonymous> (/Users/iojs/build/workspace/node-test-commit-osx/nodes/osx1010/test/parallel/test-repl-tab-complete.js:49:10)
        at Domain.emit (events.js:136:15)
        at Domain.emit (domain.js:421:20)
        at Domain._errorHandler (domain.js:203:23)
        at Object.setUncaughtExceptionCaptureCallback (domain.js:119:29)
        at process._fatalException (bootstrap_node.js:441:31)
        at testMe.complete.common.mustCall (/Users/iojs/build/workspace/node-test-commit-osx/nodes/osx1010/test/parallel/test-repl-tab-complete.js:276:16)
  ...

Would you be so kind and have a look? :-)

const walk = require('internal/deps/acorn/dist/walk');

function parseFunctionArgs(input, r) {
if(input === '') return [''];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Please add a empty space after the if in all cases (so also below).

const trimmedStr = input.trim();
const lastArgIndex = input.lastIndexOf(',');
const lastArg = input.substr(lastArgIndex + 1).trim();
return parseFunctionArgs(input.substring(0, lastArgIndex), lastArg);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation looks wrong here. Does the linter pass here?

}
}


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non blocking nit: please only use a single new line.


inputs.forEach(input => {
const results = parseFunctionArgs(input.args);
assert.strictEqual(results.length, input.expectedResult.length);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a obsolete check because the next line will verify that the input and result is identical.

// tab completion Symbol
{
assert.strictEqual(typeof repl.tabComplete, 'symbol');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test does not need a own scope as it does not interfere with anything else.

@@ -203,6 +203,86 @@ testMe.complete('toSt', common.mustCall(function(error, data) {
assert.deepStrictEqual(data, [['toString'], 'toSt']);
}));

// tab completion Symbol
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Please always use a capital letter as first character of a comment and punctuate them.

@BridgeAR
Copy link
Member

BridgeAR commented Mar 2, 2018

Ping @Bamieh

@Bamieh
Copy link
Contributor Author

Bamieh commented Mar 5, 2018

@BridgeAR please keep the PR opened, I have it scheduled on friday to finish it.

@BridgeAR
Copy link
Member

Closing due to long inactivity. @Bamieh please feel free to open a new PR or to leave a comment so this PR can be reopened in case you would like to continue working on this.

@BridgeAR BridgeAR closed this Apr 16, 2018
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
repl Issues and PRs related to the REPL subsystem. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants