--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author OpportunityLiu, glcraft -- @file complete.lua -- -- imports import("core.base.option") import("core.base.cli") local completer = {} function completer.new(pos, config, words) local instance = table.inherit(completer) instance._POSITION = 0 instance._CONFIG = {} instance._WORDS = {} instance._DEFAULT_COMMAND = "" instance:set_config(config) instance:set_position(pos) instance:set_words(words) return instance end function completer:_print_candidate(candidate) if candidate.value and #candidate.value ~= 0 then printf(candidate.value) if not self:config("nospace") and candidate.is_complete then print(" ") else print("") end end end function completer:_print_candidates(candidates) if self:config("json") then import("core.base.json") print(json.encode(candidates)) else for _, v in ipairs(candidates) do self:_print_candidate(v) end end end function completer:_find_candidates(candidates, find) if type(candidates) ~= 'table' then return {} end local has_candidate = false local results = table.new(#candidates, 0) -- find candidate starts with find str for _, v in ipairs(candidates) do if tostring(v):startswith(find) then table.insert(results, v) has_candidate = true end end -- stop searching if found any if has_candidate then return results end -- find candidate contains find str for _, v in ipairs(candidates) do if tostring(v):find(find, 1, true) then table.insert(results, v) end end return results end function completer:_complete_item(items, name) local found_candidates = {} for _, v in ipairs(self:_find_candidates(table.keys(items), name)) do table.insert(found_candidates, { value = v, is_complete = true, description = items[v].description }) end self:_print_candidates(found_candidates) return #found_candidates > 0 end -- complete values of kv function completer:_complete_option_kv_v(options, current, completing, name, value) -- find completion option local opt for _, v in ipairs(options) do if v[3] == "kv" and (v[1] == name or v[2] == name) then opt = v break end end if not opt then return false end -- show candidates of values local values = opt.values if type(values) == "function" then values = values(value, current) end if values == nil and type(opt[4]) == "boolean" then values = { "yes", "no" } -- ignore existing input value = "" end -- match values starts with value first local found_candidates = {} local nokey = self:config("nokey") for _, v in ipairs(self:_find_candidates(values, value)) do if nokey then table.insert(found_candidates, { value = v, is_complete = true }) else table.insert(found_candidates, { value = format("--%s=%s", name, v), is_complete = true }) end end self:_print_candidates(found_candidates) -- whether any candidates has been found, finish complete since we don't have more info return true end -- complete keys of kv function completer:_complete_option_kv_k(options, current, completing, name) local opcandi = table.new(0, 10) for _, opt in ipairs(options) do if opt[2] and current[opt[2]] == nil and (opt[3] == "kv" or opt[3] == "k") then opcandi[opt[2]] = opt end end local found_candidates = {} for _, k in ipairs(self:_find_candidates((table.keys(opcandi)), name)) do local opt = opcandi[k] local name = format((opt[3] == "k") and "--%s" or "--%s=", opt[2]) table.insert(found_candidates, { value = name, description = opt[5], is_complete = (opt[3] == "k") }) end self:_print_candidates(found_candidates) return true end function completer:_complete_option_kv(options, current, completing) local name, value if completing == "-" or completing == "--" then name = "" elseif completing:startswith("--") then local parg = cli.parsev({completing})[1] if parg.type == "option" then name, value = parg.key, parg.value elseif parg.type == "flag" then name = parg.key end elseif completing:startswith("-") then -- search full names only return true end if value then -- complete values return self:_complete_option_kv_v(options, current, completing, name, value) else -- complete keys return self:_complete_option_kv_k(options, current, completing, name) end end -- complete options v and vs function completer:_complete_option_v(options, current, completing) -- find completion option local opt local optvs for _, v in ipairs(options) do if v[3] == "v" and current[v[2] or v[1]] == nil then opt = v end if v[3] == "vs" and not optvs then optvs = v end end -- transform values array to candidates array local function _transform_values(values) local candidates = {} if #values > 0 and type(values[1]) == "string" then for _, v in ipairs(values) do if v:startswith(completing) then table.insert(candidates, { value = v, is_complete = true }) end end else for _, v in ipairs(values) do v.is_complete = true table.insert(candidates, v) end end return candidates end -- filter candidates with completing local function _filter_candidates(candidates) local found_candidates = {} for _, v in ipairs(candidates) do if v.value:find(completing, 1, true) then v.is_complete = true table.insert(found_candidates, v) end end return found_candidates end -- get completion candidates from values option local function _values_into_candidates(values) if values == nil then return {} elseif type(values) == "function" then -- no need to filter result of values() as we consider values() already filter candidates return _transform_values(values(completing, current)) else return _filter_candidates(_transform_values(values)) end end -- get candidates from values option local found_candidates = {} if opt then found_candidates = _values_into_candidates(opt.values) end -- get candidates from values option if optvs and #found_candidates == 0 then found_candidates = _values_into_candidates(optvs.values) end self:_print_candidates(found_candidates) return #found_candidates > 0 end function completer:_complete_option(options, segs, completing) local current_options = try { function() return option.raw_parse(segs, options, { populate_defaults = false, allow_unknown = true }) end } -- current options is invalid if not current_options then return false end -- current context is wrong if not self:config("reenter") and (current_options.file or current_options.project) then local args = {"lua", "private.utils.complete", tostring(position), table.concat(self._CONFIG, "-") .. "-reenter", table.unpack(raw_words) } if current_options.file then table.insert(args, 2, "--file=" .. current_options.file) end if current_options.project then table.insert(args, 2, "--project=" .. current_options.project) end os.execv(os.programfile(), args) return true end if completing:startswith("-") then return self:_complete_option_kv(options, current_options, completing) else return self:_complete_option_v(options, current_options, completing) end end function completer:complete(items, argv, completing) local shortnames = table.new(0, 10) for v, menu in pairs(items) do if menu.shortname then shortnames[menu.shortname] = v end end if #argv == 0 then if self:_complete_item(items, completing) then return end end local item_name = self:default_command() if argv[1] and not argv[1]:startswith("-") then item_name = table.remove(argv, 1) end if shortnames[item_name] then item_name = shortnames[item_name] end local options if items[item_name] then options = items[item_name].options end if options then self:_complete_option(options, argv, completing) end end function completer:set_words(raw_words) self._WORDS = raw_words end function completer:words() return self._WORDS end function completer:set_position(pos) self._POSITION = pos end function completer:position() return self._POSITION end function completer:set_default_command(command) self._DEFAULT_COMMAND = command end function completer:default_command() return self._DEFAULT_COMMAND end function completer:set_config(config) self._CONFIG = (config or ""):trim():split("-") end function completer:config(key) return table.find_first(self._CONFIG, key) end function new(...) return completer.new(...) end