-- Copyright 2011-17 Paul Kulchenko, ZeroBrane LLC -- authors: Luxinia Dev (Eike Decker & Christoph Kubisch) --------------------------------------------------------- local ide = ide local q = EscapeMagic -- api loading depends on Lua interpreter -- and loaded specs ------------ -- API local function newAPI(api) api = api or {} for i in pairs(api) do api[i] = nil end -- tool tip info and reserved names api.tip = { staticnames = {}, keys = {}, finfo = {}, finfoclass = {}, shortfinfo = {}, shortfinfoclass = {}, } -- autocomplete hierarchy api.ac = { childs = {}, } return api end local apis = { none = newAPI(), lua = newAPI(), } function GetApi(apitype) return apis[apitype] or apis.none end ---------- -- API loading local function gennames(tab, prefix) for i,v in pairs(tab) do v.classname = (prefix and (prefix..".") or "")..i if (v.childs) then gennames(v.childs,v.classname) end end end local function addAPI(ftype, fname) -- relative to API directory local env = apis[ftype] or newAPI() local res local api = ide.apis[ftype][fname] if type(api) == 'table' then res = api else local fn, err = loadfile(api) if err then ide:Print(TR("Error while loading API file: %s"):format(err)) return end local suc suc, res = pcall(fn, env.ac.childs) if (not suc) then ide:Print(TR("Error while processing API file: %s"):format(res)) return end -- cache the result ide.apis[ftype][fname] = res end apis[ftype] = env gennames(res) for i,v in pairs(res) do env.ac.childs[i] = v end end local function loadallAPIs(only, subapis, known) for ftype, v in pairs(only and {[only] = ide.apis[only]} or ide.apis) do if (not known or known[ftype]) then for fname in pairs(v) do if (not subapis or subapis[fname]) then addAPI(ftype, fname) end end end end end --------- -- ToolTip and reserved words list -- also fixes function descriptions local function fillTips(api,apibasename) local apiac = api.ac local tclass = api.tip tclass.staticnames = {} tclass.keys = {} tclass.finfo = {} tclass.finfoclass = {} tclass.shortfinfo = {} tclass.shortfinfoclass = {} local staticnames = tclass.staticnames local keys = tclass.keys local finfo = tclass.finfo local finfoclass = tclass.finfoclass local shortfinfo = tclass.shortfinfo local shortfinfoclass = tclass.shortfinfoclass local function traverse (tab, libname, format) if not tab.childs then return end format = tab.format or format for key,info in pairs(tab.childs) do local fullkey = (libname ~= "" and libname.."." or "")..key traverse(info, fullkey, format) if info.type == "function" or info.type == "method" or info.type == "value" then local frontname = (info.returns or "(?)").." "..fullkey.." "..(info.args or "(?)") frontname = frontname:gsub("\n"," "):gsub("\t","") local description = info.description or "" -- build info local inf = ((info.type == "value" and "" or frontname.."\n") ..description) local sentence = description:match("^(.-)%. ?\n") local infshort = ((info.type == "value" and "" or frontname.."\n") ..(sentence and sentence.."..." or description)) if type(format) == 'function' then -- apply custom formatting if requested inf = format(fullkey, info, inf) infshort = format(fullkey, info, infshort) end local infshortbatch = (info.returns and info.args) and frontname or infshort -- add to infoclass if not finfoclass[libname] then finfoclass[libname] = {} end if not shortfinfoclass[libname] then shortfinfoclass[libname] = {} end finfoclass[libname][key] = inf shortfinfoclass[libname][key] = infshort -- add to info if not finfo[key] or #finfo[key]<200 then if finfo[key] then finfo[key] = finfo[key] .. "\n\n" else finfo[key] = "" end finfo[key] = finfo[key] .. inf elseif not finfo[key]:match("\n %(%.%.%.%)$") then finfo[key] = finfo[key].."\n (...)" end -- add to shortinfo if not shortfinfo[key] or #shortfinfo[key]<200 then if shortfinfo[key] then shortfinfo[key] = shortfinfo[key] .. "\n" else shortfinfo[key] = "" end shortfinfo[key] = shortfinfo[key] .. infshortbatch elseif not shortfinfo[key]:match("\n %(%.%.%.%)$") then shortfinfo[key] = shortfinfo[key].."\n (...)" end end if info.type == "keyword" then keys[key] = true end staticnames[key] = true end end traverse(apiac,apibasename) end local function generateAPIInfo(only) for i,api in pairs(apis) do if ((not only) or i == only) then fillTips(api,"") end end end local function updateAssignCache(editor) if (editor.spec.typeassigns and not editor.assignscache) then local assigns = editor.spec.typeassigns(editor) editor.assignscache = { assigns = assigns, line = editor:GetCurrentLine(), } end end -- assumes a tidied up string (no spaces, braces..) local function resolveAssign(editor,tx) local ac = editor.api.ac local sep = editor.spec.sep local anysep = "["..q(sep).."]" local assigns = editor.assignscache and editor.assignscache.assigns local function getclass(tab,a) local key,rest = a:match("([%w_]+)"..anysep.."(.*)") key = tonumber(key) or key -- make this work for childs[0] if (key and rest and tab.childs) then if (tab.childs[key]) then return getclass(tab.childs[key],rest) end -- walk inheritance if we weren't in childs if tab.inherits then local bestTab = tab local bestRest = a for base in tab.inherits:gmatch("[%w_"..q(sep).."]+") do local tab = ac -- map "a.b.c" to class hierarchy (a.b.c) for class in base:gmatch("[%w_]+") do tab = tab.childs[class] end if tab then local t,r = getclass(tab, a) if (string.len(r) < string.len(bestRest)) then --we found a better match bestTab = t bestRest = r end end end -- did we find anything good in our inherits, then return it if string.len(bestRest) < string.len(a) then return bestTab, bestRest end end end -- process valuetype, but only if it doesn't reference the current tab if (tab.valuetype and tab ~= ac.childs[tab.valuetype]) then return getclass(ac,tab.valuetype..sep:sub(1,1)..a) end return tab,a end local c if (assigns) then -- find assign local change, n, refs, stopat = true, 0, {}, os.clock() + 0.2 while (change) do -- abort the check if the auto-complete is taking too long if n > 50 and os.clock() > stopat then if ide.config.acandtip.warning then ide:Print("Warning: Auto-complete was aborted after taking too long to complete." .. " Please report this warning along with the text you were typing to support@zerobrane.com.") end break else n = n + 1 end local classname = nil c = "" change = false for w,s in tx:gmatch("([%w_]+)("..anysep.."?)") do local old = classname -- check if what we have so far can be matched with a class name -- this can happen if it's a reference to a value with a known type classname = classname or assigns[c..w] if (s ~= "" and old ~= classname) then -- continue checking unless this can lead to recursive substitution if refs[w] then change = false; break end c = classname..s else c = c..w..s end refs[w] = true end -- check for loops in type assignment if refs[tx] then break end refs[tx] = true tx = c -- if there is any class duplication, abort the loop if classname and select(2, c:gsub(classname, classname)) > 1 then break end end else c = tx end -- then work from api return getclass(ac,c) end function GetTipInfo(editor, content, short, fullmatch) if not content then return end updateAssignCache(editor) -- try to resolve the class content = content:gsub("%b[]",".0") local tab = resolveAssign(editor, content) local sep = editor.spec.sep local anysep = "["..q(sep).."]" local caller = content:match("([%w_]+)%s*%(?%s*$") local class = (tab and tab.classname or caller and content:match("([%w_]+)"..anysep..caller.."%s*%(?%s*$") or "") local tip = editor.api.tip local classtab = short and tip.shortfinfoclass or tip.finfoclass local funcstab = short and tip.shortfinfo or tip.finfo if (editor.assignscache and not (class and classtab[class])) then local assigns = editor.assignscache.assigns class = assigns and assigns[class] or class end local res = (caller and (class and classtab[class]) and classtab[class][caller] or (not fullmatch and funcstab[caller] or nil)) -- some values may not have descriptions (for example, true/false); -- don't return empty strings as they are displayed as empty tooltips. return res and #res > 0 and res or nil end local function reloadAPI(only, subapis, known) if only then newAPI(apis[only]) end loadallAPIs(only, subapis, known) generateAPIInfo(only) end function ReloadAPIs(group, known) -- special case to reload all if group == "*" then if not known then known = {} for _, spec in pairs(ide.specs) do if (spec.apitype) then known[spec.apitype] = true end end -- by default load every known api except lua known.lua = false end reloadAPI(nil, nil, known) return end local interp = ide.interpreter local cfgapi = ide.config.api local fname = interp and interp.fname local intapi = cfgapi and fname and cfgapi[fname] local apinames = {} -- general APIs as configured for _, v in ipairs(type(cfgapi) == 'table' and cfgapi or {}) do apinames[v] = true end -- interpreter-specific APIs as configured for _, v in ipairs(type(intapi) == 'table' and intapi or {}) do apinames[v] = true end -- interpreter APIs for _, v in ipairs(interp and interp.api or {}) do apinames[v] = true end reloadAPI(group, apinames, known) end ------------- -- Dynamic Words local dywordentries = {} local dynamicwords = {} local function addDynamicWord (api,word) if api.tip.keys[word] or api.tip.staticnames[word] then return end local cnt = dywordentries[word] if cnt then dywordentries[word] = cnt + 1 return end dywordentries[word] = 1 local wlow = word:lower() for i=0,#word do local k = wlow:sub(1,i) dynamicwords[k] = dynamicwords[k] or {} table.insert(dynamicwords[k], word) end end local function removeDynamicWord (api,word) if api.tip.keys[word] or api.tip.staticnames[word] then return end local cnt = dywordentries[word] if not cnt then return end if (cnt == 1) then dywordentries[word] = nil for i=0,#word do local wlow = word:lower() local k = wlow : sub (1,i) local page = dynamicwords[k] if page then local cnt = #page for n=1,cnt do if page[n] == word then if cnt == 1 then dynamicwords[k] = nil else table.remove(page,n) end break end end end end else dywordentries[word] = cnt - 1 end end function DynamicWordsReset () dywordentries = {} dynamicwords = {} end local function getEditorLines(editor,line,numlines) return editor:GetTextRangeDyn( editor:PositionFromLine(line),editor:PositionFromLine(line+numlines+1)) end function DynamicWordsAdd(editor,content,line,numlines) if ide.config.acandtip.nodynwords then return end local api = editor.api local anysep = "["..q(editor.spec.sep).."]" content = content or getEditorLines(editor,line,numlines) for word in content:gmatch(anysep.."?%s*([a-zA-Z_]+[a-zA-Z_0-9]+)") do addDynamicWord(api,word) end end function DynamicWordsRem(editor,content,line,numlines) if ide.config.acandtip.nodynwords then return end local api = editor.api local anysep = "["..q(editor.spec.sep).."]" content = content or getEditorLines(editor,line,numlines) for word in content:gmatch(anysep.."?%s*([a-zA-Z_]+[a-zA-Z_0-9]+)") do removeDynamicWord(api,word) end end function DynamicWordsRemoveAll(editor) if ide.config.acandtip.nodynwords then return end DynamicWordsRem(editor,editor:GetTextDyn()) end ------------ -- Final Autocomplete local cachemain = {} local cachemethod = {} local laststrategy local function getAutoCompApiList(childs,fragment,method) if type(childs) ~= "table" then return {} end fragment = fragment:lower() local strategy = ide.config.acandtip.strategy if (laststrategy ~= strategy) then cachemain = {} cachemethod = {} laststrategy = strategy end local cache = method and cachemethod or cachemain if (strategy == 2) then local wlist = cache[childs] if not wlist then wlist = " " for i,v in pairs(childs) do -- in some cases (tip.finfo), v may be a string; check for that first. -- if a:b typed, then value (type == "value") not allowed -- if a.b typed, then method (type == "method") not allowed if type(v) ~= 'table' or (v.type and ((method and v.type ~= "value") or (not method and v.type ~= "method"))) then wlist = wlist..i.." " end end cache[childs] = wlist end local ret = {} local g = string.gmatch local pat = fragment ~= "" and ("%s("..fragment:gsub(".", function(c) local l = c:lower()..c:upper() return "["..l.."][%w_]*" end)..")") or "([%w_]+)" pat = pat:gsub("%s","") for c in g(wlist,pat) do table.insert(ret,c) end return ret end if cache[childs] and cache[childs][fragment] then return cache[childs][fragment] end local t = {} cache[childs] = t local sub = strategy == 1 for key,v in pairs(childs) do -- in some cases (tip.finfo), v may be a string; check for that first. -- if a:b typed, then value (type == "value") not allowed -- if a.b typed, then method (type == "method") not allowed if type(v) ~= 'table' or (v.type and ((method and v.type ~= "value") or (not method and v.type ~= "method"))) then local used = {} local kl = key:lower() for i=0,#key do local k = kl:sub(1,i) t[k] = t[k] or {} used[k] = true table.insert(t[k],key) end if (sub) then -- find camel case / _ separated subwords -- glfwGetGammaRamp -> g, gg, ggr -- GL_POINT_SPRIT -> g, gp, gps local last = "" for ks in string.gmatch(key,"([A-Z%d]*[a-z%d]*_?)") do local k = last..(ks:sub(1,1):lower()) last = k t[k] = t[k] or {} if (not used[k]) then used[k] = true table.insert(t[k],key) end end end end end return t[fragment] or {} end function CreateAutoCompList(editor,key,pos) local api = editor.api local tip = api.tip local ac = api.ac local sep = editor.spec.sep local method = key:match(":[^"..q(sep).."]*$") ~= nil -- ignore keywords if tip.keys[key] then return end updateAssignCache(editor) local tab,rest = resolveAssign(editor,key) local progress = tab and tab.childs ide:SetStatusFor(progress and tab.classname and ("Auto-completing '%s'..."):format(tab.classname) or "") if not progress then return end if (tab == ac) then local _, krest = rest:match("([%w_]+)["..q(sep).."]([%w_]*)%s*$") if (krest) then tab = #krest >= (ide.config.acandtip.startat or 2) and tip.finfo or {} rest = krest:gsub("[^%w_]","") else rest = rest:gsub("[^%w_]","") end else rest = rest:gsub("[^%w_]","") end -- list from api local apilist = getAutoCompApiList(tab.childs or tab,rest,method) local function addInheritance(tab, apilist, seen) if not tab.inherits then return end for base in tab.inherits:gmatch("[%w_"..q(sep).."]+") do local tab = ac -- map "a.b.c" to class hierarchy (a.b.c) for class in base:gmatch("[%w_]+") do tab = tab.childs[class] end if tab and not seen[tab] then seen[tab] = true for _,v in pairs(getAutoCompApiList(tab.childs,rest,method)) do table.insert(apilist, v) end addInheritance(tab, apilist, seen) end end end -- handle (multiple) inheritance; add matches from the parent class/lib addInheritance(tab, apilist, {[tab] = true}) -- include local/global variables if ide.config.acandtip.symbols and not key:find(q(sep)) then local vars, context = {} local tokens = editor:GetTokenList() local strategy = tonumber(ide.config.acandtip.symbols) local tkey = "^"..(strategy == 2 and key:gsub(".", "%1.*"):gsub("%.%*$","") or q(key)) :gsub("(%w)", function(s) return s == s:upper() and s or "["..s:lower()..s:upper().."]" end) for _, token in ipairs(tokens) do if token.fpos and pos and token.fpos > pos then break end if token[1] == 'Id' or token[1] == 'Var' then local var = token.name if var:find(tkey) -- skip the variable formed by what's being typed and (not token.fpos or not pos or token.fpos < pos-#key) then -- if it's a global variable, store in the auto-complete list, -- but if it's local, store separately as it needs to be checked table.insert(token.context[var] and vars or apilist, var) end context = token.context -- keep track of the last (innermost) context end end for _, var in pairs(context and vars or {}) do if context[var] then table.insert(apilist, var) end end end -- include dynamic words local last = key:match("([%w_]+)%s*$") if (last and #last >= (ide.config.acandtip.startat or 2)) then last = last:lower() for _, v in ipairs(dynamicwords[last] or {}) do -- ignore if word == last and sole user if (v:lower() == last and dywordentries[v] == 1) then break end table.insert(apilist, v) end end local li if apilist then if (#rest > 0 and #apilist > 1) then local strategy = ide.config.acandtip.strategy if (strategy == 2 and #apilist < 128) then -- when matching "ret": "ret." < "re.t" < "r.et" -- only do this for the first 32 captures as this is the default in Lua; -- having more captures will trigger "too many captures" error local MAXCAPTURES = 32 local patany = rest:gsub("()(.)", function(p,c) return "["..c:lower()..c:upper().."]"..(p<=MAXCAPTURES and "(.-)" or "") end) local patcase = rest:gsub("()(.)", function(p,c) return c..(p<=MAXCAPTURES and "(.-)" or "") end) local weights = {} local penalty = 0.1 local function weight(str) if not weights[str] then local w = 0 str:gsub(patany,function(...) local l = {...} -- penalize gaps between matches, more so at the beginning for n, v in ipairs(l) do w = w + #v * (1 + (#l-n)*penalty) end end) weights[str] = w + (str:find(patcase) and 0 or penalty) end return weights[str] end table.sort(apilist,function(a,b) local ma, mb = weight(a), weight(b) if (ma == mb) then return a:lower() 1024 and li:sub(1,1024).."..." or li end