|
1 | 1 | /*
|
2 |
| - * Copyright 2017 IBM Corporation |
| 2 | + * Copyright 2017-18 IBM Corporation |
3 | 3 | *
|
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | 5 | * you may not use this file except in compliance with the License.
|
@@ -31,6 +31,17 @@ const util = require('util'),
|
31 | 31 |
|
32 | 32 | debug('finished loading modules')
|
33 | 33 |
|
| 34 | +// for plugins.js |
| 35 | +exports.disambiguator = () => { |
| 36 | + const map = {} |
| 37 | + for (let command in disambiguator) { |
| 38 | + map[command] = disambiguator[command].map(({route, options}) => ({ |
| 39 | + route, plugin: options && options.plugin |
| 40 | + })) |
| 41 | + } |
| 42 | + return map |
| 43 | +} |
| 44 | + |
34 | 45 | /** shallow array equality */
|
35 | 46 | const sameArray = (A, B) => A.length === B.length && A.every((element, idx) => element === B[idx])
|
36 | 47 |
|
@@ -402,6 +413,7 @@ const withEvents = (evaluator, leaf) => {
|
402 | 413 | }
|
403 | 414 |
|
404 | 415 | return {
|
| 416 | + subtree: leaf, |
405 | 417 | route: leaf.route,
|
406 | 418 | eval: evaluator,
|
407 | 419 | options: leaf && leaf.options,
|
@@ -492,39 +504,133 @@ const read = (model, argv) => {
|
492 | 504 | *
|
493 | 505 | */
|
494 | 506 | const disambiguate = (argv, noRetry=false) => {
|
495 |
| - const resolutions = (disambiguator[argv[0]] || disambiguator[argv[argv.length -1]] || []).filter(isFileFilter) |
496 |
| - debug('disambiguate', argv, resolutions) |
| 507 | + let idx |
| 508 | + const resolutions = ( (((idx=0)||true) && disambiguator[argv[idx]]) || (((idx=argv.length-1)||true) && disambiguator[argv[idx]]) || []).filter(isFileFilter) |
| 509 | + debug('disambiguate', idx, argv, resolutions) |
497 | 510 |
|
498 | 511 | if (resolutions.length === 0 && !noRetry) {
|
499 | 512 | // maybe we haven't loaded the plugin, yet
|
| 513 | + debug('disambiguate attempting to resolve plugins') |
500 | 514 | resolver.resolve(`/${argv.join('/')}`)
|
501 | 515 | return disambiguate(argv, true)
|
502 | 516 |
|
503 | 517 | } else if (resolutions.length === 1) {
|
| 518 | + // one unambiguous resolution! great, but we need to |
| 519 | + // double-check: if the resolution is a subtree, then it better have a child that matches |
504 | 520 | const leaf = resolutions[0]
|
| 521 | + |
| 522 | + if (idx < argv.length - 1 && leaf.children) { |
| 523 | + // then the match is indeed a subtree |
| 524 | + let foundMatch = false |
| 525 | + const next = argv[argv.length - 1] |
| 526 | + for (let cmd in leaf.children) { |
| 527 | + if (cmd === next) { |
| 528 | + foundMatch = true |
| 529 | + break |
| 530 | + } |
| 531 | + } |
| 532 | + if (!foundMatch) { |
| 533 | + debug('disambiguate blocked due to subtree mismatch') |
| 534 | + return |
| 535 | + } |
| 536 | + } |
| 537 | + |
505 | 538 | debug('disambiguate success', leaf)
|
506 | 539 | return withEvents(leaf.$, leaf)
|
507 | 540 | }
|
508 | 541 | }
|
509 | 542 |
|
510 | 543 | /**
|
511 |
| - * Oops, we couldn't resolve the given command |
| 544 | + * Oops, we couldn't resolve the given command. But maybe we found |
| 545 | + * some partial matches that might be helpful to the user. |
512 | 546 | *
|
513 | 547 | */
|
514 |
| -const commandNotFoundMessage = 'Command not found' |
515 |
| -const commandNotFound = argv => { |
| 548 | +const commandNotFoundMessage = 'Command not found', |
| 549 | + commandNotFoundMessageWithPartialMatches = 'The following commands are partial matches for your request.' |
| 550 | + |
| 551 | +const commandNotFound = (argv, partialMatches) => { |
516 | 552 | eventBus.emit('/command/resolved', {
|
517 | 553 | // ANONYMIZE: namespace: namespace.current(),
|
518 | 554 | error: commandNotFoundMessage,
|
519 | 555 | command: argv[0],
|
520 | 556 | context: exports.currentContext()
|
521 | 557 | })
|
522 | 558 |
|
523 |
| - const error = new Error(commandNotFoundMessage) |
| 559 | + const error = partialMatches ? formatPartialMatches(partialMatches) : new Error(commandNotFoundMessage) |
524 | 560 | error.code = 404
|
| 561 | + |
| 562 | + // to allow for programmatic use of the partial matches, e.g. for tab completion |
| 563 | + if (partialMatches) { |
| 564 | + error.partialMatches = partialMatches.map(_ => ({ command: _.route.split('/').slice(1).join(' '), |
| 565 | + usage: _.options && _.options.usage })) |
| 566 | + } |
| 567 | + |
525 | 568 | throw error
|
526 | 569 | }
|
527 | 570 |
|
| 571 | +/** |
| 572 | + * Help the user with some partial matches for a command not found |
| 573 | + * condition. Here, we reuse the usage-error formatter, to present the |
| 574 | + * user with a list of possible completions to their (mistyped or |
| 575 | + * otherwise) command. |
| 576 | + * |
| 577 | + * We use the `available` list to present the list of available |
| 578 | + * command completions to what they typed. |
| 579 | + * |
| 580 | + */ |
| 581 | +const formatPartialMatches = matches => { |
| 582 | + return new (require('./usage-error'))({ |
| 583 | + message: commandNotFoundMessage, |
| 584 | + usage: { |
| 585 | + header: commandNotFoundMessageWithPartialMatches, |
| 586 | + available: matches.map(({options}) => options.usage).filter(x=>x) |
| 587 | + } |
| 588 | + }, { noBreadcrumb: true, noHide: true }) |
| 589 | +} |
| 590 | + |
| 591 | +/** |
| 592 | + * Command not found: let's find partial matches at head of the given |
| 593 | + * subtree. We hope that the last part of the argv is a partial match |
| 594 | + * for some command at this root. Return all such prefix matches. |
| 595 | + * |
| 596 | + */ |
| 597 | +const findPartialMatchesAt = (subtree, partial) => { |
| 598 | + debug('scanning for partial matches', partial, subtree) |
| 599 | + |
| 600 | + const matches = [] |
| 601 | + |
| 602 | + if (subtree && subtree.children && partial) { |
| 603 | + for (let cmd in subtree.children) { |
| 604 | + if (cmd.indexOf(partial) === 0) { |
| 605 | + const match = subtree.children[cmd] |
| 606 | + if (!match.options || (!match.options.synonymFor && !match.options.hide)) { |
| 607 | + // don't include synonyms or hidden commands |
| 608 | + matches.push(match) |
| 609 | + } |
| 610 | + } |
| 611 | + } |
| 612 | + } |
| 613 | + |
| 614 | + return matches |
| 615 | +} |
| 616 | + |
| 617 | +/** remove duplicates of leaf nodes from a given array */ |
| 618 | +const removeDuplicates = arr => { |
| 619 | + return arr |
| 620 | + .filter(x=>x) |
| 621 | + .reduce((state, item) => { |
| 622 | + const { M, A } = state, |
| 623 | + route = item.route |
| 624 | + |
| 625 | + if (item && !M[item.route]) { |
| 626 | + M[item.route] = true |
| 627 | + A.push(item) |
| 628 | + } |
| 629 | + |
| 630 | + return state |
| 631 | + }, { M: {}, A: [] }).A |
| 632 | +} |
| 633 | + |
528 | 634 | /** here, we will use implicit context resolutions */
|
529 | 635 | exports.read = (argv, noRetry=false, noSubtreeRetry=false) => {
|
530 | 636 | let cmd = read(model, argv) || disambiguate(argv)
|
@@ -553,7 +659,41 @@ exports.read = (argv, noRetry=false, noSubtreeRetry=false) => {
|
553 | 659 | }
|
554 | 660 |
|
555 | 661 | if (!cmd) {
|
556 |
| - return commandNotFound(argv) |
| 662 | + debug('command not found, searching for partial matches') |
| 663 | + |
| 664 | + // command not found, but maybe we can find partial matches |
| 665 | + // that might be helpful? |
| 666 | + let matches |
| 667 | + |
| 668 | + if (argv.length === 1) { |
| 669 | + debug('searching for partial matches at root') |
| 670 | + |
| 671 | + // disambiguatePartial takes a partial command, and |
| 672 | + // returns an array of matching full commands, which we |
| 673 | + // can turn into leafs via `disambiguate` |
| 674 | + matches = removeDuplicates(findPartialMatchesAt(model, argv[0]) |
| 675 | + .concat(resolver.disambiguatePartial(argv[0]).map(_ => [_]).map(_ => disambiguate(_)))) |
| 676 | + |
| 677 | + } else { |
| 678 | + const allButLast = argv.slice(0, argv.length - 1), |
| 679 | + last = argv[argv.length - 1] |
| 680 | + |
| 681 | + debug('searching for partial matches for subcommand', allButLast) |
| 682 | + |
| 683 | + const parent = read(model, allButLast) || disambiguate(allButLast) |
| 684 | + if (parent) { |
| 685 | + matches = removeDuplicates(findPartialMatchesAt(parent.subtree, last)) |
| 686 | + } |
| 687 | + } |
| 688 | + |
| 689 | + // found some partial matches? |
| 690 | + if (matches && matches.length > 0) { |
| 691 | + debug('found partial matches', matches) |
| 692 | + } else { |
| 693 | + matches = undefined |
| 694 | + } |
| 695 | + |
| 696 | + return commandNotFound(argv, matches) |
557 | 697 | } else {
|
558 | 698 | return cmd
|
559 | 699 | }
|
|
0 commit comments