aboutsummaryrefslogtreecommitdiff
path: root/node_modules/coa/src/completion.coffee
blob: 2c1e358ddd5bd7b39f3af96f51d918a53591b484 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
###*
Most of the code adopted from the npm package shell completion code.
See https://github.com/isaacs/npm/blob/master/lib/completion.js
###

Q = require 'q'
escape = require('./shell').escape
unescape = require('./shell').unescape

module.exports = ->
    @title('Shell completion')
        .helpful()
        .arg()
            .name('raw')
            .title('Completion words')
            .arr()
            .end()
        .act (opts, args) ->
            if process.platform == 'win32'
                e = new Error 'shell completion not supported on windows'
                e.code = 'ENOTSUP'
                e.errno = require('constants').ENOTSUP
                return @reject(e)

            # if the COMP_* isn't in the env, then just dump the script
            if !process.env.COMP_CWORD? or !process.env.COMP_LINE? or !process.env.COMP_POINT?
                return dumpScript(@_cmd._name)

            console.error 'COMP_LINE:  %s', process.env.COMP_LINE
            console.error 'COMP_CWORD: %s', process.env.COMP_CWORD
            console.error 'COMP_POINT: %s', process.env.COMP_POINT
            console.error 'args: %j', args.raw

            # completion opts
            opts = getOpts args.raw

            # cmd
            { cmd, argv } = @_cmd._parseCmd opts.partialWords
            Q.when complete(cmd, opts), (compls) ->
                console.error 'filtered: %j', compls
                console.log compls.map(escape).join('\n')


dumpScript = (name) ->
    fs = require 'fs'
    path = require 'path'
    defer = Q.defer()

    fs.readFile path.resolve(__dirname, 'completion.sh'), 'utf8', (err, d) ->
        if err then return defer.reject err
        d = d.replace(/{{cmd}}/g, path.basename name).replace(/^\#\!.*?\n/, '')

        onError = (err) ->
            # Darwin is a real dick sometimes.
            #
            # This is necessary because the "source" or "." program in
            # bash on OS X closes its file argument before reading
            # from it, meaning that you get exactly 1 write, which will
            # work most of the time, and will always raise an EPIPE.
            #
            # Really, one should not be tossing away EPIPE errors, or any
            # errors, so casually. But, without this, `. <(cmd completion)`
            # can never ever work on OS X.
            if err.errno == require('constants').EPIPE
                process.stdout.removeListener 'error', onError
                defer.resolve()
            else
                defer.reject(err)

        process.stdout.on 'error', onError
        process.stdout.write d, -> defer.resolve()

    defer.promise


getOpts = (argv) ->
    # get the partial line and partial word, if the point isn't at the end
    # ie, tabbing at: cmd foo b|ar
    line = process.env.COMP_LINE
    w = +process.env.COMP_CWORD
    point = +process.env.COMP_POINT
    words = argv.map unescape
    word = words[w]
    partialLine = line.substr 0, point
    partialWords = words.slice 0, w

    # figure out where in that last word the point is
    partialWord = argv[w] or ''
    i = partialWord.length
    while partialWord.substr(0, i) isnt partialLine.substr(-1 * i) and i > 0
        i--
    partialWord = unescape partialWord.substr 0, i
    if partialWord then partialWords.push partialWord

    {
        line: line
        w: w
        point: point
        words: words
        word: word
        partialLine: partialLine
        partialWords: partialWords
        partialWord: partialWord
    }


complete = (cmd, opts) ->
    compls = []

    # complete on cmds
    if opts.partialWord.indexOf('-')
        compls = Object.keys(cmd._cmdsByName)
        # Complete on required opts without '-' in last partial word
        # (if required not already specified)
        #
        # Commented out because of uselessness:
        # -b, --block suggest results in '-' on cmd line;
        # next completion suggest all options, because of '-'
        #.concat Object.keys(cmd._optsByKey).filter (v) -> cmd._optsByKey[v]._req
    else
        # complete on opt values: --opt=| case
        if m = opts.partialWord.match /^(--\w[\w-_]*)=(.*)$/
            optWord = m[1]
            optPrefix = optWord + '='
        else
            # complete on opts
            # don't complete on opts in case of --opt=val completion
            # TODO: don't complete on opts in case of unknown arg after commands
            # TODO: complete only on opts with arr() or not already used
            # TODO: complete only on full opts?
            compls = Object.keys cmd._optsByKey

    # complete on opt values: next arg case
    if not (o = opts.partialWords[opts.w - 1]).indexOf '-'
        optWord = o

    # complete on opt values: completion
    if optWord and opt = cmd._optsByKey[optWord]
        if not opt._flag and opt._comp
            compls = Q.join compls, Q.when opt._comp(opts), (c, o) ->
                c.concat o.map (v) -> (optPrefix or '') + v

    # TODO: complete on args values (context aware, custom completion?)

    # custom completion on cmds
    if cmd._comp
        compls = Q.join compls, Q.when(cmd._comp(opts)), (c, o) ->
            c.concat o

    # TODO: context aware custom completion on cmds, opts and args
    # (can depend on already entered values, especially options)

    Q.when compls, (compls) ->
        console.error 'partialWord: %s', opts.partialWord
        console.error 'compls: %j', compls
        compls.filter (c) -> c.indexOf(opts.partialWord) is 0