-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch) --------------------------------------------------------- local ide = ide local statusBar = ide.frame.statusBar -- 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 addAPI(apifile,only,subapis,known) -- relative to API directory local ftype,fname = apifile:match("api[/\\]([^/\\]+)[/\\](.*)%.") if not ftype then DisplayOutputLn(TR("The API file must be located in a subdirectory of the API directory.")) return end if ((only and ftype ~= only) or (known and not known[ftype])) then return end if (subapis and not subapis[fname]) then return end local fn,err = loadfile(apifile) if err then DisplayOutputLn(TR("Error while loading API file: %s"):format(err)) return end local env = apis[ftype] or newAPI() apis[ftype] = env env = env.ac.childs local suc,res = pcall(function()return fn(env) end) if (not suc) then DisplayOutputLn(TR("Error while processing API file: %s"):format(res)) elseif (res) then 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 gennames(res) for i,v in pairs(res) do env[i] = v end end end local function loadallAPIs (only,subapis,known) for _, dir in ipairs(FileSysGet("api/*", wx.wxDIR)) do for _, file in ipairs(FileSysGet(dir.."/*.*", wx.wxFILE)) do if file:match "%.lua$" then addAPI(file,only,subapis,known) end end end end --------- -- ToolTip and reserved words list -- also fixes function descriptions local tipwidth = math.max(20, ide.config.acandtip.width or 60) local widthmask = ("[^\n]"):rep(tipwidth-10)..("[^\n]?"):rep(10) local function fillTips(api,apibasename,apiname) 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) if not tab.childs then return end for key,info in pairs(tab.childs) do traverse(info,key) if info.type == "function" then local libstr = libname ~= "" and libname.."." or "" -- fix description local frontname = (info.returns or "(?)").." "..libstr..key.." "..(info.args or "(?)") frontname = frontname :gsub("\n"," ") :gsub("\t","") :gsub("("..widthmask..")[ \t]([^%)])","%1\n %2") info.description = info.description :gsub("\n\n","
"):gsub("\n"," "):gsub("
","\n") :gsub("[ \t]+"," ") :gsub("("..widthmask..") ","%1\n") -- build info local inf = frontname.."\n"..info.description local sentence = info.description:match("^(.-)%. ?\n") local infshort = frontname.."\n"..(sentence and sentence.."..." or info.description) 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,"",i) end end end 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 assigns = editor.assignscache and editor.assignscache.assigns local function getclass(tab,a) local key,rest = a:match("([%w_]+)[%.:](.*)") if (key and rest and tab.childs and tab.childs[key]) then return getclass(tab.childs[key],rest) end if (tab.valuetype) then return getclass(ac,tab.valuetype.."."..a) end return tab,a end local classname local c = "" if (assigns) then -- find assign for w,s in tx:gmatch("([%w_]*)([%.:]?)") do local old = classname classname = classname or (assigns[c..w]) if (s ~= "" and old ~= classname) then c = classname..s else c = c..w..s end end else c = tx end -- then work from api return getclass(ac,c) end function GetTipInfo(editor, content, short) local caller = content:match("([%w_]+)%(%s*$") local class = caller and content:match("([%w_]+)[%.:]"..caller.."%(%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 UpdateAssignCache(editor) if (editor.assignscache and not (class and classtab[class])) then local assigns = editor.assignscache.assigns class = assigns and assigns[class] or class end return caller and (class and classtab[class]) and classtab[class][caller] or funcstab[caller] end local function reloadAPI(only,subapis) newAPI(apis[only]) loadallAPIs(only,subapis) generateAPIInfo(only) end function ReloadLuaAPI() local interpreterapi = ide.interpreter interpreterapi = interpreterapi and interpreterapi.api if (interpreterapi) then local apinames = {} for _, v in ipairs(interpreterapi) do apinames[v] = true end interpreterapi = apinames end reloadAPI("lua",interpreterapi) end do local known = {} for _, spec in pairs(ide.specs) do if (spec.apitype) then known[spec.apitype] = true end end -- by defaul load every known api except lua known.lua = false loadallAPIs(nil,nil,known) generateAPIInfo() 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) local tx = "" for i=0,numlines do tx = tx..editor:GetLine(line + i) end return tx end function DynamicWordsAdd(ev,editor,content,line,numlines) if ide.config.acandtip.nodynwords then return end local api = editor.api local content = content or getEditorLines(editor,line,numlines) for word in content:gmatch "[%.:]?%s*([a-zA-Z_]+[a-zA-Z_0-9]+)" do addDynamicWord(api,word) end end function DynamicWordsRem(ev,editor,content,line,numlines) if ide.config.acandtip.nodynwords then return end local api = editor.api local content = content or getEditorLines(editor,line,numlines) for word in content:gmatch "[%.:]?%s*([a-zA-Z_]+[a-zA-Z_0-9]+)" do removeDynamicWord(api,word) end end function DynamicWordsRemoveAll (editor) local tx = editor:GetText() DynamicWordsRem("close",editor,tx) end ------------ -- Final Autocomplete local cache = {} local laststrategy local function getAutoCompApiList(childs,fragment) fragment = fragment:lower() local strategy = ide.config.acandtip.strategy if (laststrategy ~= strategy) then cache = {}; laststrategy = strategy end if (strategy == 2) then local wlist = cache[childs] if not wlist then wlist = " " for i in pairs(childs) do wlist = wlist..i.." " 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] then return cache[childs][fragment] end local t = {} cache[childs] = t local sub = strategy == 1 for key in pairs(childs) do 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 return t end function ClearAutoCompCache() cache = {} end -- make syntype dependent function CreateAutoCompList(editor,key) local api = editor.api local tip = api.tip local ac = api.ac -- ignore keywords if tip.keys[key] then return end UpdateAssignCache(editor) local tab,rest = resolveAssign(editor,key) local progress = tab and tab.childs statusBar:SetStatusText(progress and tab.classname or "",1) if not (progress) then return end if (tab == ac) then local _, krest = rest:match("([%w_]+)[:%.]([%w_]+)%s*$") if (krest) then if (#krest < 3) then return end tab = tip.finfo rest = krest:gsub("[^%w_]","") else rest = rest:gsub("[^%w_]","") end else rest = rest:gsub("[^%w_]","") end local last = key:match("([%w_]+)%s*$") -- build dynamic word list -- only if api search couldnt descend -- ie we couldnt find matching sub items local dw = "" if (tab == ac and last and #last >= (ide.config.acandtip.startat or 2)) then last = last:lower() if dynamicwords[last] then local list = dynamicwords[last] table.sort(list,function(a,b) local ma,mb = a:sub(1,#last)==last, b:sub(1,#last)==last if (ma and mb) or (not ma and not mb) then return a 0) then local strategy = ide.config.acandtip.strategy if (strategy == 2 and #apilist < 128) then local pat = rest:gsub(".",function(c) local l = c:lower()..c:upper() return "["..l.."]([^"..l.." ]*)" end) local g = string.gsub table.sort(apilist,function(a,b) local ma,mb = 0,0 g(a,pat,function(...) local l = {...} for _, v in ipairs(l) do ma = ma + ((v=="") and 0 or 1) end end) g(b,pat,function(...) local l = {...} for _, v in ipairs(l) do mb = mb + ((v=="") and 0 or 1) end end) if (ma == mb) then return a:lower() 1024 and li:sub(1,1024).."..." or li) end