-- Copyright 2011-17 Paul Kulchenko, ZeroBrane LLC -- authors: Lomtik Software (J. Winwood & John Labenski) -- Luxinia Dev (Eike Decker & Christoph Kubisch) -- David Manura --------------------------------------------------------- function EscapeMagic(s) return s:gsub('([%(%)%.%%%+%-%*%?%[%^%$%]])','%%%1') end function GetPathSeparator() return string.char(wx.wxFileName.GetPathSeparator()) end do local sep = GetPathSeparator() function IsDirectory(dir) return dir:find(sep.."$") 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:gsub("%s+$",""):match("%.([^./\\]*)$") return match and match:lower() or '' end function GetFileName(filePath) return filePath and filePath:gsub("%s+$",""):match("([^/\\]*)$") 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 local function closeDir(dir) -- wxlua < 3.0 doesn't provide Close method for wxDir, so check for it if ide:IsValidProperty(dir, "Close") then dir:Close() end end function FileDirHasContent(path) local dir = wx.wxDir() dir:Open(path) if not dir:IsOpened() then return false end local found = dir:GetFirst("*", wx.wxDIR_DIRS + wx.wxDIR_FILES + (ide.config.showhiddenfiles and wx.wxDIR_HIDDEN or 0)) closeDir(dir) return found end -- `fs` library provides Windows-specific functions and requires LuaJIT local ok, fs = pcall(require, "fs") if not ok then fs = nil end local function isSymlink(path) return require("lfs").attributes(path).mode == "link" end if fs then isSymlink = fs.is_symlink end function FileSysGetRecursive(path, recursive, spec, opts) local content = {} local showhidden = ide.config and ide.config.showhiddenfiles local sep = GetPathSeparator() -- trim trailing separator and adjust the separator in the path path = path:gsub("[\\/]$",""):gsub("[\\/]", sep) local queue = {path} local pathpatt = "^"..EscapeMagic(path)..sep.."?" local optyield = (opts or {}).yield local optfolder = (opts or {}).folder ~= false local optsort = (opts or {}).sort ~= false local optpath = (opts or {}).path ~= false local optfollowsymlink = (opts or {}).followsymlink ~= false local optskipbinary = (opts or {}).skipbinary local optondirectory = (opts or {}).ondirectory local optmaxnum = tonumber((opts or {}).maxnum) local function spec2list(spect, list) -- return empty list if no spec is provided if spect == nil or spect == "*" or spect == "*.*" then return {}, 0 end -- accept "*.lua" and "*.txt,*.wlua" combinations local masknum, list = 0, list or {} for spec, specopt in pairs(type(spect) == 'table' and spect or {spect}) do -- specs can be kept as `{[spec] = true}` or `{spec}`, so handle both cases if type(spec) == "number" then spec = specopt end if specopt == false then spec = "" end -- skip keys with `false` values for m in spec:gmatch("[^%s;,]+") do m = m:gsub("[\\/]", sep) if m:find("^%*%.%w+"..sep.."?$") then list[m:sub(2)] = true else -- escape all special characters table.insert(list, EscapeMagic(m) :gsub("%%%*%%%*", ".*") -- replace (escaped) ** with any character (.*) :gsub("%%%*", "[^/\\]*") -- replace (escaped) * with anything except path separator :gsub("^"..sep, ide:GetProject() or "") -- replace leading separator with project directory (if set) .."$") end masknum = masknum + 1 end end return list, masknum end local inmasks, masknum = spec2list(spec) if masknum >= 2 then spec = nil end local exmasks = spec2list(ide.config.excludelist or {}) if optskipbinary then -- add any binary files to the list to skip exmasks = spec2list(type(optskipbinary) == 'table' and optskipbinary or ide.config.binarylist or {}, exmasks) end local function ismatch(file, inmasks, exmasks) -- convert extension 'foo' to '.foo', as need to distinguish file -- from extension with the same name local ext = '.'..GetFileExt(file) -- check exclusions if needed if exmasks[file] or exmasks[ext] then return false end for _, mask in ipairs(exmasks) do if file:find(mask) then return false end end -- return true if none of the exclusions match and no inclusion list if not inmasks or not next(inmasks) then return true end -- now check inclusions if inmasks[file] or inmasks[ext] then return true end for _, mask in ipairs(inmasks) do if file:find(mask) then return true end end return false end local function report(fname) if optyield then return coroutine.yield(fname) end table.insert(content, fname) end local dir = wx.wxDir() local num = 0 local function getDir(path) dir:Open(path) if not dir:IsOpened() then if TR then ide:Print(TR("Can't open '%s': %s"):format(path, wx.wxSysErrorMsg())) end return true -- report and continue end -- recursion is done in all folders if requested, -- but only those folders that match the spec are returned local _ = wx.wxLogNull() -- disable error reporting; will report as needed local found, file = dir:GetFirst("*", wx.wxDIR_DIRS + ((showhidden == true or showhidden == wx.wxDIR_DIRS) and wx.wxDIR_HIDDEN or 0)) while found do local fname = path..sep..file if optfolder and ismatch(fname..sep, inmasks, exmasks) then report((optpath and fname or fname:gsub(pathpatt, ""))..sep) end -- `optondirectory` may return: -- `true` to traverse the directory and continue -- `false` to skip the directory and continue -- `nil` to abort the process local ondirectory = optondirectory and optondirectory(fname) if optondirectory and ondirectory == nil then closeDir(dir); return false end if recursive and ismatch(fname..sep, nil, exmasks) and (ondirectory ~= false) and (optfollowsymlink or not isSymlink(fname)) -- 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. and select(2, fname:gsub(EscapeMagic(file..sep),'')) <= 2 then table.insert(queue, fname) end num = num + 1 if optmaxnum and num >= optmaxnum then closeDir(dir); return false end found, file = dir:GetNext() end found, file = dir:GetFirst(spec or "*", wx.wxDIR_FILES + ((showhidden == true or showhidden == wx.wxDIR_FILES) and wx.wxDIR_HIDDEN or 0)) while found do local fname = path..sep..file if ismatch(fname, inmasks, exmasks) then report(optpath and fname or fname:gsub(pathpatt, "")) end num = num + 1 if optmaxnum and num >= optmaxnum then closeDir(dir); return false end found, file = dir:GetNext() end closeDir(dir) return true end while #queue > 0 and getDir(table.remove(queue)) do end if optyield then if #queue > 0 then coroutine.yield(false) end -- signal aborted processing return end if optsort then 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) end return content end local normalflags = wx.wxPATH_NORM_ABSOLUTE + wx.wxPATH_NORM_DOTS + wx.wxPATH_NORM_TILDE 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. -- Normalization with PATH_NORM_DOTS removes leading dots, which need to be added back. -- This allows things like `-cfg ../myconfig.lua` to work. local rel, rest = p:match("^(%.[/\\.]*[/\\])(.*)") if rel and rest then p = rest end return (file:Normalize(normalflags, p) and (rel or ""):gsub("[/\\]", GetPathSeparator())..file:GetFullPath() or nil) end function FileNormalizePath(path) local filePath = wx.wxFileName(path) filePath:Normalize() filePath:SetVolume(filePath:GetVolume():upper()) return filePath:GetFullPath() end function FileGetLongPath(path) local fn = wx.wxFileName(path) local vol = fn:GetVolume():upper() local volsep = vol and vol:byte() and wx.wxFileName.GetVolumeSeparator(vol:byte()-("A"):byte()+1) local dir = wx.wxDir() local dirs = fn:GetDirs() table.insert(dirs, fn:GetFullName()) local normalized = vol and volsep and vol..volsep or (path:match("^[/\\]") or ".") while #dirs > 0 do dir:Open(normalized) if not dir:IsOpened() then return path end local p = table.remove(dirs, 1) local ok, segment = dir:GetFirst(p) closeDir(dir) if not ok then return path end normalized = MergeFullPath(normalized,segment) end local file = wx.wxFileName(normalized) file:Normalize(wx.wxPATH_NORM_DOTS) -- remove leading dots, if any return file:GetFullPath() end function CreateFullPath(path) local ok = wx.wxFileName.Mkdir(path, tonumber("755",8), wx.wxPATH_MKDIR_FULL) return ok, not ok and wx.wxSysErrorMsg() or nil end function GetFullPathIfExists(p, f) local path = MergeFullPath(p, f) return path and wx.wxFileExists(path) and path or nil end function FileWrite(file, content) local _ = 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 local ok = file:Write(content, #content) == #content file:Close() return ok, not ok and wx.wxSysErrorMsg() or nil 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 -- check if fs library is available and provide better versions of available functions if fs then function FileWrite(file, content) local _ = 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 -- use `fs` library to write a file, as this preserves its attributes local f, errmsg = fs.open(file, { access = 'write', creation = 'open_always', flags = 'rdwr backup_semantics'}) if not f then return nil, errmsg end local bytes local ok, errmsg = f:truncate() if ok then bytes, errmsg = f:write(content, #content) ok = bytes == #content end f:close() return ok, not ok and errmsg or nil end function ShowLocation(fname) fs.shell_open_and_select(fname) end end function FileSize(fname) if not wx.wxFileExists(fname) then return end local size = wx.wxFileSize(fname) -- size can be returned as 0 for symlinks, so check with wxFile:Length(); -- can't only use wxFile:Length(), as it's reported incorrectly for some non-seekable -- files (see https://github.com/pkulchenko/ZeroBraneStudio/issues/458); -- the combination of wxFileSize and wxFile:Length() should do the right thing. if size == 0 then size = wx.wxFile(fname, wx.wxFile.read):Length() end return size end function FileRead(fname, length, callback) -- on OSX "Open" dialog allows to open applications, which are folders if wx.wxDirExists(fname) then return nil, "Can't read directory as file." end local _ = wx.wxLogNull() -- disable error reporting; will report as needed local file = wx.wxFile(fname, wx.wxFile.read) if not file:IsOpened() then return nil, wx.wxSysErrorMsg() end if type(callback) == 'function' then length = length or 8192 local pos = 0 while true do local len, content = file:Read(length) local res, msg = callback(content) -- may return `false` to signal to stop if res == false then file:Close() return false, msg or "Unknown error" end if len < length then break end pos = pos + len file:Seek(pos) end file:Close() return true, wx.wxSysErrorMsg() end local _, content = file:Read(length or FileSize(fname)) file:Close() return content, wx.wxSysErrorMsg() end function FileRemove(file) local _ = wx.wxLogNull() -- disable error reporting; will report as needed return wx.wxRemoveFile(file), wx.wxSysErrorMsg() end function FileRename(file1, file2) local _ = wx.wxLogNull() -- disable error reporting; will report as needed return wx.wxRenameFile(file1, file2), wx.wxSysErrorMsg() end function FileCopy(file1, file2) local _ = wx.wxLogNull() -- disable error reporting; will report as needed return wx.wxCopyFile(file1, file2), wx.wxSysErrorMsg() end function IsBinary(text) return text and text:find("[^\7\8\9\10\12\13\27\32-\255]") and true or false 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 s:find("^[%z\1-\127]", p) then p = p + 1 elseif s:find("^[\194-\223][\128-\191]", p) then p = p + 2 elseif s:find( "^\224[\160-\191][\128-\191]", p) or s:find("^[\225-\236][\128-\191][\128-\191]", p) or s:find( "^\237[\128-\159][\128-\191]", p) or s:find("^[\238-\239][\128-\191][\128-\191]", p) then p = p + 3 elseif s:find( "^\240[\144-\191][\128-\191][\128-\191]", p) or s:find("^[\241-\243][\128-\191][\128-\191][\128-\191]", p) or s:find( "^\244[\128-\143][\128-\191][\128-\191]", p) then p = p + 4 else if not repl then return end -- just signal invalid UTF8 string 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 TR(msg, count) local messages = ide.messages local lang = ide.config.language local counter = messages[lang] and messages[lang][0] local message = messages[lang] and messages[lang][msg] -- if there is count and no corresponding message, then -- get the message from the (default) english language, -- otherwise the message is not going to be pluralized properly if count and (not message or type(message) == 'table' and not next(message)) then message, counter = messages.en[msg], messages.en[0] end return count and counter and message and type(message) == 'table' and message[counter(count)] or (type(message) == 'string' and 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 LoadLuaFileExt(tab, file, proto) local cfgfn,err = loadfile(file) if not cfgfn then ide:Print(("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 ide:Print(("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 if tab[name] then tab[name].fpath = file end end end end return tab end function LoadLuaConfig(filename,isstring) if not filename then return end -- skip those files that don't exist if not isstring and not wx.wxFileExists(filename) then return end -- if it's marked as command, but exists as a file, load it as a file if isstring and wx.wxFileExists(filename) then isstring = false end local loadstring = loadstring or load local cfgfn, err, msg if isstring then msg, cfgfn, err = "string", loadstring(filename) else msg, cfgfn, err = "file", loadfile(filename) end if not cfgfn then ide:Print(("Error while loading configuration %s: '%s'."):format(msg, err)) else setfenv(cfgfn,ide.config) table.insert(ide.configqueue, (wx.wxFileName.SplitPath(filename))) local _, err = pcall(function()cfgfn(assert(_G or _ENV))end) table.remove(ide.configqueue) if err then ide:Print(("Error while processing configuration %s: '%s'."):format(msg, err)) end end return true end function LoadSafe(data) local loadstring = loadstring or load 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 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 function MergeSettings(localSettings, savedSettings) for name in pairs(localSettings) do if savedSettings[name] ~= nil and type(savedSettings[name]) == type(localSettings[name]) then if type(localSettings[name]) == 'table' and next(localSettings[name]) ~= nil then -- check every value in the table to make sure that it's possible -- to add new keys to the table and they get correct default values -- (even though that are absent in savedSettings) for setting in pairs(localSettings[name]) do if savedSettings[name][setting] ~= nil then localSettings[name][setting] = savedSettings[name][setting] end end else localSettings[name] = savedSettings[name] end end end end function UpdateMenuUI(menu, obj) if not menu or not obj then return end for pos = 0, menu:GetMenuItemCount()-1 do local id = menu:FindItemByPosition(pos):GetId() local uievent = wx.wxUpdateUIEvent(id) obj:ProcessEvent(uievent) menu:Enable(id, not uievent:GetSetEnabled() or uievent:GetEnabled()) end end local function plaindump(val, opts, done) local keyignore = opts and opts.keyignore or {} local final = done == nil opts, done = opts or {}, done or {} local t = type(val) if t == "table" then done[#done+1] = '{' done[#done+1] = '' for key, value in pairs (val) do if not keyignore[key] then done[#done+1] = '[' plaindump(key, opts, done) done[#done+1] = ']=' plaindump(value, opts, done) done[#done+1] = "," end end done[#done] = '}' elseif t == "string" then done[#done+1] = ("%q"):format(val):gsub("\010","n"):gsub("\026","\\026") elseif t == "number" then done[#done+1] = ("%.17g"):format(val) else done[#done+1] = tostring(val) end return final and table.concat(done, '') end DumpPlain = plaindump