-- Copyright 2011-14 Paul Kulchenko, ZeroBrane LLC -- authors: Lomtik Software (J. Winwood & John Labenski) -- Luxinia Dev (Eike Decker & Christoph Kubisch) -- David Manura --------------------------------------------------------- -- Equivalent to C's "cond ? a : b", all terms will be evaluated function iff(cond, a, b) if cond then return a else return b end end function EscapeMagic(s) return s:gsub('([%(%)%.%%%+%-%*%?%[%^%$%]])','%%%1') end -- Does the num have all the bits in value function HasBit(value, num) for n = 32, 0, -1 do local b = 2^n local num_b = num - b local value_b = value - b if num_b >= 0 then num = num_b else return true -- already tested bits in num end if value_b >= 0 then value = value_b end if (num_b >= 0) and (value_b < 0) then return false end end return true end function GetPathSeparator() return string.char(wx.wxFileName.GetPathSeparator()) end do local sep = GetPathSeparator() function IsDirectory(dir) return dir:find(sep.."$") end end function StripCommentsC(tx) local out = "" local lastc = "" local skip local skipline local skipmulti local tx = string.gsub(tx, "\r\n", "\n") for c in tx:gmatch(".") do local oc = c local tu = lastc..c skip = c == '/' if ( not (skipmulti or skipline)) then if (tu == "//") then skipline = true elseif (tu == "/*") then skipmulti = true c = "" elseif (lastc == '/') then oc = tu end elseif (skipmulti and tu == "*/") then skipmulti = false c = "" elseif (skipline and lastc == "\n") then out = out.."\n" skipline = false end lastc = c if (not (skip or skipline or skipmulti)) then out = out..oc end end return out..lastc end -- http://lua-users.org/wiki/EnhancedFileLines function FileLines(f) local CHUNK_SIZE = 1024 local buffer = "" local pos_beg = 1 return function() local pos, chars while 1 do pos, chars = buffer:match('()([\r\n].)', pos_beg) if pos or not f then break elseif f then local chunk = f:read(CHUNK_SIZE) if chunk then buffer = buffer:sub(pos_beg) .. chunk pos_beg = 1 else f = nil end end end if not pos then pos = #buffer elseif chars == '\r\n' then pos = pos + 1 end local line = buffer:sub(pos_beg, pos) pos_beg = pos + 1 if #line > 0 then return line end end end function PrependStringToArray(t, s, maxstrings, issame) if string.len(s) == 0 then return end for i = #t, 1, -1 do local v = t[i] if v == s or issame and issame(s, v) then table.remove(t, i) -- remove old copy -- don't break here in case there are multiple copies to remove end end table.insert(t, 1, s) if #t > (maxstrings or 15) then table.remove(t, #t) end -- keep reasonable length end function GetFileModTime(filePath) if filePath and #filePath > 0 then local fn = wx.wxFileName(filePath) if fn:FileExists() then return fn:GetModificationTime() end end return nil end function GetFileExt(filePath) local match = filePath and filePath:match("%.([^./\\]*)$") return match and match:lower() or '' end function IsLuaFile(filePath) return filePath and (string.len(filePath) > 4) and (string.lower(string.sub(filePath, -4)) == ".lua") end function GetPathWithSep(wxfn) if type(wxfn) == 'string' then wxfn = wx.wxFileName(wxfn) end return wxfn:GetPath(bit.bor(wx.wxPATH_GET_VOLUME, wx.wxPATH_GET_SEPARATOR)) end function FileDirHasContent(dir) local f = wx.wxFindFirstFile(dir, wx.wxFILE + wx.wxDIR) return #f>0 end function FileSysGetRecursive(path, recursive, spec, skip) spec = spec or "*" local content = {} local sep = GetPathSeparator() -- recursion is done in all folders but only those folders that match -- the spec are returned. This is the pattern that matches the spec. local specmask = spec:gsub("%.", "%%."):gsub("%*", ".*").."$" local function getDir(path, spec) local dir = wx.wxDir(path) if not dir:IsOpened() then return end local log = wx.wxLogNull() -- disable error reporting; will report as needed local found, file = dir:GetFirst("*", wx.wxDIR_DIRS) while found do if not skip or not file:find(skip) then local fname = wx.wxFileName(path, file):GetFullPath() if fname:find(specmask) then table.insert(content, fname..sep) end -- check if this name already appears in the path earlier; -- Skip the processing if it does as it could lead to infinite -- recursion with circular references created by symlinks. if recursive and select(2, fname:gsub(EscapeMagic(file..sep),'')) <= 2 then getDir(fname, spec) end end found, file = dir:GetNext() end local found, file = dir:GetFirst(spec, wx.wxDIR_FILES) while found do if not skip or not file:find(skip) then local fname = wx.wxFileName(path, file):GetFullPath() table.insert(content, fname) end found, file = dir:GetNext() end end getDir(path, spec) local prefix = '\001' -- prefix to sort directories first local shadow = {} for _, v in ipairs(content) do shadow[v] = (v:sub(-1) == sep and prefix or '')..v:lower() end table.sort(content, function(a,b) return shadow[a] < shadow[b] end) return content end local normalflags = wx.wxPATH_NORM_ABSOLUTE + wx.wxPATH_NORM_DOTS + wx.wxPATH_NORM_TILDE function GetFullPathIfExists(p, f) if not p or not f then return end local file = wx.wxFileName(f) -- Normalize call is needed to make the case of p = '/abc/def' and -- f = 'xyz/main.lua' work correctly. Normalize() returns true if done. return (file:Normalize(normalflags, p) and file:FileExists() and file:GetFullPath() or nil) end function MergeFullPath(p, f) if not p or not f then return end local file = wx.wxFileName(f) -- Normalize call is needed to make the case of p = '/abc/def' and -- f = 'xyz/main.lua' work correctly. Normalize() returns true if done. return (file:Normalize(normalflags, p) and file:GetFullPath() or nil) end function FileWrite(file, content) local log = wx.wxLogNull() -- disable error reporting; will report as needed if not wx.wxFileExists(file) and not wx.wxFileName(file):Mkdir(tonumber(755,8), wx.wxPATH_MKDIR_FULL) then return nil, wx.wxSysErrorMsg() end local file = wx.wxFile(file, wx.wxFile.write) if not file:IsOpened() then return nil, wx.wxSysErrorMsg() end file:Write(content, #content) file:Close() return true end function FileRead(file) -- on OSX "Open" dialog allows to open applications, which are folders if wx.wxDirExists(file) then return nil, "Can't read directory as file." end local log = wx.wxLogNull() -- disable error reporting; will report as needed local file = wx.wxFile(file, wx.wxFile.read) if not file:IsOpened() then return nil, wx.wxSysErrorMsg() end local _, content = file:Read(file:Length()) file:Close() return content, wx.wxSysErrorMsg() end function FileRename(file1, file2) local log = wx.wxLogNull() -- disable error reporting; will report as needed return wx.wxRenameFile(file1, file2), wx.wxSysErrorMsg() end function FileCopy(file1, file2) local log = wx.wxLogNull() -- disable error reporting; will report as needed return wx.wxCopyFile(file1, file2), wx.wxSysErrorMsg() end local ok, socket = pcall(require, "socket") TimeGet = ok and socket.gettime or os.clock function isBinary(text) return text:find("[^\7\8\9\10\12\13\27\32-\255]") end function pairsSorted(t, f) local a = {} for n in pairs(t) do table.insert(a, n) end table.sort(a, f) local i = 0 -- iterator variable local iter = function () -- iterator function i = i + 1 if a[i] == nil then return nil else return a[i], t[a[i]] end end return iter end function FixUTF8(s, repl) local p, len, invalid = 1, #s, {} while p <= len do if p == s:find("[%z\1-\127]", p) then p = p + 1 elseif p == s:find("[\194-\223][\128-\191]", p) then p = p + 2 elseif p == s:find( "\224[\160-\191][\128-\191]", p) or p == s:find("[\225-\236][\128-\191][\128-\191]", p) or p == s:find( "\237[\128-\159][\128-\191]", p) or p == s:find("[\238-\239][\128-\191][\128-\191]", p) then p = p + 3 elseif p == s:find( "\240[\144-\191][\128-\191][\128-\191]", p) or p == s:find("[\241-\243][\128-\191][\128-\191][\128-\191]", p) or p == s:find( "\244[\128-\143][\128-\191][\128-\191]", p) then p = p + 4 else local repl = type(repl) == 'function' and repl(s:sub(p,p)) or repl s = s:sub(1, p-1)..repl..s:sub(p+1) table.insert(invalid, p) -- adjust position/length as the replacement may be longer than one char p = p + #repl len = len + #repl - 1 end end return s, invalid end function RequestAttention() local frame = ide.frame if not frame:IsActive() then frame:RequestUserAttention() if ide.osname == "Macintosh" then local cmd = [[osascript -e 'tell application "%s" to activate']] wx.wxExecute(cmd:format(ide.editorApp:GetAppName()), wx.wxEXEC_ASYNC) elseif ide.osname == "Windows" then if frame:IsIconized() then frame:Iconize(false) end frame:Raise() -- raise the window local winapi = require 'winapi' if winapi then local pid = winapi.get_current_pid() local wins = winapi.find_all_windows(function(w) return w:get_process():get_pid() == pid and w:get_class_name() == 'wxWindowNR' end) if wins and #wins > 0 then -- found the window, now need to activate it: -- send some input to the window and then -- bring our window to foreground (doesn't work without some input) -- send Attn key twice (down and up) winapi.send_to_window(0xF6, false) winapi.send_to_window(0xF6, true) for _, w in ipairs(wins) do w:set_foreground() end end end end end end local messages, lang, counter function TR(msg, count) lang = lang or ide.config.language messages = messages or ide.config.messages counter = counter or (messages[lang] and messages[lang][0]) local message = messages[lang] and messages[lang][msg] return count and counter and message and type(message) == 'table' and message[counter(count)] or message or msg end -- wxwidgets 2.9.x may report the last folder twice (depending on how the -- user selects the folder), which makes the selected folder incorrect. -- check if the last segment is repeated and drop it. function FixDir(path) if wx.wxDirExists(path) then return path end local dir = wx.wxFileName.DirName(path) local dirs = dir:GetDirs() if #dirs > 1 and dirs[#dirs] == dirs[#dirs-1] then dir:RemoveLastDir() end return dir:GetFullPath() end function ShowLocation(fname) local osxcmd = [[osascript -e 'tell application "Finder" to reveal POSIX file "%s"']] .. [[ -e 'tell application "Finder" to activate']] local wincmd = [[explorer /select,"%s"]] local lnxcmd = [[xdg-open "%s"]] -- takes path, not a filename local cmd = ide.osname == "Windows" and wincmd:format(fname) or ide.osname == "Macintosh" and osxcmd:format(fname) or ide.osname == "Unix" and lnxcmd:format(wx.wxFileName(fname):GetPath()) if cmd then wx.wxExecute(cmd, wx.wxEXEC_ASYNC) end end function LoadLuaFileExt(tab, file, proto) local cfgfn,err = loadfile(file) local report = DisplayOutputLn or print if not cfgfn then report(("Error while loading file: '%s'."):format(err)) else local name = file:match("([a-zA-Z_0-9%-]+)%.lua$") if not name then return end -- check if os/arch matches to allow packages for different systems local osvals = {windows = true, unix = true, macintosh = true} local archvals = {x64 = true, x86 = true} local os, arch = name:match("-(%w+)-?(%w*)") if os and os:lower() ~= ide.osname:lower() and osvals[os:lower()] or arch and #arch > 0 and arch:lower() ~= ide.osarch:lower() and archvals[arch:lower()] then return end if os and osvals[os:lower()] then name = name:gsub("-.*","") end local success, result = pcall(function()return cfgfn(assert(_G or _ENV))end) if not success then report(("Error while processing file: '%s'."):format(result)) else if (tab[name]) then local out = tab[name] for i,v in pairs(result) do out[i] = v end else tab[name] = proto and result and setmetatable(result, proto) or result end end end end function LoadLuaConfig(filename,isstring) if not filename then return end -- skip those files that don't exist if not isstring and not wx.wxFileName(filename):FileExists() then return end -- if it's marked as command, but exists as a file, load it as a file if isstring and wx.wxFileName(filename):FileExists() then isstring = false end local cfgfn, err, msg if isstring then msg, cfgfn, err = "string", loadstring(filename) else msg, cfgfn, err = "file", loadfile(filename) end local report = DisplayOutputLn or print if not cfgfn then report(("Error while loading configuration %s: '%s'."):format(msg, err)) else setfenv(cfgfn,ide.config) local _, err = pcall(function()cfgfn(assert(_G or _ENV))end) if err then report(("Error while processing configuration %s: '%s'."):format(msg, err)) end end end function LoadSafe(data) local f, res = loadstring(data) if not f then return f, res end local count = 0 debug.sethook(function () count = count + 1 if count >= 3 then error("cannot call functions") end end, "c") local ok, res = pcall(f) count = 0 debug.sethook() return ok, res end local function isCtrlFocused(e) local ctrl = e and e:FindFocus() return ctrl and (ctrl:GetId() == e:GetId() or ide.osname == 'Macintosh' and ctrl:GetParent():GetId() == e:GetId()) and ctrl or nil end function GetEditorWithFocus(...) -- need to distinguish GetEditorWithFocus() and GetEditorWithFocus(nil) -- as the latter may happen when GetEditor() is passed and returns `nil` if select('#', ...) > 0 then local ed = ... return isCtrlFocused(ed) and ed or nil end local bnb = ide.frame.bottomnotebook for _, e in pairs({bnb.shellbox, bnb.errorlog}) do if isCtrlFocused(e) then return e end end local editor = GetEditor() return isCtrlFocused(editor) and editor or nil end function GenerateProgramFilesPath(exec, sep) local env = os.getenv('ProgramFiles') return (env and env..'\\'..exec..sep or '').. [[C:\Program Files\]]..exec..sep.. [[D:\Program Files\]]..exec..sep.. [[C:\Program Files (x86)\]]..exec..sep.. [[D:\Program Files (x86)\]]..exec end --[[ format placeholders - %f -- full project name (project path) - %s -- short project name (directory name) - %i -- interpreter name - %S -- file name - %F -- file path - %n -- line number - %c -- line content - %T -- application title - %v -- application version - %t -- current tab name --]] function ExpandPlaceholders(msg, ph) ph = ph or {} if type(msg) == 'function' then return msg(ph) end local editor = ide:GetEditor() local proj = ide:GetProject() or "" local dirs = wx.wxFileName(proj):GetDirs() local doc = editor and ide:GetDocument(editor) local nb = ide:GetEditorNotebook() local def = { f = proj, s = dirs[#dirs] or "", i = ide:GetInterpreter():GetName() or "", S = doc and doc:GetFileName() or "", F = doc and doc:GetFilePath() or "", n = editor and editor:GetCurrentLine()+1 or 0, c = editor and editor:GetLine(editor:GetCurrentLine()) or "", T = GetIDEString("editor") or "", v = ide.VERSION, t = editor and nb:GetPageText(nb:GetPageIndex(editor)) or "", } return(msg:gsub('%%(%w)', function(p) return ph[p] or def[p] or '?' end)) end