Order files

This commit is contained in:
Andros Fenollosa
2016-11-12 12:27:08 +01:00
parent 73cec1f153
commit 892d89c7f1
1814 changed files with 85 additions and 80 deletions

View File

@ -0,0 +1,332 @@
-- About
-- ----------------------------------------------------
-- This file contains lua table definitons used by
-- automatic loaded files, not part of the
-- editor source.
--
-- /<app>/config.lua
-- /cfg/user.lua
-- /interpreters/*.lua
-- /specs/*.lua
-- /tools/*.lua
-- /api/<apitype>/*.lua
-- style definition
-- ----------------------------------------------------
-- all entries are optional
stattr = {
fg = {r,g,b}, -- foreground color 0-255
bg = {r,g,b}, -- background color
i = false, -- italic
b = false, -- bold
u = false, -- underline
fill = true, -- fill to lineend
-- fn = "Lucida Console", -- font Face Name
-- fx = 11, -- font size
-- hs = true or {r,g,b}, -- turn hotspot on
-- use the specified color as activeForeground
-- use "hs = true", to turn it on without changing the color
-- HotspotActiveUnderline and HotspotSingleLine are on automatically
-- v = true, -- visibility for symbols of the current style
}
style = {
-- lexer specific (inherit fg/bg from text)
lexerdef = stattr,
comment = stattr,
stringtxt = stattr,
stringeol = stattr,
preprocessor = stattr,
operator = stattr,
number = stattr,
keywords0 = stattr,
keywords1 = stattr,
keywords2 = stattr,
keywords3 = stattr,
keywords4 = stattr,
keywords5 = stattr,
keywords6 = stattr,
keywords7 = stattr,
-- common (inherit fg/bg from text)
text = stattr,
linenumber = stattr,
bracematch = stattr,
bracemiss = stattr,
ctrlchar = stattr,
indent = stattr,
calltip = stattr,
-- common special (need custom fg & bg )
sel = nil,
caret = nil,
caretlinebg = nil,
fold = nil,
whitespace = nil,
}
-- config definition
-- ----------------------------------------------------
-- tables must exist
-- content is optional
-- config is loaded into existing config table
config = {
path = { -- path for tools/interpreters
lua = "C:/lua/lua.exe", -- path to lua exe
},
editor = {
fontname = "Courier New", -- default font
fontsize = 10, -- default size
caretline = true, -- show active line
iofilter = nil, -- input/output filtering of strings
showfncall = true, -- use indicator to show function calls if spec allows
tabwidth = 4,
usetabs = true, -- if false then spaces are used
usewrap = true, -- if true then the text is wrapped in the editor
whitespace = false,
autotabs = true, -- if true test for tabs after file load,
-- sets "usetabs" to true for this file
calltipdelay = nil, -- delay to show calltip (in ms)
autoactivate = false, -- auto-activate/open files during debugging
smartindent = false, -- use smart indentation if spec allows
fold = true, -- enable code folding
foldcompact = true, -- use compact fold that includes empty lines
checkeol = true, -- check for eol encoding on loaded files and use it
-- also report mixed eol encodings
defaulteol = nil, -- default line-endings for new files; valid values are
-- wxstc.wxSTC_EOL_CRLF, wxstc.wxSTC_EOL_LF and nil (OS default)
nomousezoom = nil, -- disable zooming using mouse wheel
autoreload = nil, -- trigger auto-reload when file is updated
saveallonrun = nil, -- save all modified files before Run/Debug
indentguide = true, -- show indentation guides
backspaceunindent = true, -- unindent when backspace is used
},
default = {
name = 'untitled',
fullname = 'untitled.lua',
interpreter = 'luadeb',
},
debugger = {
verbose = false,
hostname = nil, -- hostname to use when the detected one is incorrect
port = nil, -- port number to use
runonstart = nil, -- if debugger should run immediately after starting
-- default values are different for different interpreters
redirect = nil, -- "d", "c", or "r" values for default, copy, or redirect
},
outputshell = { -- output and shell settings
fontname = "Courier New", -- default font
fontsize = 10, -- defult size
},
filetree = { -- filetree settings
fontname = nil, -- no default font as it is system dependent
fontsize = nil, -- no default size as it is system dependent
},
format = { -- various formatting strings
menurecentprojects = nil,
},
keymap = {}, -- mapping of menu IDs to hot keys
messages = {}, -- list of messages in a particular language
language = "en", -- current UI language
styles = {}, -- styles table as above for editor
stylesoutshell = {}, -- styles for output/shell
interpreter = "luadeb", -- the default "project" lua interpreter
autocomplete = true, -- whether autocomplete is on by default
autoanalyzer = true, -- whether auto syntax analyzer is on by default
acandtip = {
shorttip = false, -- tooltips are compact during typing
nodynwords = false, -- no dynamic words (user entered words)
ignorecase = false, -- ignores case when performing comparison with autocomplete list
symbols = true, -- include local/global symbols
startat = 2, -- start suggesting dynamic words after 2 characters
strategy = 2,
-- 0: is string comparison
-- 1: substring leading characters (camel case or _ separated)
-- 2: leading + any correctly ordered fragments (default)
width = 60, -- width of the tooltip text (in characters)
maxlength = 450, -- max length of the tooltip on the screen
},
arg = {}, -- command line arguments
savebak = false, -- if bak files are created on save
filehistorylength = 20, -- historylength for files
projecthistorylength = 15, -- historylength for project directories
singleinstance = true, -- if true creates a UDP server to run IDE once and to load files
singleinstanceport = 0xe493, -- UDP port for single instance communication
activateoutput = false, -- activate output/console on Run/Debug/Compile
unhidewindow = false, -- to unhide a gui window
projectautoopen = false, -- allow auto open/close files on a project switch
autorecoverinactivity = nil, -- period of inactivity (s) for autorecover
hidpi = false, -- HiDPI/Retina display support
}
-- application engine
-- ----------------------------------------------------
app = {
preinit = function() end, -- post spec/tool loading, but prior subsystems/ui generation
postinit = function() end, -- post init, prior starting mainloop
loadfilters = {
tools = function(file) return true end,
specs = function(file) return true end,
interpreters = function(file) return true end,
},
stringtable = { -- optional entries uses defaults otherwise
editor = nil, statuswelcome = nil,
-- ...
}
}
-- api definition
-- ----------------------------------------------------
-- hierarchy encoded into children
api = {
-- global space words, e.g "table"
["blah"] = {
-- "function", "class", "keyword", "value", "lib", "method"
-- method is for class:func functions
type = "function",
description = "this does something",
-- value/function/method:
-- for autocomplete type guessing, insert the string
-- that the variable name is replace with
-- e.g. "test = somefunc()" somefunc has valuetype of "math"
-- then typing "test." will be treated as "math." in
-- autcomplete logic
valuetype = "api.ClassName",
-- function:
args = "(blah,blubb)",
returns = "(foo)",
-- autogenerated post load:
-- concated hierarchy name (e.g. "lib.class")
classname = "blah",
-- children in the class hierarchy
childs = {
--.. recursive
}
},
["blubb"] = {
--...
},
}
-- spec definition
-- ----------------------------------------------------
-- all entries are optional
spec = {
exts = {"ext","ext2",},
-- compatible extensions
lexer = wxstc.wxSTC_LEX_LUA,
-- scintilla lexer
lexerstyleconvert = {
-- table mapping each styles to
-- appropriate lexer id
stringeol = {wxstc.wxSTC_LUA_STRINGEOL,},
-- ...
},
linecomment = "//",
-- string for linecomments
sep = "%.:",
-- class.function separator match string,
-- e.g in lua both . and : are allowed
-- default is "\1" which should yield no matches
-- and therefore disable class.func type autocompletion
isfncall = function(str) return from,to end,
-- function that detects positions for a substring that
-- stands for a functioncall, ie " call(..)" -> 2,5
apitype = "api",
-- which sub directory of "api" is relevant
-- api files handle autocomplete and tooltips
-- api won't affect syntax coloring
keywords = {
-- up to 8 strings containing space separated keywords
-- used by the lexer for coloring (NOT for autocomplete).
-- however each lexer supports varying amount
-- of keyword types
"foo bar word",
"more words",
}
}
-- tool definition
-- ----------------------------------------------------
-- main entries are optional
tool = {
fninit = function(frame,menubar) end,
-- guarantees that ide is initialized
-- can be used for init
-- and adding custom menu
exec = {
-- quick exec action, listed under "Tools" menu
name = "",
description = "",
fn = function(wxfilename,projectdir) end,
}
}
-- debuginterface definition
-- ----------------------------------------------------
debuginterface = {
update = function(self) end, -- run in idle when active
close = function(self) end, -- run when closed
-- following are "debugging" actions and must return
-- error, running, [filePath, fileLine]
run = function(self) end,
step = function(self) end,
over = function(self) end,
out = function(self) end,
terminate = function(self) end,
breaknow = function(self) end,
breakpoint = function(self,file,line,state) end, -- set breakpoint state
-- returns result table if successful
evaluate = function(self, expressions, fnSetValues) end, -- for watches tables
stack = function(self) end, -- get stack information
}
-- interpreter definition-- ----------------------------------------------------
interpreter = {
name = "",
description = "",
api = {"apifile_without_extension"}, -- (opt) to limit loaded lua apis
frun = function(self,wfilename,withdebugger) end,
fprojdir = function(self,wfilename) return "projpath_from_filename" end, -- (opt)
fattachdebug = function(self) end, -- (opt)
hasdebugger = false, -- if debugging is available
scratchextloop = nil, -- (opt) indicates scratchpad support
-- nil, no support for scratchpad;
-- false, scratchpad supported;
-- true, scratchpad supported and requires handling for external loop.
skipcompile = nil, -- don't compile before running if true
}

View File

@ -0,0 +1,652 @@
-- Copyright 2011-15 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
DisplayOutputLn(TR("Error while loading API file: %s"):format(err))
return
end
local suc
suc, res = pcall(function() return fn(env.ac.childs) end)
if (not suc) then
DisplayOutputLn(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
local function scanAPIs()
for _, file in ipairs(FileSysGetRecursive("api", true, "*.lua")) do
if not IsDirectory(file) then
local ftype, fname = file:match("api[/\\]([^/\\]+)[/\\](.*)%.")
if not ftype or not fname then
DisplayOutputLn(TR("The API file must be located in a subdirectory of the API directory."))
return
end
ide.apis[ftype] = ide.apis[ftype] or {}
ide.apis[ftype][fname] = file
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 and tab.childs[key]) then
return getclass(tab.childs[key],rest)
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
DisplayOutputLn("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
change = not classname:find("^"..w) and not classname:find("^"..c..w)
c = classname..s
else
c = c..w..s
end
end
-- check for loops in type assignment
if refs[tx] then break end
refs[tx] = c
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*$")
local class = (tab and tab.classname
or caller and content:match("([%w_]+)"..anysep..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
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)
newAPI(apis[only])
loadallAPIs(only,subapis)
generateAPIInfo(only)
end
function ReloadLuaAPI()
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("lua",apinames)
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
scanAPIs()
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)
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)
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
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()
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(key, 1, true) == 1
-- 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
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 i,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) then
local strategy = ide.config.acandtip.strategy
if (strategy == 2 and #apilist < 128) then
-- when matching "ret": "ret." < "re.t" < "r.et"
local patany = rest:gsub(".", function(c) return "["..c:lower()..c:upper().."](.-)" end)
local patcase = rest:gsub(".", function(c) return c.."(.-)" 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()<b:lower() end
return ma<mb
end)
else
table.sort(apilist,function(a,b)
local ma,mb = a:sub(1,#rest)==rest, b:sub(1,#rest)==rest
if (ma and mb) or (not ma and not mb) then return a<b end
return ma
end)
end
else
table.sort(apilist)
end
local prev = apilist[#apilist]
for i = #apilist-1,1,-1 do
if prev == apilist[i] then
table.remove(apilist, i+1)
else prev = apilist[i] end
end
li = table.concat(apilist," ")
end
return li and #li > 1024 and li:sub(1,1024).."..." or li
end

View File

@ -0,0 +1,359 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
---------------------------------------------------------
local unpack = table.unpack or unpack
local maxlines = 8
local row_height = 46
function CommandBarShow(params)
local onDone, onUpdate, onItem, onSelection, defaultText, selectedText =
params.onDone, params.onUpdate, params.onItem, params.onSelection,
params.defaultText, params.selectedText
local row_width = ide.config.commandbar.width or 0
if row_width < 1 then
row_width = math.max(450, math.floor(row_width * ide:GetMainFrame():GetClientSize():GetWidth()))
end
local lines = {}
local linesnow = #lines
local linenow = 0
local nb = ide:GetEditorNotebook()
local pos = nb:GetScreenPosition()
if pos then
local miny
for p = 0, nb:GetPageCount()-1 do
local y = nb:GetPage(p):GetScreenPosition():GetY()
-- just in case, compare with the position of the notebook itself;
-- this is needed because the tabs that haven't been refreshed yet
-- may report 0 as their screen position on Linux, which is incorrect.
if y > pos:GetY() and (not miny or y < miny) then miny = y end
end
pos:SetX(pos:GetX()+nb:GetClientSize():GetWidth()-row_width-16)
pos:SetY((miny or pos:GetY())+2)
else
pos = wx.wxDefaultPosition
end
local frame = wx.wxFrame(ide:GetMainFrame(), wx.wxID_ANY, "Command Bar",
pos, wx.wxDefaultSize,
wx.wxFRAME_NO_TASKBAR + wx.wxFRAME_FLOAT_ON_PARENT + wx.wxNO_BORDER)
local panel = wx.wxPanel(frame or ide:GetMainFrame(), wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxFULL_REPAINT_ON_RESIZE)
local search = wx.wxTextCtrl(panel, wx.wxID_ANY, "\1",
wx.wxDefaultPosition,
-- make the text control a bit smaller on OSX
wx.wxSize(row_width, ide.osname == 'Macintosh' and 20 or 24),
wx.wxTE_PROCESS_ENTER + wx.wxTE_PROCESS_TAB + wx.wxNO_BORDER)
local results = wx.wxScrolledWindow(panel, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxSize(0, 0))
local style, styledef = ide.config.styles, StylesGetDefault()
local textcolor = wx.wxColour(unpack(style.text.fg or styledef.text.fg))
local backcolor = wx.wxColour(unpack(style.text.bg or styledef.text.bg))
local selcolor = wx.wxColour(unpack(style.caretlinebg.bg or styledef.caretlinebg.bg))
local pancolor = ide:GetUIManager():GetArtProvider():GetColor(wxaui.wxAUI_DOCKART_SASH_COLOUR)
local borcolor = ide:GetUIManager():GetArtProvider():GetColor(wxaui.wxAUI_DOCKART_BORDER_COLOUR)
search:SetBackgroundColour(backcolor)
search:SetForegroundColour(textcolor)
local nbrush = wx.wxBrush(backcolor, wx.wxSOLID)
local sbrush = wx.wxBrush(selcolor, wx.wxSOLID)
local bbrush = wx.wxBrush(pancolor, wx.wxSOLID)
local lpen = wx.wxPen(borcolor, 1, wx.wxDOT)
local bpen = wx.wxPen(borcolor, 1, wx.wxSOLID)
local sash = ide:GetUIManager():GetArtProvider():GetMetric(wxaui.wxAUI_DOCKART_SASH_SIZE)
local border = sash + 2
local hoffset = 4
local voffset = 4
local topSizer = wx.wxFlexGridSizer(2, 1, -border*2, 0)
topSizer:SetFlexibleDirection(wx.wxVERTICAL)
topSizer:AddGrowableRow(1, 1)
topSizer:Add(search, wx.wxSizerFlags(0):Expand():Border(wx.wxALL, border))
topSizer:Add(results, wx.wxSizerFlags(1):Expand():Border(wx.wxALL, border))
panel:SetSizer(topSizer)
topSizer:Fit(frame) -- fit the frame/panel around the controls
local minheight = frame:GetClientSize():GetHeight()
local tfont = ide:GetProjectTree():GetFont()
local ffont = (ide:GetEditor() or ide:CreateBareEditor()):GetFont()
ffont:SetPointSize(ffont:GetPointSize()+2)
local sfont = wx.wxFont(tfont)
tfont:SetPointSize(tfont:GetPointSize()+2)
search:SetFont(tfont)
-- make a one-time callback;
-- needed because KILL_FOCUS handler can be called after closing window
local function onExit(index)
onExit = function() end
onDone(index and lines[index], index, search:GetValue())
frame:Close()
end
local function onPaint(event)
-- adjust the scrollbar before working with the canvas
local _, starty = results:GetViewStart()
if #lines ~= linesnow then
-- adjust the starting line when the current line is the last one
if linenow > starty+maxlines then starty = starty + 1 end
results:SetScrollbars(1, row_height, 1, #lines, 0, starty*row_height, false)
linesnow = #lines
end
local dc = wx.wxMemoryDC(results)
results:PrepareDC(dc)
local size = results:GetVirtualSize()
local w,h = size:GetWidth(),size:GetHeight()
local bitmap = wx.wxBitmap(w,h)
dc:SelectObject(bitmap)
-- clear the background
dc:SetBackground(nbrush)
dc:Clear()
dc:SetTextForeground(textcolor)
dc:SetBrush(sbrush)
for r = 1, #lines do
if r == linenow then
dc:SetPen(wx.wxTRANSPARENT_PEN)
dc:DrawRectangle(0, row_height*(r-1), row_width, row_height+1)
end
dc:SetPen(lpen)
dc:DrawLine(hoffset, row_height*(r-1), row_width-hoffset*2, row_height*(r-1))
local fline, sline = onItem(lines[r])
if fline then
dc:SetFont(ffont)
dc:DrawText(fline, hoffset, row_height*(r-1)+voffset)
end
if sline then
dc:SetFont(sfont)
dc:DrawText(sline, hoffset, row_height*(r-1)+row_height/2+voffset)
end
end
dc:SetPen(wx.wxNullPen)
dc:SetBrush(wx.wxNullBrush)
dc:SelectObject(wx.wxNullBitmap)
dc:delete()
dc = wx.wxPaintDC(results)
dc:DrawBitmap(bitmap, 0, 0, true)
dc:delete()
end
local function onPanelPaint(event)
local dc = wx.wxBufferedPaintDC(panel)
dc:SetBrush(bbrush)
dc:SetPen(bpen)
local psize = panel:GetClientSize()
dc:DrawRectangle(0, 0, psize:GetWidth(), psize:GetHeight())
dc:DrawRectangle(sash+1, sash+1, psize:GetWidth()-2*(sash+1), psize:GetHeight()-2*(sash+1))
dc:SetPen(wx.wxNullPen)
dc:SetBrush(wx.wxNullBrush)
dc:delete()
end
local linewas -- line that was reported when updated
local function onTextUpdated(event)
local text = search:GetValue()
lines = onUpdate(text)
linenow = #text > 0 and #lines > 0 and 1 or 0
linewas = nil
local size = frame:GetClientSize()
local height = minheight + row_height*math.min(maxlines,#lines)
if height ~= size:GetHeight() then
results:SetScrollbars(1, 1, 1, 1, 0, 0, false)
size:SetHeight(height)
frame:SetClientSize(size)
end
results:Refresh()
end
local function onKeyDown(event)
local keycode = event:GetKeyCode()
if keycode == wx.WXK_RETURN then
onExit(linenow)
return
elseif event:GetModifiers() ~= wx.wxMOD_NONE then
event:Skip()
return
elseif keycode == wx.WXK_UP then
if linesnow > 0 then
linenow = linenow - 1
if linenow <= 0 then linenow = linesnow end
end
elseif keycode == wx.WXK_DOWN then
if linesnow > 0 then
linenow = linenow % linesnow + 1
end
elseif keycode == wx.WXK_PAGEDOWN then
if linesnow > 0 then
linenow = linenow + maxlines
if linenow > linesnow then linenow = linesnow end
end
elseif keycode == wx.WXK_PAGEUP then
if linesnow > 0 then
linenow = linenow - maxlines
if linenow <= 0 then linenow = 1 end
end
elseif keycode == wx.WXK_ESCAPE then
onExit(false)
return
else
event:Skip()
return
end
local _, starty = results:GetViewStart()
if linenow < starty+1 then results:Scroll(-1, linenow-1)
elseif linenow > starty+maxlines then results:Scroll(-1, linenow-maxlines) end
results:Refresh()
end
local function onMouseLeftDown(event)
local pos = event:GetPosition()
local _, y = results:CalcUnscrolledPosition(pos.x, pos.y)
onExit(math.floor(y / row_height)+1)
end
local function onIdle(event)
if linewas == linenow then return end
linewas = linenow
if linenow == 0 then return end
-- save the selection/insertion point as it's reset on Linux (wxwidgets 2.9.5)
local ip = search:GetInsertionPoint()
local f, t = search:GetSelection()
-- this may set focus to a different object/tab,
-- so disable the focus event and then set the focus back
search:SetEvtHandlerEnabled(false)
onSelection(lines[linenow], search:GetValue())
search:SetFocus()
search:SetEvtHandlerEnabled(true)
if ide.osname == 'Unix' then
search:SetInsertionPoint(ip)
search:SetSelection(f, t)
end
end
frame:Connect(wx.wxEVT_CLOSE_WINDOW, function() frame:Destroy() end)
panel:Connect(wx.wxEVT_PAINT, onPanelPaint)
panel:Connect(wx.wxEVT_ERASE_BACKGROUND, function() end)
panel:Connect(wx.wxEVT_IDLE, onIdle)
results:Connect(wx.wxEVT_PAINT, onPaint)
results:Connect(wx.wxEVT_LEFT_DOWN, onMouseLeftDown)
results:Connect(wx.wxEVT_ERASE_BACKGROUND, function() end)
search:SetFocus()
search:Connect(wx.wxEVT_KEY_DOWN, onKeyDown)
search:Connect(wx.wxEVT_COMMAND_TEXT_UPDATED, onTextUpdated)
search:Connect(wx.wxEVT_COMMAND_TEXT_ENTER, function() onExit(linenow) end)
search:Connect(wx.wxEVT_KILL_FOCUS, function() onExit() end)
frame:Show(true)
frame:Update()
frame:Refresh()
search:SetValue((defaultText or "")..(selectedText or ""))
search:SetSelection(#(defaultText or ""), -1)
end
local sep = "[/\\%-_ ]+"
local weights = {onegram = 0.1, digram = 0.4, trigram = 0.5}
local cache = {}
local missing = 3 -- penalty for missing symbols (1 missing == N matching)
local casemismatch = 0.9 -- score for case mismatch (%% of full match)
local function score(p, v)
local function ngrams(str, num, low, needcache)
local key = str..'\1'..num
if cache[key] then return unpack(cache[key]) end
local t, l, p = {}, {}, 0
for i = 1, #str-num+1 do
local pair = str:sub(i, i+num-1)
p = p + (t[pair] and 0 or 1)
if low and pair:find('%u') then l[pair:lower()] = casemismatch end
t[pair] = 1
end
if needcache then cache[key] = {t, p, l} end
return t, p, l
end
local function overlap(pattern, value, num)
local ph, ps = ngrams(pattern, num, false, true)
local vh, vs, vl = ngrams(value, num, true)
if ps + vs == 0 then return 0 end
local is = 0 -- intersection of two sets of ngrams
for k in pairs(ph) do is = is + (vh[k] or vl[k:lower()] or 0) end
return is / (ps + vs) - (num == 1 and missing * (ps - is) / (ps + vs) or 0)
end
local key = p..'\2'..v
if not cache[key] then
local score = weights.onegram * overlap(p, v, 1)
if score > 0 then -- don't bother with those that can't even score 1grams
p = ' '..(p:gsub(sep, ' '))
v = ' '..(v:gsub(sep, ' '))
score = score + weights.digram * overlap(p, v, 2)
score = score + weights.trigram * overlap(' '..p, ' '..v, 3)
end
cache[key] = 2 * 100 * score
end
return cache[key]
end
function CommandBarScoreItems(t, pattern, limit)
local r, plen = {}, #pattern
local maxp = 0
local num = 0
local prefilter = ide.config.commandbar and ide.config.commandbar.prefilter
-- anchor for 1-2 symbol patterns to speed up search
local needanchor = prefilter and prefilter * 4 <= #t and plen <= 2
local filter = prefilter and prefilter <= #t
-- expand `abc` into `a.*b.*c`, but limit the prefix to avoid penalty for `s.*s.*s.*....`
and pattern:gsub("[^%w_]+",""):sub(1,4):lower():gsub(".", "%1.*"):gsub("%.%*$","")
or nil
for _, v in ipairs(t) do
if #v >= plen then
local match = filter and v:lower():find(filter)
-- check if the current name needs to be prefiltered or anchored (for better performance);
-- if it needs to be anchored, then anchor it at the beginning of the string or the word
if not filter or (match and (not needanchor or match == 1 or v:find("^[%p%s]", match-1))) then
local p = score(pattern, v)
maxp = math.max(p, maxp)
if p > 1 and p > maxp / 4 then
num = num + 1
r[num] = {v, p}
end
end
end
end
table.sort(r, function(a, b) return a[2] > b[2] end)
-- limit the list to be displayed
-- `r[limit+1] = nil` is not desired as the resulting table may be sorted incorrectly
if tonumber(limit) and limit < #r then
local tmp = r
r = {}
for i = 1, limit do r[i] = tmp[i] end
end
return r
end
ide:AddPackage('core.commandbar', {
-- reset ngram cache when switching projects to conserve memory
onProjectLoad = function() cache = {} end
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,903 @@
-- Copyright 2011-16 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
ide.filetree = {
projdir = "",
projdirlist = {},
projdirpartmap = {},
projtreeCtrl = nil,
imglist = ide:CreateImageList("PROJECT",
"FOLDER", "FILE-KNOWN", "FILE-NORMAL", "FILE-NORMAL-START",
"FOLDER-MAPPED"),
settings = {extensionignore = {}, startfile = {}, mapped = {}},
}
local filetree = ide.filetree
local iscaseinsensitive = wx.wxFileName("A"):SameAs(wx.wxFileName("a"))
local pathsep = GetPathSeparator()
local q = EscapeMagic
local image = {
DIRECTORY = 0, FILEKNOWN = 1, FILEOTHER = 2, FILEOTHERSTART = 3,
DIRECTORYMAPPED = 4,
}
MergeSettings(filetree.settings, ide:AddPackage('core.filetree', {}):GetSettings())
-- generic tree
-- ------------
local function getIcon(name, isdir)
local startfile = GetFullPathIfExists(FileTreeGetDir(),
filetree.settings.startfile[FileTreeGetDir()])
local known = GetSpec(GetFileExt(name))
local icon = isdir and image.DIRECTORY or known and image.FILEKNOWN or image.FILEOTHER
if startfile and startfile == name then icon = image.FILEOTHERSTART end
return icon
end
local function treeAddDir(tree,parent_id,rootdir)
local items = {}
local item, cookie = tree:GetFirstChild(parent_id)
while item:IsOk() do
items[tree:GetItemText(item) .. tree:GetItemImage(item)] = item
item, cookie = tree:GetNextChild(parent_id, cookie)
end
local cache = {}
local curr
local files = FileSysGetRecursive(rootdir)
local dirmapped = {}
if tree:IsRoot(parent_id) then
local mapped = filetree.settings.mapped[FileTreeGetDir()] or {}
table.sort(mapped)
-- insert into files at the sorted order
for i, v in ipairs(mapped) do
table.insert(files, i, v)
dirmapped[v] = true
end
end
for _, file in ipairs(files) do
local name, dir = file:match("([^"..pathsep.."]+)("..pathsep.."?)$")
local isdir = #dir>0
if isdir or not filetree.settings.extensionignore[GetFileExt(name)] then
local icon = getIcon(file, isdir)
-- keep full name for the mapped directories
if dirmapped[file] then name, icon = file, image.DIRECTORYMAPPED end
local item = items[name .. icon]
if item then -- existing item
-- keep deleting items until we find item
while true do
local next = (curr
and tree:GetNextSibling(curr)
or tree:GetFirstChild(parent_id))
if not next:IsOk() or name == tree:GetItemText(next) then
curr = next
break
end
tree:Delete(next)
end
else -- new item
curr = (curr
and tree:InsertItem(parent_id, curr, name, icon)
or tree:PrependItem(parent_id, name, icon))
if isdir then tree:SetItemHasChildren(curr, FileDirHasContent(file)) end
end
if curr:IsOk() then cache[iscaseinsensitive and name:lower() or name] = curr end
end
end
-- delete any leftovers (something that exists in the tree, but not on disk)
while true do
local next = (curr
and tree:GetNextSibling(curr)
or tree:GetFirstChild(parent_id))
if not next:IsOk() then break end
tree:Delete(next)
end
-- cache the mapping from names to tree items
if ide.wxver >= "2.9.5" then
local data = wx.wxLuaTreeItemData()
data:SetData(cache)
tree:SetItemData(parent_id, data)
end
tree:SetItemHasChildren(parent_id,
tree:GetChildrenCount(parent_id, false) > 0)
end
local function treeSetRoot(tree,rootdir)
tree:DeleteAllItems()
if (not wx.wxDirExists(rootdir)) then return end
local root_id = tree:AddRoot(rootdir, image.DIRECTORY)
tree:SetItemHasChildren(root_id, true) -- make sure that the item can expand
tree:Expand(root_id) -- this will also populate the tree
end
local function findItem(tree, match)
local node = tree:GetRootItem()
local label = tree:GetItemText(node)
local s, e
if iscaseinsensitive then
s, e = string.find(match:lower(), label:lower(), 1, true)
else
s, e = string.find(match, label, 1, true)
end
if not s or s ~= 1 then return end
for token in string.gmatch(string.sub(match,e+1), "[^%"..pathsep.."]+") do
local data = tree:GetItemData(node)
local cache = data and data:GetData()
if cache and cache[iscaseinsensitive and token:lower() or token] then
node = cache[iscaseinsensitive and token:lower() or token]
else
-- token is missing; may need to re-scan the folder; maybe new file
local dir = tree:GetItemFullName(node)
treeAddDir(tree,node,dir)
local item, cookie = tree:GetFirstChild(node)
while true do
if not item:IsOk() then return end -- not found
if tree:GetItemText(item) == token then
node = item
break
end
item, cookie = tree:GetNextChild(node, cookie)
end
end
end
-- this loop exits only when a match is found
return node
end
local function treeSetConnectorsAndIcons(tree)
tree:AssignImageList(filetree.imglist)
local function isIt(item, imgtype) return tree:GetItemImage(item) == imgtype end
function tree:IsDirectory(item_id) return isIt(item_id, image.DIRECTORY) end
function tree:IsDirMapped(item_id) return isIt(item_id, image.DIRECTORYMAPPED) end
function tree:IsFileKnown(item_id) return isIt(item_id, image.FILEKNOWN) end
function tree:IsFileOther(item_id) return isIt(item_id, image.FILEOTHER) end
function tree:IsFileStart(item_id) return isIt(item_id, image.FILEOTHERSTART) end
function tree:IsRoot(item_id) return not tree:GetItemParent(item_id):IsOk() end
function tree:FindItem(match)
return findItem(self, (wx.wxIsAbsolutePath(match) or match == '') and match
or MergeFullPath(ide:GetProject(), match))
end
function tree:GetItemFullName(item_id)
local tree = self
local str = tree:GetItemText(item_id)
local cur = str
while (#cur > 0) do
item_id = tree:GetItemParent(item_id)
if not item_id:IsOk() then break end
cur = tree:GetItemText(item_id)
if cur and #cur > 0 then str = MergeFullPath(cur, str) end
end
-- as root may already include path separator, normalize the path
local fullPath = wx.wxFileName(str)
fullPath:Normalize()
return fullPath:GetFullPath()
end
function tree:RefreshChildren(node)
node = node or tree:GetRootItem()
treeAddDir(tree,node,tree:GetItemFullName(node))
local item, cookie = tree:GetFirstChild(node)
while true do
if not item:IsOk() then return end
if tree:IsExpanded(item) then tree:RefreshChildren(item) end
item, cookie = tree:GetNextChild(node, cookie)
end
end
local function refreshAncestors(node)
-- when this method is called from END_EDIT, it causes infinite loop
-- on OSX (wxwidgets 2.9.5) as Delete in treeAddDir calls END_EDIT again.
-- disable handlers while the tree is populated and then enable back.
tree:SetEvtHandlerEnabled(false)
while node:IsOk() do
local dir = tree:GetItemFullName(node)
treeAddDir(tree,node,dir)
node = tree:GetItemParent(node)
end
tree:SetEvtHandlerEnabled(true)
end
function tree:ActivateItem(item_id)
local name = tree:GetItemFullName(item_id)
local event = wx.wxTreeEvent(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED, item_id:GetValue())
if PackageEventHandle("onFiletreeActivate", tree, event, item_id) == false then
return
end
-- refresh the folder
if (tree:IsDirectory(item_id) or tree:IsDirMapped(item_id)) then
if wx.wxDirExists(name) then treeAddDir(tree,item_id,name)
else refreshAncestors(tree:GetItemParent(item_id)) end -- stale content
else -- open file
if wx.wxFileExists(name) then LoadFile(name,nil,true)
else refreshAncestors(tree:GetItemParent(item_id)) end -- stale content
end
end
local function unMapDir(dir)
local project = FileTreeGetDir()
local mapped = filetree.settings.mapped[project] or {}
for k, m in ipairs(mapped) do
if m == dir then table.remove(mapped, k) end
end
filetree.settings.mapped[project] = mapped
refreshAncestors(tree:GetRootItem())
end
local function mapDir()
local project = FileTreeGetDir()
local dirPicker = wx.wxDirDialog(ide.frame, TR("Choose a directory to map"),
project ~= "" and project or wx.wxGetCwd(), wx.wxDIRP_DIR_MUST_EXIST)
if dirPicker:ShowModal(true) ~= wx.wxID_OK then return end
local dir = wx.wxFileName.DirName(FixDir(dirPicker:GetPath())):GetFullPath()
local mapped = filetree.settings.mapped[project] or {}
for _, m in ipairs(mapped) do
if m == dir then return end -- already on the list
end
table.insert(mapped, dir)
filetree.settings.mapped[project] = mapped
refreshAncestors(tree:GetRootItem())
end
local empty = ""
local function renameItem(itemsrc, target)
local cache = type(itemsrc) == 'table' and itemsrc or nil
local isdir = not cache and tree:IsDirectory(itemsrc) or cache and cache.isdir or false
local isnew = not cache and tree:GetItemText(itemsrc) == empty or cache and cache.isnew or false
local source = cache and cache.fullname or tree:GetItemFullName(itemsrc)
local fn = wx.wxFileName(target)
-- check if the target is the same as the source;
-- SameAs check is not used here as "Test" and "test" are the same
-- on case insensitive systems, but need to be allowed in renaming.
if source == target then return end
local docs = {}
if not isnew then -- find if source is already opened in the editor
docs = (isdir
and ide:FindDocumentsByPartialPath(source)
or {ide:FindDocument(source)})
for _, doc in ipairs(docs) do
if not isdir and PackageEventHandle("onEditorPreSave", doc.editor, source) == false then
return false
end
if SaveModifiedDialog(doc.editor, true) == wx.wxID_CANCEL then return end
end
end
-- check if existing file/dir is going to be overwritten
local overwrite = ((wx.wxFileExists(target) or wx.wxDirExists(target))
and not wx.wxFileName(source):SameAs(fn))
if overwrite and not ApproveFileOverwrite() then return false end
if not fn:Mkdir(tonumber(755,8), wx.wxPATH_MKDIR_FULL) then
ReportError(TR("Unable to create directory '%s'."):format(target))
return false
end
if isnew then -- new directory or file; create manually
if (isdir and not wx.wxFileName.DirName(target):Mkdir(tonumber(755,8), wx.wxPATH_MKDIR_FULL))
or (not isdir and not FileWrite(target, "")) then
ReportError(TR("Unable to create file '%s'."):format(target))
return false
end
else -- existing directory or file; rename/move it
local ok, err = FileRename(source, target)
if not ok then
ReportError(TR("Unable to rename file '%s'."):format(source)
.."\nError: "..err)
return false
end
end
refreshAncestors(cache and cache.parent or tree:GetItemParent(itemsrc))
-- load file(s) into the same editor (if any); will also refresh the tree
if #docs > 0 then
for _, doc in ipairs(docs) do
local fullpath = doc.filePath
doc.filePath = nil -- remove path to avoid "file no longer exists" message
-- when moving folders, /foo/bar/file.lua can be replaced with
-- /foo/baz/bar/file.lua, so change /foo/bar to /foo/baz/bar
local path = (not iscaseinsensitive and fullpath:gsub(q(source), target)
or fullpath:lower():gsub(q(source:lower()), target))
local editor = LoadFile(path)
-- check if the file was loaded into another editor;
-- this is possible if "foo" is renamed to "bar" and both are opened;
-- if this happens, then "bar" is refreshed and "foo" can be closed.
if doc.editor:GetId() ~= editor:GetId() then ClosePage(doc.index) end
if not isdir and editor then PackageEventHandle("onEditorSave", editor) end
end
else -- refresh the tree and select the new item
local itemdst = tree:FindItem(target)
if itemdst then
refreshAncestors(tree:GetItemParent(itemdst))
tree:SelectItem(itemdst)
tree:EnsureVisible(itemdst)
tree:SetScrollPos(wx.wxHORIZONTAL, 0, true)
end
end
-- refresh the target if it's open and has been overwritten
if overwrite and not isdir then
local doc = ide:FindDocument(target)
if doc then LoadFile(doc:GetFilePath(), doc:GetEditor()) end
end
return true
end
local function deleteItem(item_id)
-- if delete is for mapped directory, unmap it instead
if tree:IsDirMapped(item_id) then
unMapDir(tree:GetItemText(item_id))
return
end
local isdir = tree:IsDirectory(item_id)
local source = tree:GetItemFullName(item_id)
if isdir and FileDirHasContent(source..pathsep) then return false end
if wx.wxMessageBox(
TR("Do you want to delete '%s'?"):format(source),
GetIDEString("editormessage"),
wx.wxYES_NO + wx.wxCENTRE, ide.frame) ~= wx.wxYES then return false end
if isdir then
if not wx.wxRmdir(source) then
ReportError(TR("Unable to delete directory '%s': %s")
:format(source, wx.wxSysErrorMsg()))
end
else
local doc = ide:FindDocument(source)
if doc then ClosePage(doc.index) end
if not wx.wxRemoveFile(source) then
ReportError(TR("Unable to delete file '%s': %s")
:format(source, wx.wxSysErrorMsg()))
end
end
refreshAncestors(tree:GetItemParent(item_id))
return true
end
tree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_EXPANDING,
function (event)
local item_id = event:GetItem()
local dir = tree:GetItemFullName(item_id)
if wx.wxDirExists(dir) then treeAddDir(tree,item_id,dir) -- refresh folder
else refreshAncestors(tree:GetItemParent(item_id)) end -- stale content
return true
end)
tree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED,
function (event)
tree:ActivateItem(event:GetItem())
end)
local function saveSettings()
ide:AddPackage('core.filetree', {}):SetSettings(filetree.settings)
end
-- refresh the tree
local function refreshChildren()
tree:RefreshChildren()
-- now mark the current file (if it was previously disabled)
local editor = ide:GetEditor()
if editor then FileTreeMarkSelected(ide:GetDocument(editor):GetFilePath()) end
end
-- handle context menu
local function addItem(item_id, name, img)
local isdir = tree:IsDirectory(item_id)
local parent = isdir and item_id or tree:GetItemParent(item_id)
if isdir then tree:Expand(item_id) end -- expand to populate if needed
local item = tree:PrependItem(parent, name, img)
tree:SetItemHasChildren(parent, true)
-- temporarily disable expand as we don't need this node populated
tree:SetEvtHandlerEnabled(false)
tree:EnsureVisible(item)
tree:SetEvtHandlerEnabled(true)
return item
end
local function unsetStartFile()
local project = FileTreeGetDir()
local startfile = filetree.settings.startfile[project]
filetree.settings.startfile[project] = nil
if startfile then
local item_id = tree:FindItem(startfile)
if item_id and item_id:IsOk() then
tree:SetItemImage(item_id, getIcon(tree:GetItemFullName(item_id)))
end
end
end
local function setStartFile(item_id)
local project = FileTreeGetDir()
local startfile = tree:GetItemFullName(item_id):gsub(project, "")
filetree.settings.startfile[project] = startfile
tree:SetItemImage(item_id, getIcon(tree:GetItemFullName(item_id)))
end
tree:Connect(ID_NEWFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
tree:EditLabel(addItem(tree:GetSelection(), empty, image.FILEOTHER))
end)
tree:Connect(ID_NEWDIRECTORY, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
tree:EditLabel(addItem(tree:GetSelection(), empty, image.DIRECTORY))
end)
tree:Connect(ID_RENAMEFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
function() tree:EditLabel(tree:GetSelection()) end)
tree:Connect(ID_DELETEFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
function() deleteItem(tree:GetSelection()) end)
tree:Connect(ID_COPYFULLPATH, wx.wxEVT_COMMAND_MENU_SELECTED,
function() ide:CopyToClipboard(tree:GetItemFullName(tree:GetSelection())) end)
tree:Connect(ID_OPENEXTENSION, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
local fname = tree:GetItemFullName(tree:GetSelection())
local ext = '.'..wx.wxFileName(fname):GetExt()
local ft = wx.wxTheMimeTypesManager:GetFileTypeFromExtension(ext)
if ft then
local cmd = ft:GetOpenCommand(fname:gsub('"','\\"'))
local pid = wx.wxExecute(cmd, wx.wxEXEC_ASYNC)
if ide.osname == 'Windows' and pid and pid > 0 then
-- some programs on Windows (for example, PhotoViewer) accept
-- files with spaces in names ONLY if they are not in quotes.
-- wait for the process that failed to open file to finish
-- and retry without quotes.
wx.wxMilliSleep(250) -- 250ms seems enough; picked empirically.
if not wx.wxProcess.Exists(pid) then
local cmd = ft:GetOpenCommand(""):gsub('""%s*$', '')..fname
wx.wxExecute(cmd, wx.wxEXEC_ASYNC)
end
end
end
end)
tree:Connect(ID_REFRESH, wx.wxEVT_COMMAND_MENU_SELECTED,
function() refreshChildren() end)
tree:Connect(ID_SHOWLOCATION, wx.wxEVT_COMMAND_MENU_SELECTED,
function() ShowLocation(tree:GetItemFullName(tree:GetSelection())) end)
tree:Connect(ID_HIDEEXTENSION, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
local ext = GetFileExt(tree:GetItemText(tree:GetSelection()))
filetree.settings.extensionignore[ext] = true
saveSettings()
refreshChildren()
end)
tree:Connect(ID_SHOWEXTENSIONALL, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
filetree.settings.extensionignore = {}
saveSettings()
refreshChildren()
end)
tree:Connect(ID_SETSTARTFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
unsetStartFile()
setStartFile(tree:GetSelection())
saveSettings()
end)
tree:Connect(ID_UNSETSTARTFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
unsetStartFile()
saveSettings()
end)
tree:Connect(ID_MAPDIRECTORY, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
mapDir()
saveSettings()
end)
tree:Connect(ID_UNMAPDIRECTORY, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
unMapDir(tree:GetItemText(tree:GetSelection()))
saveSettings()
end)
tree:Connect(ID_PROJECTDIRFROMDIR, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
ProjectUpdateProjectDir(tree:GetItemFullName(tree:GetSelection()))
end)
tree:Connect(wx.wxEVT_COMMAND_TREE_ITEM_MENU,
function (event)
local item_id = event:GetItem()
tree:SelectItem(item_id)
local renamelabel = (tree:IsRoot(item_id)
and TR("&Edit Project Directory")
or TR("&Rename"))
local fname = tree:GetItemText(item_id)
local ext = GetFileExt(fname)
local startfile = filetree.settings.startfile[FileTreeGetDir()]
local menu = wx.wxMenu {
{ ID_NEWFILE, TR("New &File") },
{ ID_NEWDIRECTORY, TR("&New Directory") },
{ },
{ ID_RENAMEFILE, renamelabel..KSC(ID_RENAMEFILE) },
{ ID_DELETEFILE, TR("&Delete")..KSC(ID_DELETEFILE) },
{ ID_REFRESH, TR("Refresh") },
{ },
{ ID_HIDEEXTENSION, TR("Hide '.%s' Files"):format(ext) },
{ },
{ ID_SETSTARTFILE, TR("Set As Start File") },
{ ID_UNSETSTARTFILE, TR("Unset '%s' As Start File"):format(startfile or "<none>") },
{ },
{ ID_MAPDIRECTORY, TR("Map Directory...") },
{ ID_UNMAPDIRECTORY, TR("Unmap Directory") },
{ ID_OPENEXTENSION, TR("Open With Default Program") },
{ ID_COPYFULLPATH, TR("Copy Full Path") },
{ ID_SHOWLOCATION, TR("Show Location") },
}
local extlist = {
{},
{ ID_SHOWEXTENSIONALL, TR("Show All Files"), TR("Show all files") },
}
for ext in pairs(filetree.settings.extensionignore) do
local id = ID("filetree.showextension."..ext)
table.insert(extlist, 1, {id, '.'..ext})
menu:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, function()
filetree.settings.extensionignore[ext] = nil
saveSettings()
refreshChildren()
end)
end
local _, _, hideextpos = ide:FindMenuItem(ID_HIDEEXTENSION, menu)
assert(hideextpos, "Can't find HideExtension menu item")
menu:Insert(hideextpos+1, wx.wxMenuItem(menu, ID_SHOWEXTENSION,
TR("Show Hidden Files"), TR("Show files previously hidden"),
wx.wxITEM_NORMAL, wx.wxMenu(extlist)))
local projectdirectorymenu = wx.wxMenu {
{ },
{ID_PROJECTDIRCHOOSE, TR("Choose...")..KSC(ID_PROJECTDIRCHOOSE), TR("Choose a project directory")},
{ID_PROJECTDIRFROMDIR, TR("Set To Selected Directory")..KSC(ID_PROJECTDIRFROMDIR), TR("Set project directory to the selected one")},
}
local projectdirectory = wx.wxMenuItem(menu, ID_PROJECTDIR,
TR("Project Directory"), TR("Set the project directory to be used"),
wx.wxITEM_NORMAL, projectdirectorymenu)
local _, _, unmapdirpos = ide:FindMenuItem(ID_UNMAPDIRECTORY, menu)
assert(unmapdirpos, "Can't find UnMapDirectory menu item")
menu:Insert(unmapdirpos+1, projectdirectory)
FileTreeProjectListUpdate(projectdirectorymenu, 0)
-- disable Delete on non-empty directories
local isdir = tree:IsDirectory(item_id)
local ismapped = tree:IsDirMapped(item_id)
menu:Destroy(ismapped and ID_MAPDIRECTORY or ID_UNMAPDIRECTORY)
if not startfile then menu:Destroy(ID_UNSETSTARTFILE) end
if ismapped then menu:Enable(ID_RENAMEFILE, false) end
if isdir then
local source = tree:GetItemFullName(item_id)
menu:Enable(ID_DELETEFILE, not FileDirHasContent(source..pathsep))
menu:Enable(ID_OPENEXTENSION, false)
menu:Enable(ID_HIDEEXTENSION, false)
else
local ft = wx.wxTheMimeTypesManager:GetFileTypeFromExtension('.'..ext)
menu:Enable(ID_OPENEXTENSION, ft and #ft:GetOpenCommand("") > 0)
menu:Enable(ID_HIDEEXTENSION, not filetree.settings.extensionignore[ext])
menu:Enable(ID_PROJECTDIRFROMDIR, false)
end
menu:Enable(ID_SETSTARTFILE, tree:IsFileOther(item_id) or tree:IsFileKnown(item_id))
menu:Enable(ID_SHOWEXTENSION, next(filetree.settings.extensionignore) ~= nil)
PackageEventHandle("onMenuFiletree", menu, tree, event)
-- stopping/restarting garbage collection is generally not needed,
-- but on Linux not stopping is causing crashes (wxwidgets 2.9.5 and 3.1.0)
-- when symbol indexing is done while popup menu is open (with gc methods in the trace).
-- this only happens when EVT_IDLE is called when popup menu is open.
collectgarbage("stop")
-- stopping UI updates is generally not needed as well,
-- but it's causing a crash on OSX (wxwidgets 2.9.5 and 3.1.0)
-- when symbol indexing is done while popup menu is open, so it's disabled
local interval = wx.wxUpdateUIEvent.GetUpdateInterval()
wx.wxUpdateUIEvent.SetUpdateInterval(-1) -- don't update
tree:PopupMenu(menu)
wx.wxUpdateUIEvent.SetUpdateInterval(interval)
collectgarbage("restart")
end)
tree:Connect(wx.wxEVT_RIGHT_DOWN,
function (event)
local item_id = tree:HitTest(event:GetPosition())
if PackageEventHandle("onFiletreeRDown", tree, event, item_id) == false then
return
end
event:Skip()
end)
-- toggle a folder on a single click
tree:Connect(wx.wxEVT_LEFT_DOWN,
function (event)
-- only toggle if this is a folder and the click is on the item line
-- (exclude the label as it's used for renaming and dragging)
local mask = (wx.wxTREE_HITTEST_ONITEMINDENT
+ wx.wxTREE_HITTEST_ONITEMICON + wx.wxTREE_HITTEST_ONITEMRIGHT)
local item_id, flags = tree:HitTest(event:GetPosition())
if PackageEventHandle("onFiletreeLDown", tree, event, item_id) == false then
return
end
if item_id and bit.band(flags, mask) > 0 then
if tree:IsDirectory(item_id) then
tree:Toggle(item_id)
tree:SelectItem(item_id)
else
local name = tree:GetItemFullName(item_id)
if wx.wxFileExists(name) then LoadFile(name,nil,true) end
end
else
event:Skip()
end
return true
end)
local parent
tree:Connect(wx.wxEVT_COMMAND_TREE_BEGIN_LABEL_EDIT,
function (event)
local itemsrc = event:GetItem()
parent = tree:GetItemParent(itemsrc)
if not itemsrc:IsOk() or tree:IsDirMapped(itemsrc) then event:Veto() end
end)
tree:Connect(wx.wxEVT_COMMAND_TREE_END_LABEL_EDIT,
function (event)
-- veto the event to keep the original label intact as the tree
-- is going to be refreshed with the correct names.
event:Veto()
local itemsrc = event:GetItem()
if not itemsrc:IsOk() then return end
local label = event:GetLabel():gsub("^%s+$","") -- clean all spaces
-- edited the root element; set the new project directory if needed
local cancelled = event:IsEditCancelled()
if tree:IsRoot(itemsrc) then
if not cancelled and wx.wxDirExists(label) then
ProjectUpdateProjectDir(label)
end
return
end
if not parent or not parent:IsOk() then return end
local target = MergeFullPath(tree:GetItemFullName(parent), label)
if cancelled or label == empty then refreshAncestors(parent)
elseif target then
-- normally, none of this caching would be needed as `renameItem`
-- would be called to check if the item can be renamed;
-- however, as it may open a dialog box, on Linux it's causing a crash
-- (caused by the same END_LABEL_EDIT even triggered one more time),
-- so to protect from that, `renameItem` is called from IDLE event.
-- Unfortunately, by that time, the filetree item (`itemsrc`) may
-- already have incorrect state (as it's removed from the tree),
-- so its properties need to be cached to be used from IDLE event.
local cache = {
isdir = tree:IsDirectory(itemsrc),
isnew = tree:GetItemText(itemsrc) == empty,
fullname = tree:GetItemFullName(itemsrc),
parent = parent,
}
ide:DoWhenIdle(function()
if not renameItem(cache, target) then refreshAncestors(parent) end
end)
end
end)
local itemsrc
tree:Connect(wx.wxEVT_COMMAND_TREE_BEGIN_DRAG,
function (event)
if ide.config.filetree.mousemove and tree:GetItemParent(event:GetItem()):IsOk() then
itemsrc = event:GetItem()
event:Allow()
end
end)
tree:Connect(wx.wxEVT_COMMAND_TREE_END_DRAG,
function (event)
local itemdst = event:GetItem()
if not itemdst:IsOk() or not itemsrc:IsOk() then return end
-- check if itemdst is a folder
local target = tree:GetItemFullName(itemdst)
if wx.wxDirExists(target) then
local source = tree:GetItemFullName(itemsrc)
-- check if moving the directory and target is a subfolder of source
if (target..pathsep):find("^"..q(source)..pathsep) then return end
renameItem(itemsrc, MergeFullPath(target, tree:GetItemText(itemsrc)))
end
end)
end
-- project
local projtree = wx.wxTreeCtrl(ide.frame, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxDefaultSize,
wx.wxTR_HAS_BUTTONS + wx.wxTR_SINGLE + wx.wxTR_LINES_AT_ROOT
+ wx.wxTR_EDIT_LABELS + wx.wxNO_BORDER)
projtree:SetFont(ide.font.fNormal)
filetree.projtreeCtrl = projtree
ide:GetProjectNotebook():AddPage(projtree, TR("Project"), true)
-- proj connectors
-- ---------------
treeSetConnectorsAndIcons(projtree)
-- proj functions
-- ---------------
function filetree:updateProjectDir(newdir)
if (not newdir) or not wx.wxDirExists(newdir) then return end
local dirname = wx.wxFileName.DirName(newdir)
if filetree.projdir and #filetree.projdir > 0
and dirname:SameAs(wx.wxFileName.DirName(filetree.projdir)) then return end
-- strip the last path separator if any
local newdir = dirname:GetPath(wx.wxPATH_GET_VOLUME)
-- save the current interpreter as it may be reset in onProjectClose
-- when the project event handlers manipulates interpreters
local intfname = ide.interpreter and ide.interpreter.fname
if filetree.projdir and #filetree.projdir > 0 then
PackageEventHandle("onProjectClose", filetree.projdir)
end
PackageEventHandle("onProjectPreLoad", newdir)
if ide.config.projectautoopen and filetree.projdir then
StoreRestoreProjectTabs(filetree.projdir, newdir, intfname)
end
filetree.projdir = newdir
filetree.projdirpartmap = {}
PrependStringToArray(
filetree.projdirlist,
newdir,
ide.config.projecthistorylength,
function(s1, s2) return dirname:SameAs(wx.wxFileName.DirName(s2)) end)
ProjectUpdateProjectDir(newdir,true)
treeSetRoot(projtree,newdir)
-- sync with the current editor window and activate selected file
local editor = GetEditor()
if editor then FileTreeMarkSelected(ide:GetDocument(editor):GetFilePath()) end
-- refresh Recent Projects menu item
ide.frame:AddPendingEvent(wx.wxUpdateUIEvent(ID_RECENTPROJECTS))
PackageEventHandle("onProjectLoad", newdir)
end
function FileTreeGetDir()
return (filetree.projdir and #filetree.projdir > 0
and wx.wxFileName.DirName(filetree.projdir):GetFullPath() or nil)
end
function FileTreeSetProjects(tab)
filetree.projdirlist = tab
if (tab and tab[1]) then
filetree:updateProjectDir(tab[1])
end
end
function FileTreeGetProjects()
return filetree.projdirlist
end
local function getProjectLabels()
local labels = {}
local fmt = ide.config.format.menurecentprojects or '%f'
for _, proj in ipairs(FileTreeGetProjects()) do
local config = ide.session.projects[proj]
local intfname = config and config[2] and config[2].interpreter or ide.interpreter:GetFileName()
local interpreter = intfname and ide.interpreters[intfname]
local parts = wx.wxFileName(proj..pathsep):GetDirs()
table.insert(labels, ExpandPlaceholders(fmt, {
f = proj,
i = interpreter and interpreter:GetName() or (intfname or '')..'?',
s = parts[#parts] or '',
}))
end
return labels
end
function FileTreeProjectListClear()
-- remove all items from the list except the current one
filetree.projdirlist = {FileTreeGetDir()}
end
function FileTreeProjectListUpdate(menu, items)
-- protect against recent project menu not being present
if not ide:FindMenuItem(ID_RECENTPROJECTS) then return end
local list = getProjectLabels()
for i=#list, 1, -1 do
local id = ID("file.recentprojects."..i)
local label = list[i]
if i <= items then -- this is an existing item; update the label
menu:FindItem(id):SetItemLabel(label)
else -- need to add an item
local item = wx.wxMenuItem(menu, id, label, "")
menu:Insert(items, item)
ide.frame:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, function()
wx.wxSafeYield() -- let the menu on screen (if any) disappear
ProjectUpdateProjectDir(FileTreeGetProjects()[i])
end)
end
-- disable the currently selected project
if i == 1 then menu:Enable(id, false) end
end
for i=items, #list+1, -1 do -- delete the rest if the list got shorter
menu:Delete(menu:FindItemByPosition(i-1))
end
return #list
end
local curr_file
function FileTreeMarkSelected(file)
if not file or not filetree.projdir or #filetree.projdir == 0 then return end
local item_id = wx.wxIsAbsolutePath(file) and projtree:FindItem(file)
-- if the select item is different from the current one
-- or the current one is the same, but not bold (which may happen when
-- the project is changed to one that includes the current item)
if curr_file ~= file
or item_id and not projtree:IsBold(item_id) then
if curr_file then
local curr_id = wx.wxIsAbsolutePath(curr_file) and projtree:FindItem(curr_file)
if curr_id and projtree:IsBold(curr_id) then
projtree:SetItemBold(curr_id, false)
end
end
if item_id then
projtree:EnsureVisible(item_id)
projtree:SetScrollPos(wx.wxHORIZONTAL, 0, true)
projtree:SetItemBold(item_id, true)
end
curr_file = file
if ide.wxver < "2.9.5" and ide.osname == 'Macintosh' then
projtree:Refresh()
end
end
end
function FileTreeFindByPartialName(name)
-- check if it's already cached
if filetree.projdirpartmap[name] then return filetree.projdirpartmap[name] end
-- this function may get a partial name that starts with ... and has
-- an abbreviated path (as generated by stack traces);
-- remove starting "..." if any and escape
local pattern = q(name:gsub("^%.%.%.","")):gsub("[\\/]", "[\\/]").."$"
local lpattern = pattern:lower()
for _, file in ipairs(FileSysGetRecursive(filetree.projdir, true)) do
if file:find(pattern) or iscaseinsensitive and file:lower():find(lpattern) then
filetree.projdirpartmap[name] = file
return file
end
end
return
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,567 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
-- Lomtik Software (J. Winwood & John Labenski)
---------------------------------------------------------
local ide = ide
local unpack = table.unpack or unpack
-- Pick some reasonable fixed width fonts to use for the editor
local function setFont(style, config)
return wx.wxFont(config.fontsize or 10, wx.wxFONTFAMILY_MODERN, style,
wx.wxFONTWEIGHT_NORMAL, false, config.fontname or "",
config.fontencoding or wx.wxFONTENCODING_DEFAULT)
end
ide.font.eNormal = setFont(wx.wxFONTSTYLE_NORMAL, ide.config.editor)
ide.font.eItalic = setFont(wx.wxFONTSTYLE_ITALIC, ide.config.editor)
ide.font.oNormal = setFont(wx.wxFONTSTYLE_NORMAL, ide.config.outputshell)
ide.font.oItalic = setFont(wx.wxFONTSTYLE_ITALIC, ide.config.outputshell)
-- treeCtrl font requires slightly different handling
do local gui, config = wx.wxTreeCtrl():GetFont(), ide.config.filetree
if config.fontsize then gui:SetPointSize(config.fontsize) end
if config.fontname then gui:SetFaceName(config.fontname) end
ide.font.fNormal = gui
end
-- ----------------------------------------------------------------------------
-- Create the wxFrame
-- ----------------------------------------------------------------------------
local function createFrame()
local frame = wx.wxFrame(wx.NULL, wx.wxID_ANY, GetIDEString("editor"),
wx.wxDefaultPosition, wx.wxSize(1100, 700))
frame:Center()
-- wrap into protected call as DragAcceptFiles fails on MacOS with
-- wxwidgets 2.8.12 even though it should work according to change notes
-- for 2.8.10: "Implemented wxWindow::DragAcceptFiles() on all platforms."
pcall(function() frame:DragAcceptFiles(true) end)
frame:Connect(wx.wxEVT_DROP_FILES,function(evt)
local files = evt:GetFiles()
if not files or #files == 0 then return end
for _, f in ipairs(files) do
LoadFile(f,nil,true)
end
end)
-- update best size of the toolbar after resizing
frame:Connect(wx.wxEVT_SIZE, function(event)
local mgr = ide:GetUIManager()
local toolbar = mgr:GetPane("toolbar")
if toolbar and toolbar:IsOk() then
toolbar:BestSize(event:GetSize():GetWidth(), ide:GetToolBar():GetClientSize():GetHeight())
mgr:Update()
end
end)
local menuBar = wx.wxMenuBar()
local statusBar = frame:CreateStatusBar(5)
local section_width = statusBar:GetTextExtent("OVRW")
statusBar:SetStatusStyles({wx.wxSB_FLAT, wx.wxSB_FLAT, wx.wxSB_FLAT, wx.wxSB_FLAT, wx.wxSB_FLAT})
statusBar:SetStatusWidths({-1, section_width, section_width, section_width*5, section_width*4})
statusBar:SetStatusText(GetIDEString("statuswelcome"))
statusBar:Connect(wx.wxEVT_LEFT_DOWN, function (event)
local rect = wx.wxRect()
statusBar:GetFieldRect(4, rect)
if rect:Contains(event:GetPosition()) then -- click on the interpreter
local menuitem = ide:FindMenuItem(ID.INTERPRETER)
if menuitem then
local menu = ide:CloneMenu(menuitem:GetSubMenu())
if menu then statusBar:PopupMenu(menu) end
end
end
end)
local mgr = wxaui.wxAuiManager()
mgr:SetManagedWindow(frame)
-- allow the panes to be larger than the defalt 1/3 of the main window size
mgr:SetDockSizeConstraint(0.8,0.8)
frame.menuBar = menuBar
frame.statusBar = statusBar
frame.uimgr = mgr
return frame
end
local function SCinB(id) -- shortcut in brackets
local shortcut = KSC(id):gsub("\t","")
return shortcut and #shortcut > 0 and (" ("..shortcut..")") or ""
end
local function menuDropDownPosition(event)
local tb = event:GetEventObject():DynamicCast('wxAuiToolBar')
local rect = tb:GetToolRect(event:GetId())
return ide.frame:ScreenToClient(tb:ClientToScreen(rect:GetBottomLeft()))
end
local function tbIconSize()
-- use large icons by default on OSX and on large screens
local iconsize = (tonumber(ide.config.toolbar and ide.config.toolbar.iconsize)
or ((ide.osname == 'Macintosh' or wx.wxGetClientDisplayRect():GetWidth() >= 1500) and 24 or 16))
if iconsize ~= 24 then iconsize = 16 end
return iconsize
end
local function createToolBar(frame)
local toolBar = wxaui.wxAuiToolBar(frame, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxDefaultSize,
wxaui.wxAUI_TB_PLAIN_BACKGROUND)
-- there are two sets of icons: use 24 on OSX and 16 on others.
local iconsize = tbIconSize()
local toolBmpSize = wx.wxSize(iconsize, iconsize)
local icons, prev = ide.config.toolbar.icons
for _, id in ipairs(icons) do
if icons[id] ~= false then -- skip explicitly disabled icons
if id == ID_SEPARATOR then
-- make sure that there are no two separators next to each other;
-- this may happen when some of the icons are disabled.
if prev ~= ID_SEPARATOR then toolBar:AddSeparator() end
else
local iconmap = ide.config.toolbar.iconmap[id]
if iconmap then
local icon, description = unpack(iconmap)
local isbitmap = type(icon) == "userdata" and icon:GetClassInfo():GetClassName() == "wxBitmap"
local bitmap = isbitmap and icon or ide:GetBitmap(icon, "TOOLBAR", toolBmpSize)
toolBar:AddTool(id, "", bitmap, (TR)(description)..SCinB(id))
end
end
prev = id
end
end
toolBar:SetToolDropDown(ID_OPEN, true)
toolBar:Connect(ID_OPEN, wxaui.wxEVT_COMMAND_AUITOOLBAR_TOOL_DROPDOWN, function(event)
if event:IsDropDownClicked() then
local menu = wx.wxMenu()
FileRecentListUpdate(menu)
toolBar:PopupMenu(menu, menuDropDownPosition(event))
else
event:Skip()
end
end)
toolBar:SetToolDropDown(ID_PROJECTDIRCHOOSE, true)
toolBar:Connect(ID_PROJECTDIRCHOOSE, wxaui.wxEVT_COMMAND_AUITOOLBAR_TOOL_DROPDOWN, function(event)
if event:IsDropDownClicked() then
local menu = wx.wxMenu()
FileTreeProjectListUpdate(menu, 0)
toolBar:PopupMenu(menu, menuDropDownPosition(event))
else
event:Skip()
end
end)
toolBar:GetArtProvider():SetElementSize(wxaui.wxAUI_TBART_GRIPPER_SIZE, 0)
toolBar:Realize()
frame.toolBar = toolBar
return toolBar
end
local function createNotebook(frame)
-- notebook for editors
local notebook = wxaui.wxAuiNotebook(frame, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxDefaultSize,
wxaui.wxAUI_NB_DEFAULT_STYLE + wxaui.wxAUI_NB_TAB_EXTERNAL_MOVE
+ wxaui.wxAUI_NB_WINDOWLIST_BUTTON + wx.wxNO_BORDER)
-- wxEVT_SET_FOCUS could be used, but it only works on Windows with wx2.9.5+
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_PAGE_CHANGED,
function (event)
local ed = GetEditor(notebook:GetSelection())
local doc = ed and ed:GetId() and ide.openDocuments[ed:GetId()]
-- skip activation when any of the following is true:
-- (1) there is no document yet, the editor tab was just added,
-- so no changes needed as there will be a proper later call;
-- (2) the page change event was triggered after a tab is closed;
-- (3) on OSX from AddPage event when changing from the last tab
-- (this is to work around a duplicate event generated in this case
-- that first activates the added tab and then some other tab (2.9.5)).
local double = (ide.osname == 'Macintosh'
and event:GetOldSelection() == notebook:GetPageCount()
and debug:traceback():find("'AddPage'"))
if doc and event:GetOldSelection() ~= -1 and not double then
SetEditorSelection(notebook:GetSelection())
end
end)
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_PAGE_CLOSE,
function (event)
ClosePage(event:GetSelection())
event:Veto() -- don't propagate the event as the page is already closed
end)
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_BG_DCLICK,
function (event)
-- as this event can be on different tab controls,
-- need to find the control to add the page to
local tabctrl = event:GetEventObject():DynamicCast("wxAuiTabCtrl")
-- check if the active page is in the current control
local active = tabctrl:GetActivePage()
if (active >= 0 and tabctrl:GetPage(active).window
~= notebook:GetPage(notebook:GetSelection())) then
-- if not, need to activate the control that was clicked on;
-- find the last window and switch to it (assuming there is always one)
assert(tabctrl:GetPageCount() >= 1, "Expected at least one page in a notebook tab control.")
local lastwin = tabctrl:GetPage(tabctrl:GetPageCount()-1).window
notebook:SetSelection(notebook:GetPageIndex(lastwin))
end
NewFile()
end)
-- tabs can be dragged around which may change their indexes;
-- when this happens stored indexes need to be updated to reflect the change.
-- there is DRAG_DONE event that I'd prefer to use, but it
-- doesn't fire for some reason using wxwidgets 2.9.5 (tested on Windows).
if ide.wxver >= "2.9.5" then
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_END_DRAG,
function (event)
for page = 0, notebook:GetPageCount()-1 do
local editor = GetEditor(page)
if editor then ide.openDocuments[editor:GetId()].index = page end
end
-- first set the selection on the dragged tab to reset its state
notebook:SetSelection(event:GetSelection())
-- select the content of the tab after drag is done
SetEditorSelection(event:GetSelection())
event:Skip()
end)
end
local selection
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_TAB_RIGHT_UP,
function (event)
-- event:GetSelection() returns the index *inside the current tab*;
-- for split notebooks, this may not be the same as the index
-- in the notebook we are interested in here
local idx = event:GetSelection()
local tabctrl = event:GetEventObject():DynamicCast("wxAuiTabCtrl")
-- save tab index the event is for
selection = notebook:GetPageIndex(tabctrl:GetPage(idx).window)
local menu = wx.wxMenu {
{ ID_CLOSE, TR("&Close Page") },
{ ID_CLOSEALL, TR("Close A&ll Pages") },
{ ID_CLOSEOTHER, TR("Close &Other Pages") },
{ },
{ ID_SAVE, TR("&Save") },
{ ID_SAVEAS, TR("Save &As...") },
{ },
{ ID_COPYFULLPATH, TR("Copy Full Path") },
{ ID_SHOWLOCATION, TR("Show Location") },
}
PackageEventHandle("onMenuEditorTab", menu, notebook, event, selection)
notebook:PopupMenu(menu)
end)
local function IfAtLeastOneTab(event)
event:Enable(notebook:GetPageCount() > 0)
if ide.osname == 'Macintosh' and (event:GetId() == ID_CLOSEALL
or event:GetId() == ID_CLOSE and notebook:GetPageCount() <= 1)
then event:Enable(false) end
end
notebook:Connect(ID_SAVE, wx.wxEVT_COMMAND_MENU_SELECTED, function ()
ide:GetDocument(GetEditor(selection)):Save()
end)
notebook:Connect(ID_SAVE, wx.wxEVT_UPDATE_UI, function(event)
local doc = ide:GetDocument(GetEditor(selection))
event:Enable(doc:IsModified() or doc:IsNew())
end)
notebook:Connect(ID_SAVEAS, wx.wxEVT_COMMAND_MENU_SELECTED, function()
SaveFileAs(GetEditor(selection))
end)
notebook:Connect(ID_SAVEAS, wx.wxEVT_UPDATE_UI, IfAtLeastOneTab)
notebook:Connect(ID_CLOSE, wx.wxEVT_COMMAND_MENU_SELECTED, function()
ClosePage(selection)
end)
notebook:Connect(ID_CLOSE, wx.wxEVT_UPDATE_UI, IfAtLeastOneTab)
notebook:Connect(ID_CLOSEALL, wx.wxEVT_COMMAND_MENU_SELECTED, function()
CloseAllPagesExcept(nil)
end)
notebook:Connect(ID_CLOSEALL, wx.wxEVT_UPDATE_UI, IfAtLeastOneTab)
notebook:Connect(ID_CLOSEOTHER, wx.wxEVT_COMMAND_MENU_SELECTED, function ()
CloseAllPagesExcept(selection)
end)
notebook:Connect(ID_CLOSEOTHER, wx.wxEVT_UPDATE_UI, function (event)
event:Enable(notebook:GetPageCount() > 1)
end)
notebook:Connect(ID_SHOWLOCATION, wx.wxEVT_COMMAND_MENU_SELECTED, function()
ShowLocation(ide:GetDocument(GetEditor(selection)):GetFilePath())
end)
notebook:Connect(ID_SHOWLOCATION, wx.wxEVT_UPDATE_UI, IfAtLeastOneTab)
notebook:Connect(ID_COPYFULLPATH, wx.wxEVT_COMMAND_MENU_SELECTED, function()
ide:CopyToClipboard(ide:GetDocument(GetEditor(selection)):GetFilePath())
end)
frame.notebook = notebook
return notebook
end
local function addDND(notebook)
-- this handler allows dragging tabs into this notebook
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_ALLOW_DND,
function (event)
local notebookfrom = event:GetDragSource()
if notebookfrom ~= ide.frame.notebook then
-- disable cross-notebook movement of specific tabs
local win = notebookfrom:GetPage(event:GetSelection())
if not win then return end
local winid = win:GetId()
if winid == ide:GetOutput():GetId()
or winid == ide:GetConsole():GetId()
or winid == ide:GetProjectTree():GetId()
or ide.findReplace:IsPreview(win) -- search results preview
then return end
local mgr = ide.frame.uimgr
local pane = mgr:GetPane(notebookfrom)
if not pane:IsOk() then return end -- not a managed window
if pane:IsFloating() then
notebookfrom:GetParent():Hide()
else
pane:Hide()
mgr:Update()
end
mgr:DetachPane(notebookfrom)
-- this is a workaround for wxwidgets bug (2.9.5+) that combines
-- content from two windows when tab is dragged over an active tab.
local mouse = wx.wxGetMouseState()
local mouseatpoint = wx.wxPoint(mouse:GetX(), mouse:GetY())
local ok, tabs = pcall(function() return wx.wxFindWindowAtPoint(mouseatpoint):DynamicCast("wxAuiTabCtrl") end)
if ok then tabs:SetNoneActive() end
event:Allow()
end
end)
-- these handlers allow dragging tabs out of this notebook.
-- I couldn't find a good way to stop dragging event as it's not known
-- where the event is going to end when it's started, so we manipulate
-- the flag that allows splits and disable it when needed.
-- It is then enabled in BEGIN_DRAG event.
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_BEGIN_DRAG,
function (event)
event:Skip()
-- allow dragging if it was disabled earlier
local flags = notebook:GetWindowStyleFlag()
if bit.band(flags, wxaui.wxAUI_NB_TAB_SPLIT) == 0 then
notebook:SetWindowStyleFlag(flags + wxaui.wxAUI_NB_TAB_SPLIT)
end
end)
-- there is currently no support in wxAuiNotebook for dragging tabs out.
-- This is implemented as removing a tab that was dragged out and
-- recreating it with the right control. This is complicated by the fact
-- that tabs can be split, so if the destination is withing the area where
-- splits happen, the tab is not removed.
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_END_DRAG,
function (event)
event:Skip()
local mgr = ide.frame.uimgr
local win = mgr:GetPane(notebook).window
local x = win:GetScreenPosition():GetX()
local y = win:GetScreenPosition():GetY()
local w, h = win:GetSize():GetWidth(), win:GetSize():GetHeight()
local mouse = wx.wxGetMouseState()
local mx, my = mouse:GetX(), mouse:GetY()
if mx >= x and mx <= x + w and my >= y and my <= y + h then return end
-- disallow split as the target is outside the notebook
local flags = notebook:GetWindowStyleFlag()
if bit.band(flags, wxaui.wxAUI_NB_TAB_SPLIT) ~= 0 then
notebook:SetWindowStyleFlag(flags - wxaui.wxAUI_NB_TAB_SPLIT)
end
-- don't allow dragging out single tabs from tab ctrl
-- as wxwidgets doesn't like removing pages from split notebooks.
local tabctrl = event:GetEventObject():DynamicCast("wxAuiTabCtrl")
if tabctrl:GetPageCount() == 1 then return end
local idx = event:GetSelection() -- index within the current tab ctrl
local selection = notebook:GetPageIndex(tabctrl:GetPage(idx).window)
local label = notebook:GetPageText(selection)
local pane = ide:RestorePanelByLabel(label)
if not pane then return end
pane:FloatingPosition(mx-10, my-10)
pane:Show()
notebook:RemovePage(selection)
mgr:Update()
end)
end
local function createBottomNotebook(frame)
-- bottomnotebook (errorlog,shellbox)
local bottomnotebook = wxaui.wxAuiNotebook(frame, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxDefaultSize,
wxaui.wxAUI_NB_DEFAULT_STYLE + wxaui.wxAUI_NB_TAB_EXTERNAL_MOVE
- wxaui.wxAUI_NB_CLOSE_ON_ACTIVE_TAB + wx.wxNO_BORDER)
addDND(bottomnotebook)
bottomnotebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_PAGE_CHANGED,
function (event)
if not ide.findReplace then return end
local nb = event:GetEventObject():DynamicCast("wxAuiNotebook")
local preview = ide.findReplace:IsPreview(nb:GetPage(nb:GetSelection()))
local flags = nb:GetWindowStyleFlag()
if preview and bit.band(flags, wxaui.wxAUI_NB_CLOSE_ON_ACTIVE_TAB) == 0 then
nb:SetWindowStyleFlag(flags + wxaui.wxAUI_NB_CLOSE_ON_ACTIVE_TAB)
elseif not preview and bit.band(flags, wxaui.wxAUI_NB_CLOSE_ON_ACTIVE_TAB) ~= 0 then
nb:SetWindowStyleFlag(flags - wxaui.wxAUI_NB_CLOSE_ON_ACTIVE_TAB)
end
end)
-- disallow tabs closing
bottomnotebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_PAGE_CLOSE,
function (event)
local nb = event:GetEventObject():DynamicCast("wxAuiNotebook")
if ide.findReplace
and ide.findReplace:IsPreview(nb:GetPage(nb:GetSelection())) then
event:Skip()
else
event:Veto()
end
end)
local errorlog = ide:CreateStyledTextCtrl(bottomnotebook, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxBORDER_NONE)
errorlog:Connect(wx.wxEVT_CONTEXT_MENU,
function (event)
local menu = wx.wxMenu {
{ ID_UNDO, TR("&Undo") },
{ ID_REDO, TR("&Redo") },
{ },
{ ID_CUT, TR("Cu&t") },
{ ID_COPY, TR("&Copy") },
{ ID_PASTE, TR("&Paste") },
{ ID_SELECTALL, TR("Select &All") },
{ },
{ ID_CLEAROUTPUT, TR("C&lear Output Window") },
}
PackageEventHandle("onMenuOutput", menu, errorlog, event)
errorlog:PopupMenu(menu)
end)
errorlog:Connect(ID_CLEAROUTPUT, wx.wxEVT_COMMAND_MENU_SELECTED,
function(event) ClearOutput(true) end)
local shellbox = ide:CreateStyledTextCtrl(bottomnotebook, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxDefaultSize, wx.wxBORDER_NONE)
local menupos
shellbox:Connect(wx.wxEVT_CONTEXT_MENU,
function (event)
local menu = wx.wxMenu {
{ ID_UNDO, TR("&Undo") },
{ ID_REDO, TR("&Redo") },
{ },
{ ID_CUT, TR("Cu&t") },
{ ID_COPY, TR("&Copy") },
{ ID_PASTE, TR("&Paste") },
{ ID_SELECTALL, TR("Select &All") },
{ },
{ ID_SELECTCONSOLECOMMAND, TR("&Select Command") },
{ ID_CLEARCONSOLE, TR("C&lear Console Window") },
}
menupos = event:GetPosition()
PackageEventHandle("onMenuConsole", menu, shellbox, event)
shellbox:PopupMenu(menu)
end)
shellbox:Connect(ID_SELECTCONSOLECOMMAND, wx.wxEVT_COMMAND_MENU_SELECTED,
function(event) ConsoleSelectCommand(menupos) end)
shellbox:Connect(ID_CLEARCONSOLE, wx.wxEVT_COMMAND_MENU_SELECTED,
function(event) ide:GetConsole():Erase() end)
bottomnotebook:AddPage(errorlog, TR("Output"), true)
bottomnotebook:AddPage(shellbox, TR("Local console"), false)
bottomnotebook.errorlog = errorlog
bottomnotebook.shellbox = shellbox
frame.bottomnotebook = bottomnotebook
return bottomnotebook
end
local function createProjNotebook(frame)
local projnotebook = wxaui.wxAuiNotebook(frame, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxDefaultSize,
wxaui.wxAUI_NB_DEFAULT_STYLE + wxaui.wxAUI_NB_TAB_EXTERNAL_MOVE
- wxaui.wxAUI_NB_CLOSE_ON_ACTIVE_TAB + wx.wxNO_BORDER)
addDND(projnotebook)
-- disallow tabs closing
projnotebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_PAGE_CLOSE,
function (event) event:Veto() end)
frame.projnotebook = projnotebook
return projnotebook
end
-- ----------------------------------------------------------------------------
-- Add the child windows to the frame
local frame = createFrame()
ide.frame = frame
createToolBar(frame)
createNotebook(frame)
createProjNotebook(frame)
createBottomNotebook(frame)
do
local frame = ide.frame
local mgr = frame.uimgr
--[[mgr:AddPane(frame.toolBar, wxaui.wxAuiPaneInfo():
Name("toolbar"):Caption("Toolbar"):
ToolbarPane():Top():CloseButton(false):PaneBorder(false):
LeftDockable(false):RightDockable(false))]]--
mgr:AddPane(frame.notebook, wxaui.wxAuiPaneInfo():
Name("notebook"):
CenterPane():PaneBorder(false))
mgr:AddPane(frame.projnotebook, wxaui.wxAuiPaneInfo():
Name("projpanel"):CaptionVisible(false):
MinSize(200,200):FloatingSize(200,400):
Left():Layer(1):Position(1):PaneBorder(false):
CloseButton(true):MaximizeButton(false):PinButton(true))
mgr:AddPane(frame.bottomnotebook, wxaui.wxAuiPaneInfo():
Name("bottomnotebook"):CaptionVisible(false):
MinSize(100,100):BestSize(200,200):FloatingSize(400,200):
Bottom():Layer(1):Position(1):PaneBorder(false):
CloseButton(true):MaximizeButton(false):PinButton(true))
--mgr:GetPane("toolbar"):Show(false)
mgr:GetPane("projpanel"):Show(false)
mgr:GetPane("bottomnotebook"):Show(false)
if type(ide.config.bordersize) == 'number' then
for _, uimgr in pairs {mgr, frame.notebook:GetAuiManager(),
frame.bottomnotebook:GetAuiManager(), frame.projnotebook:GetAuiManager()} do
uimgr:GetArtProvider():SetMetric(wxaui.wxAUI_DOCKART_SASH_SIZE,
ide.config.bordersize)
end
end
for _, nb in pairs {frame.bottomnotebook, frame.projnotebook} do
nb:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_BG_DCLICK,
function() PaneFloatToggle(nb) end)
end
mgr.defaultPerspective = mgr:SavePerspective()
end

View File

@ -0,0 +1,190 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
-- Generate a unique new wxWindowID
local ID_IDCOUNTER = wx.wxID_HIGHEST + 1
function NewID()
ID_IDCOUNTER = ID_IDCOUNTER + 1
return ID_IDCOUNTER
end
-- some Ubuntu versions (Ubuntu 13.10) ignore labels on stock menu IDs,
-- so don't use stock IDs on Linux
local linux = ide.osname == 'Unix'
ID_SEPARATOR = NewID()
-- File menu
ID_NEW = linux and NewID() or wx.wxID_NEW
ID_OPEN = linux and NewID() or wx.wxID_OPEN
ID_CLOSE = NewID()
ID_CLOSEALL = NewID()
ID_CLOSEOTHER = NewID()
ID_NEWFILE = NewID()
ID_NEWDIRECTORY = NewID()
ID_RENAMEFILE = NewID()
ID_DELETEFILE = NewID()
ID_SAVE = linux and NewID() or wx.wxID_SAVE
ID_SAVEAS = linux and NewID() or wx.wxID_SAVEAS
ID_SAVEALL = NewID()
ID_RECENTFILES = NewID()
ID_RECENTFILESCLEAR = NewID()
ID_RECENTFILESPREV = NewID()
ID_RECENTFILESNEXT = NewID()
ID_RECENTPROJECTS = NewID()
ID_RECENTPROJECTSCLEAR = NewID()
ID_RECENTPROJECTSPREV = NewID()
ID_PRINT = NewID()
ID_PAGESETUP = NewID()
ID_EXIT = linux and NewID() or wx.wxID_EXIT
-- Edit menu
ID_CUT = linux and NewID() or wx.wxID_CUT
ID_COPY = linux and NewID() or wx.wxID_COPY
ID_PASTE = linux and NewID() or wx.wxID_PASTE
ID_SELECTALL = linux and NewID() or wx.wxID_SELECTALL
ID_UNDO = linux and NewID() or wx.wxID_UNDO
ID_REDO = linux and NewID() or wx.wxID_REDO
ID_SHOWTOOLTIP = NewID()
ID_AUTOCOMPLETE = NewID()
ID_AUTOCOMPLETEENABLE = NewID()
ID_COMMENT = NewID()
ID_FOLD = NewID()
ID_CLEARDYNAMICWORDS = NewID()
ID_SOURCE = NewID()
ID_REINDENT = NewID()
ID_BOOKMARK = NewID()
ID_BOOKMARKTOGGLE = NewID()
ID_BOOKMARKNEXT = NewID()
ID_BOOKMARKPREV = NewID()
ID_NAVIGATE = NewID()
ID_NAVIGATETOFILE = NewID()
ID_NAVIGATETOLINE = NewID()
ID_NAVIGATETOSYMBOL = NewID()
ID_NAVIGATETOMETHOD = NewID()
-- don't use wx.wxID_PREFERENCES to avoid merging with OSX app menu, because
-- Apple guidelines describe Preferences as a "normal" item without submenus.
ID_PREFERENCES = NewID()
ID_PREFERENCESSYSTEM = NewID()
ID_PREFERENCESUSER = NewID()
-- Search menu
ID_FIND = linux and NewID() or wx.wxID_FIND
ID_FINDNEXT = NewID()
ID_FINDPREV = NewID()
ID_FINDSELECTNEXT = NewID()
ID_FINDSELECTPREV = NewID()
ID_REPLACE = NewID()
ID_FINDINFILES = NewID()
ID_REPLACEINFILES = NewID()
ID_SORT = NewID()
-- View menu
ID_VIEWFILETREE = NewID()
ID_VIEWOUTPUT = NewID()
ID_VIEWCALLSTACK = NewID()
ID_VIEWWATCHWINDOW = NewID()
ID_VIEWOUTLINE = NewID()
ID_VIEWMARKERS = NewID()
ID_VIEWTOOLBAR = NewID()
ID_VIEWSTATUSBAR = NewID()
ID_VIEWDEFAULTLAYOUT = NewID()
ID_VIEWFULLSCREEN = NewID()
ID_VIEWMINIMIZE = NewID()
ID_ZOOM = NewID()
ID_ZOOMRESET = NewID()
ID_ZOOMIN = NewID()
ID_ZOOMOUT = NewID()
-- Project menu
ID_BREAKPOINT = NewID()
ID_BREAKPOINTTOGGLE = NewID()
ID_BREAKPOINTNEXT = NewID()
ID_BREAKPOINTPREV = NewID()
ID_TOGGLEBREAKPOINT = ID_BREAKPOINTTOGGLE -- for compatibility; to remove after v1.30
ID_COMPILE = NewID()
ID_ANALYZE = NewID()
ID_RUN = NewID()
ID_RUNNOW = NewID()
ID_ATTACHDEBUG = NewID()
ID_STARTDEBUG = NewID()
ID_STOPDEBUG = NewID()
ID_DETACHDEBUG = NewID()
ID_STEP = NewID()
ID_STEPOVER = NewID()
ID_STEPOUT = NewID()
ID_RUNTO = NewID()
ID_BREAK = NewID()
ID_TRACE = NewID()
ID_CLEAROUTPUT = NewID()
ID_CLEARCONSOLE = NewID()
ID_COMMANDLINEPARAMETERS = NewID()
ID_INTERPRETER = NewID()
ID_PROJECTDIR = NewID()
ID_PROJECTDIRFROMFILE = NewID()
ID_PROJECTDIRFROMDIR = NewID()
ID_PROJECTDIRCHOOSE = NewID()
-- Help menu
ID_ABOUT = linux and NewID() or wx.wxID_ABOUT
ID_HELPPROJECT = NewID()
ID_HELPDOCUMENTATION = NewID()
ID_HELPGETTINGSTARTED = NewID()
ID_HELPTUTORIALS = NewID()
ID_HELPFAQ = NewID()
ID_HELPCOMMUNITY = NewID()
-- Watch window menu items
ID_ADDWATCH = NewID()
ID_EDITWATCH = NewID()
ID_DELETEWATCH = NewID()
ID_COPYWATCHVALUE = NewID()
-- Editor popup menu items
ID_GOTODEFINITION = NewID()
ID_RENAMEALLINSTANCES = NewID()
ID_REPLACEALLSELECTIONS = NewID()
ID_QUICKADDWATCH = NewID()
ID_QUICKEVAL = NewID()
ID_ADDTOSCRATCHPAD = NewID()
-- filetree menu
ID_HIDEEXTENSION = NewID()
ID_SETSTARTFILE = NewID()
ID_UNSETSTARTFILE = NewID()
ID_SHOWEXTENSION = NewID()
ID_SHOWEXTENSIONALL = NewID()
ID_MAPDIRECTORY = NewID()
ID_UNMAPDIRECTORY = NewID()
ID_OPENEXTENSION = NewID()
ID_COPYFULLPATH = NewID()
ID_SHOWLOCATION = NewID()
ID_REFRESH = NewID()
ID_SYMBOLDIRREFRESH = NewID()
ID_SYMBOLDIRINDEX = NewID()
ID_SYMBOLDIRDISABLE = NewID()
ID_SYMBOLDIRENABLE = NewID()
-- outline menu
ID_OUTLINESORT = NewID()
-- console menu
ID_SELECTCONSOLECOMMAND = NewID()
-- search toolbar
ID_FINDALL = NewID()
ID_FINDREPLACENEXT = NewID()
ID_FINDREPLACEALL = NewID()
ID_FINDSETDIR = NewID()
ID_FINDSETTOPROJDIR = NewID()
ID_FINDOPTSCOPE = NewID()
ID_FINDOPTSTATUS = NewID()
ID_FINDOPTDIRECTION = NewID()
ID_FINDOPTWRAPWROUND = NewID()
ID_FINDOPTSELECTION = NewID()
ID_FINDOPTWORD = NewID()
ID_FINDOPTCASE = NewID()
ID_FINDOPTREGEX = NewID()
ID_FINDOPTCONTEXT = NewID()
ID_FINDOPTSUBDIR = NewID()
ID_FINDOPTMULTIRESULTS = NewID()
ID_RECENTSCOPECLEAR = NewID()
local ids = {}
function IDgen (name)
ids[name] = ids[name] or NewID()
return ids[name]
end
function IDget (name) return ids[name] end
ID = setmetatable({}, ide.proto.ID)

View File

@ -0,0 +1,243 @@
-- Copyright 2012-15 Paul Kulchenko, ZeroBrane LLC
-- Integration with LuaInspect
---------------------------------------------------------
local M, LA, LI, T = {}
local function init()
if LA then return end
-- metalua is using 'checks', which noticeably slows the execution
-- stab it with out own
package.loaded.checks = {}
checks = function() end
LA = require "luainspect.ast"
LI = require "luainspect.init"
T = require "luainspect.types"
end
function M.pos2line(pos)
return pos and 1 + select(2, M.src:sub(1,pos):gsub(".-\n[^\n]*", ""))
end
function M.warnings_from_string(src, file)
init()
local ast, err, linenum, colnum = LA.ast_from_string(src, file)
if not ast and err then return nil, err, linenum, colnum end
LI.uninspect(ast)
if ide.config.staticanalyzer.infervalue then
local tokenlist = LA.ast_to_tokenlist(ast, src)
LI.clear_cache()
LI.inspect(ast, tokenlist, src)
LI.mark_related_keywords(ast, tokenlist, src)
else
-- stub out LI functions that depend on tokenlist,
-- which is not built in the "fast" mode
local ec, iv = LI.eval_comments, LI.infer_values
LI.eval_comments, LI.infer_values = function() end, function() end
LI.inspect(ast, nil, src)
LA.ensure_parents_marked(ast)
LI.eval_comments, LI.infer_values = ec, iv
end
local globinit = {arg = true} -- skip `arg` global variable
local spec = GetSpec(wx.wxFileName(file):GetExt())
for k in pairs(spec and GetApi(spec.apitype or "none").ac.childs or {}) do
globinit[k] = true
end
M.src, M.file = src, file
return M.show_warnings(ast, globinit)
end
local function cleanError(err)
return err and err:gsub(".-:%d+: file%s+",""):gsub(", line (%d+), char %d+", ":%1")
end
function AnalyzeFile(file)
local src, err = FileRead(file)
if not src and err then return nil, TR("Can't open file '%s': %s"):format(file, err) end
local warn, err, line, pos = M.warnings_from_string(src, file)
return warn, cleanError(err), line, pos
end
function AnalyzeString(src, file)
local warn, err, line, pos = M.warnings_from_string(src, file or "<string>")
return warn, cleanError(err), line, pos
end
function M.show_warnings(top_ast, globinit)
local warnings = {}
local function warn(msg, linenum, path)
warnings[#warnings+1] = (path or M.file or "?") .. ":" .. (linenum or M.pos2line(M.ast.pos) or 0) .. ": " .. msg
end
local function known(o) return not T.istype[o] end
local function index(f) -- build abc.def.xyz name recursively
if not f or f.tag ~= 'Index' or not f[1] or not f[2] then return end
local main = f[1].tag == 'Id' and f[1][1] or index(f[1])
return main and type(f[2][1]) == "string" and (main .. '.' .. f[2][1]) or nil
end
local globseen, isseen, fieldseen = globinit or {}, {}, {}
LA.walk(top_ast, function(ast)
M.ast = ast
local path, line = tostring(ast.lineinfo):gsub('<C|','<'):match('<([^|]+)|L(%d+)')
local name = ast[1]
-- check if we're masking a variable in the same scope
if ast.localmasking and name ~= '_' and
ast.level == ast.localmasking.level then
local linenum = ast.localmasking.lineinfo
and tostring(ast.localmasking.lineinfo.first):match('|L(%d+)')
or M.pos2line(ast.localmasking.pos)
local parent = ast.parent and ast.parent.parent
local func = parent and parent.tag == 'Localrec'
warn("local " .. (func and 'function' or 'variable') .. " '" ..
name .. "' masks earlier declaration " ..
(linenum and "on line " .. linenum or "in the same scope"),
line, path)
end
if ast.localdefinition == ast and not ast.isused and
not ast.isignore then
local parent = ast.parent and ast.parent.parent
local isparam = parent and parent.tag == 'Function'
if isparam then
if name ~= 'self' then
local func = parent.parent and parent.parent.parent
local assignment = not func.tag or func.tag == 'Set' or func.tag == 'Localrec'
-- anonymous functions can also be defined in expressions,
-- for example, 'Op' or 'Return' tags
local expression = not assignment and func.tag
local func1 = func[1][1]
local fname = assignment and func1 and type(func1[1]) == 'string'
and func1[1] or (func1 and func1.tag == 'Index' and index(func1))
-- "function foo(bar)" => func.tag == 'Set'
-- `Set{{`Id{"foo"}},{`Function{{`Id{"bar"}},{}}}}
-- "local function foo(bar)" => func.tag == 'Localrec'
-- "local _, foo = 1, function(bar)" => func.tag == 'Local'
-- "print(function(bar) end)" => func.tag == nil
-- "a = a or function(bar) end" => func.tag == nil
-- "return(function(bar) end)" => func.tag == 'Return'
-- "function tbl:foo(bar)" => func.tag == 'Set'
-- `Set{{`Index{`Id{"tbl"},`String{"foo"}}},{`Function{{`Id{"self"},`Id{"bar"}},{}}}}
-- "function tbl.abc:foo(bar)" => func.tag == 'Set'
-- `Set{{`Index{`Index{`Id{"tbl"},`String{"abc"}},`String{"foo"}}},{`Function{{`Id{"self"},`Id{"bar"}},{}}}},
warn("unused parameter '" .. name .. "'" ..
(func and (assignment or expression)
and (fname and func.tag
and (" in function '" .. fname .. "'")
or " in anonymous function")
or ""),
line, path)
end
else
if parent and parent.tag == 'Localrec' then -- local function foo...
warn("unused local function '" .. name .. "'", line, path)
else
warn("unused local variable '" .. name .. "'; "..
"consider removing or replacing with '_'", line, path)
end
end
end
-- added check for "fast" mode as ast.seevalue relies on value evaluation,
-- which is very slow even on simple and short scripts
if ide.config.staticanalyzer.infervalue and ast.isfield
and not(known(ast.seevalue.value) and ast.seevalue.value ~= nil) then
if not fieldseen[name] then
fieldseen[name] = true
local var = index(ast.parent)
local parent = ast.parent and var
and (" in '"..var:gsub("%."..name.."$","").."'")
or ""
local tblref = ast.parent and ast.parent[1]
local localparam = (tblref and tblref.localdefinition
and tblref.localdefinition.isparam)
if not localparam then
warn("first use of unknown field '" .. name .."'"..parent,
ast.lineinfo and tostring(ast.lineinfo.first):match('|L(%d+)'), path)
end
end
elseif ast.tag == 'Id' and not ast.localdefinition and not ast.definedglobal then
if not globseen[name] then
globseen[name] = true
local parent = ast.parent
-- if being called and not one of the parameters
if parent and parent.tag == 'Call' and parent[1] == ast then
warn("first use of unknown global function '" .. name .. "'", line, path)
else
warn("first use of unknown global variable '" .. name .. "'", line, path)
end
end
elseif ast.tag == 'Id' and not ast.localdefinition and ast.definedglobal then
local parent = ast.parent and ast.parent.parent
if parent and parent.tag == 'Set' and not globseen[name] -- report assignments to global
-- only report if it is on the left side of the assignment
-- this is a bit tricky as it can be assigned as part of a, b = c, d
-- `Set{ {lhs+} {expr+} } -- lhs1, lhs2... = e1, e2...
and parent[1] == ast.parent
and parent[2][1].tag ~= "Function" then -- but ignore global functions
warn("first assignment to global variable '" .. name .. "'", line, path)
globseen[name] = true
end
elseif (ast.tag == 'Set' or ast.tag == 'Local') and #(ast[2]) > #(ast[1]) then
warn(("value discarded in multiple assignment: %d values assigned to %d variable%s")
:format(#(ast[2]), #(ast[1]), #(ast[1]) > 1 and 's' or ''), line, path)
end
local vast = ast.seevalue or ast
local note = vast.parent
and (vast.parent.tag == 'Call' or vast.parent.tag == 'Invoke')
and vast.parent.note
if note and not isseen[vast.parent] and type(name) == "string" then
isseen[vast.parent] = true
warn("function '" .. name .. "': " .. note, line, path)
end
end)
return warnings
end
local frame = ide.frame
-- insert after "Compile" item
local _, menu, compilepos = ide:FindMenuItem(ID_COMPILE)
if compilepos then
menu:Insert(compilepos+1, ID_ANALYZE, TR("Analyze")..KSC(ID_ANALYZE), TR("Analyze the source code"))
end
local function analyzeProgram(editor)
-- save all files (if requested) for "infervalue" analysis to keep the changes on disk
if ide.config.editor.saveallonrun and ide.config.staticanalyzer.infervalue then SaveAll(true) end
if ide:GetLaunchedProcess() == nil and not ide:GetDebugger():IsConnected() then ClearOutput() end
DisplayOutput("Analyzing the source code")
frame:Update()
local editorText = editor:GetTextDyn()
local doc = ide:GetDocument(editor)
local filePath = doc:GetFilePath() or doc:GetFileName()
local warn, err = M.warnings_from_string(editorText, filePath)
if err then -- report compilation error
DisplayOutputLn((": not completed.\n%s"):format(cleanError(err)))
return false
end
DisplayOutputLn((": %s warning%s.")
:format(#warn > 0 and #warn or 'no', #warn == 1 and '' or 's'))
DisplayOutputNoMarker(table.concat(warn, "\n") .. (#warn > 0 and "\n" or ""))
return true -- analyzed ok
end
frame:Connect(ID_ANALYZE, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
ActivateOutput()
local editor = GetEditor()
if not analyzeProgram(editor) then
CompileProgram(editor, { reportstats = false, keepoutput = true })
end
end)
frame:Connect(ID_ANALYZE, wx.wxEVT_UPDATE_UI,
function (event) event:Enable(GetEditor() ~= nil) end)

View File

@ -0,0 +1,71 @@
-- Copyright 2011-14 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
ide.iofilters["GermanUtf8Ascii"] = {
-- this function converts some utf8 character output. It's a hack.
-- Since luxinia is not utf8 prepared, this is still necessary.
-- if you wish to turn this off, edit user.lua and set this filefunction to nil
output = function (fpath, content)
local utf8escape = ("string").char(195)
-- only simple cases are handled (umlauts)
local chr = ("string").char
local charconv = {
[chr(164)] = chr(228), -- ä
[chr(182)] = chr(246), -- ö
[chr(188)] = chr(252), -- ü
[chr(132)] = chr(196), -- Ä
[chr(150)] = chr(214), -- Ö
[chr(156)] = chr(220), -- Ü
[chr(159)] = chr(223), -- ß
}
return content : gsub (utf8escape.."(.)",charconv)
end,
-- this function is another hack to read an ANSI encoded
-- file and converts the umlauts to utf8 chars
input = function (fpath, content)
local utf8escape = ("string").char(195)
local chr = ("string").char
local charconv = {
[chr(228)] = utf8escape..chr(164), -- ä
[chr(246)] = utf8escape..chr(182), -- ö
[chr(252)] = utf8escape..chr(188), -- ü
[chr(196)] = utf8escape..chr(132), -- Ä
[chr(214)] = utf8escape..chr(150), -- Ö
[chr(220)] = utf8escape..chr(156), -- Ü
[chr(223)] = utf8escape..chr(159), -- ß
}
local lst = "["
for k in pairs(charconv) do lst = lst .. k end
lst = lst.."]"
return content:gsub(lst,charconv)
end,
}
ide.iofilters["0d0d0aFix"] = {
-- this function converts 0d0d0a line ending to 0d0a
input = function(fpath, content)
return content:gsub("\013\013\010","\013\010")
end,
}
--üäß
for i,filter in pairs(ide.iofilters) do
if filter.input and filter.output then
assert(filter.output("",filter.input("","äöüÄÖÜß")),
"„”äöüÄÖÜß","UTF8-ANSI conversion failed: "..(i))
end
end
-- which: "input" or "output"
function GetConfigIOFilter(which)
local filtername = ide.config.editor.iofilter
return (filtername and ide.iofilters[filtername] and ide.iofilters[filtername][which])
end

View File

@ -0,0 +1,136 @@
local ide = ide
--[[
Accelerator general syntax is any combination of "CTRL", "ALT" and "SHIFT"
strings (case doesn't matter) separated by either '-' or '+' characters and
followed by the accelerator itself. The accelerator may be any alphanumeric
character, any function key (from F1 to F12) or one of the special characters
listed below (again, case doesn't matter):
DEL/DELETE Delete key
INS/INSERT Insert key
ENTER/RETURN Enter key
PGUP PageUp key
PGDN PageDown key
LEFT Left cursor arrow key
RIGHT Right cursor arrow key
UP Up cursor arrow key
DOWN Down cursor arrow key
HOME Home key
END End key
SPACE Space
TAB Tab key
ESC/ESCAPE Escape key (Windows only)
--]]
ide.config.keymap = {
-- File menu
[ID.NEW] = "Ctrl-N",
[ID.OPEN] = "Ctrl-O",
[ID.CLOSE] = "Ctrl-W",
[ID.SAVE] = "Ctrl-S",
[ID.SAVEAS] = "Alt-Shift-S",
[ID.SAVEALL] = "",
[ID.RECENTFILES] = "",
[ID.RECENTFILESPREV] = "Ctrl-,",
[ID.RECENTFILESNEXT] = "Ctrl-.",
[ID.EXIT] = "Ctrl-Q",
[ID.RECENTPROJECTSPREV] = "Ctrl-Shift-<",
-- Edit menu
[ID.CUT] = "Ctrl-X",
[ID.COPY] = "Ctrl-C",
[ID.PASTE] = "Ctrl-V",
[ID.SELECTALL] = "Ctrl-A",
[ID.UNDO] = "Ctrl-Z",
[ID.REDO] = "Ctrl-Y",
[ID.SHOWTOOLTIP] = "Ctrl-T",
[ID.AUTOCOMPLETE] = "Ctrl-K",
[ID.AUTOCOMPLETEENABLE] = "",
[ID.COMMENT] = "Ctrl-U",
[ID.FOLD] = "F12",
[ID.CLEARDYNAMICWORDS] = "",
[ID.REINDENT] = "Ctrl-I",
[ID.BOOKMARKTOGGLE] = "Ctrl-F2",
[ID.BOOKMARKNEXT] = "F2",
[ID.BOOKMARKPREV] = "Shift-F2",
[ID.NAVIGATETOFILE] = "Ctrl-P",
[ID.NAVIGATETOLINE] = "Ctrl-G",
[ID.NAVIGATETOSYMBOL] = "Ctrl-B",
[ID.NAVIGATETOMETHOD] = "Ctrl-;",
-- Search menu
[ID.FIND] = "Ctrl-F",
[ID.FINDNEXT] = "F3",
[ID.FINDPREV] = "Shift-F3",
[ID.FINDSELECTNEXT] = "Ctrl-F3",
[ID.FINDSELECTPREV] = "Ctrl-Shift-F3",
-- [ID.REPLACE] = "Ctrl-R",
[ID.FINDINFILES] = "Ctrl-Shift-F",
[ID.REPLACEINFILES] = "Ctrl-Shift-R",
[ID.SORT] = "",
-- View menu
[ID.VIEWFILETREE] = "Ctrl-Shift-P",
[ID.VIEWOUTPUT] = "Ctrl-Shift-O",
[ID.VIEWWATCHWINDOW] = "Ctrl-Shift-W",
[ID.VIEWCALLSTACK] = "Ctrl-Shift-S",
[ID.VIEWDEFAULTLAYOUT] = "",
[ID.VIEWFULLSCREEN] = "Ctrl-Shift-A",
[ID.ZOOMRESET] = "Ctrl-0",
[ID.ZOOMIN] = "Ctrl-+",
[ID.ZOOMOUT] = "Ctrl--",
-- Project menu
[ID.RUN] = "F6",
[ID.RUNNOW] = "Ctrl-F6",
[ID.COMPILE] = "F7",
[ID.ANALYZE] = "Shift-F7",
[ID.STARTDEBUG] = "F5",
[ID.ATTACHDEBUG] = "",
[ID.DETACHDEBUG] = "",
[ID.STOPDEBUG] = "Shift-F5",
[ID.STEP] = "F10",
[ID.STEPOVER] = "Shift-F10",
[ID.STEPOUT] = "Ctrl-F10",
[ID.RUNTO] = "Ctrl-Shift-F10",
[ID.TRACE] = "",
[ID.BREAK] = "",
[ID.BREAKPOINTTOGGLE] = "Ctrl-F9",
[ID.BREAKPOINTNEXT] = "F9",
[ID.BREAKPOINTPREV] = "Shift-F9",
[ID.CLEAROUTPUT] = "",
[ID.INTERPRETER] = "",
[ID.PROJECTDIR] = "",
-- Help menu
[ID.ABOUT] = "F1",
-- Watch window menu items
[ID.ADDWATCH] = "Ins",
[ID.EDITWATCH] = "F2",
[ID.DELETEWATCH] = "Del",
-- Editor popup menu items
[ID.QUICKADDWATCH] = "",
[ID.QUICKEVAL] = "",
-- Filetree popup menu items
[ID.RENAMEFILE] = "F2",
[ID.DELETEFILE] = "Del",
}
function KSC(id, default)
-- this is only for the rare case of someone assigning a complete list
-- to ide.config.keymap.
local keymap = ide.config.keymap
return (keymap[id] and "\t"..keymap[id]) or (default and "\t"..default) or ""
end
ide.config.editor.keymap = {
-- key, modifier, command, os: http://www.scintilla.org/ScintillaDoc.html#KeyboardCommands
-- Cmd+Left/Right moves to start/end of line
{wxstc.wxSTC_KEY_LEFT, wxstc.wxSTC_SCMOD_CTRL, wxstc.wxSTC_CMD_HOME, "Macintosh"},
{wxstc.wxSTC_KEY_RIGHT, wxstc.wxSTC_SCMOD_CTRL, wxstc.wxSTC_CMD_LINEEND, "Macintosh"},
-- Cmd+Shift+Left/Right selects to the beginning/end of the line
{wxstc.wxSTC_KEY_LEFT, wxstc.wxSTC_SCMOD_CTRL+wxstc.wxSTC_SCMOD_SHIFT, wxstc.wxSTC_CMD_HOMEEXTEND, "Macintosh"},
{wxstc.wxSTC_KEY_RIGHT, wxstc.wxSTC_SCMOD_CTRL+wxstc.wxSTC_SCMOD_SHIFT, wxstc.wxSTC_CMD_LINEENDEXTEND, "Macintosh"},
-- Cmd+Shift+Up/Down selects to the beginning/end of the text
{wxstc.wxSTC_KEY_UP, wxstc.wxSTC_SCMOD_CTRL+wxstc.wxSTC_SCMOD_SHIFT, wxstc.wxSTC_CMD_LINEUPEXTEND, "Macintosh"},
{wxstc.wxSTC_KEY_DOWN, wxstc.wxSTC_SCMOD_CTRL+wxstc.wxSTC_SCMOD_SHIFT, wxstc.wxSTC_CMD_LINEDOWNEXTEND, "Macintosh"},
-- Opt+Left/Right moves one word left (to the beginning)/right (to the end)
{wxstc.wxSTC_KEY_LEFT, wxstc.wxSTC_SCMOD_ALT, wxstc.wxSTC_CMD_WORDLEFT, "Macintosh"},
{wxstc.wxSTC_KEY_RIGHT, wxstc.wxSTC_SCMOD_ALT, wxstc.wxSTC_CMD_WORDRIGHTEND, "Macintosh"},
}

View File

@ -0,0 +1,247 @@
-- Copyright 2015 Paul Kulchenko, ZeroBrane LLC
local ide = ide
ide.markers = {
markersCtrl = nil,
imglist = ide:CreateImageList("MARKERS", "FILE-NORMAL", "DEBUG-BREAKPOINT-TOGGLE", "BOOKMARK-TOGGLE"),
needrefresh = {},
settings = {markers = {}},
}
local unpack = table.unpack or unpack
local markers = ide.markers
local caches = {}
local image = { FILE = 0, BREAKPOINT = 1, BOOKMARK = 2 }
local markertypes = {breakpoint = 0, bookmark = 0}
local maskall = 0
for markertype in pairs(markertypes) do
markertypes[markertype] = 2^ide:GetMarker(markertype)
maskall = maskall + markertypes[markertype]
end
local function resetMarkersTimer()
if ide.config.markersinactivity then
ide.timers.markers:Start(ide.config.markersinactivity*1000, wx.wxTIMER_ONE_SHOT)
end
end
local function needRefresh(editor)
ide.markers.needrefresh[editor] = true
resetMarkersTimer()
end
local function getMarkers(editor)
local edmarkers = {}
local line = editor:MarkerNext(0, maskall)
while line > -1 do
local markerval = editor:MarkerGet(line)
for markertype, val in pairs(markertypes) do
if bit.band(markerval, val) > 0 then
table.insert(edmarkers, {line, markertype})
end
end
line = editor:MarkerNext(line + 1, maskall)
end
return edmarkers
end
local function markersRefresh()
local ctrl = ide.markers.markersCtrl
local win = ide:GetMainFrame():FindFocus()
ctrl:Freeze()
for editor in pairs(ide.markers.needrefresh) do
local cache = caches[editor]
if cache then
local fileitem = cache.fileitem
if not fileitem then
local filename = ide:GetDocument(editor):GetTabText()
local root = ctrl:GetRootItem()
if not root or not root:IsOk() then return end
fileitem = ctrl:AppendItem(root, filename, image.FILE)
ctrl:SortChildren(root)
cache.fileitem = fileitem
end
-- disabling event handlers is not strictly necessary, but it's expected
-- to fix a crash on Windows that had DeleteChildren in the trace (#442).
ctrl:SetEvtHandlerEnabled(false)
ctrl:DeleteChildren(fileitem)
ctrl:SetEvtHandlerEnabled(true)
for _, edmarker in ipairs(getMarkers(editor)) do
local line, markertype = unpack(edmarker)
local text = ("%d: %s"):format(line+1, FixUTF8(editor:GetLineDyn(line)))
ctrl:AppendItem(fileitem, text:gsub("[\r\n]+$",""), image[markertype:upper()])
end
-- if no markers added, then remove the file from the markers list
ctrl:Expand(fileitem)
if not ctrl:ItemHasChildren(fileitem) then
ctrl:Delete(fileitem)
cache.fileitem = nil
end
end
end
ctrl:Thaw()
if win and win ~= ide:GetMainFrame():FindFocus() then win:SetFocus() end
end
local function item2editor(item_id)
for editor, cache in pairs(caches) do
if cache.fileitem and cache.fileitem:GetValue() == item_id:GetValue() then return editor end
end
end
local function createMarkersWindow()
local width, height = 360, 200
local ctrl = wx.wxTreeCtrl(ide.frame, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxSize(width, height),
wx.wxTR_LINES_AT_ROOT + wx.wxTR_HAS_BUTTONS + wx.wxTR_HIDE_ROOT + wx.wxNO_BORDER)
markers.markersCtrl = ctrl
ide.timers.markers = wx.wxTimer(ctrl)
ctrl:AddRoot("Markers")
ctrl:SetImageList(markers.imglist)
ctrl:SetFont(ide.font.fNormal)
function ctrl:ActivateItem(item_id, toggle)
local itemimage = ctrl:GetItemImage(item_id)
if itemimage == image.FILE then
-- activate editor tab
local editor = item2editor(item_id)
if editor then ide:GetDocument(editor):SetActive() end
else -- clicked on the marker item
local parent = ctrl:GetItemParent(item_id)
if parent:IsOk() and ctrl:GetItemImage(parent) == image.FILE then
local editor = item2editor(parent)
if editor then
local line = tonumber(ctrl:GetItemText(item_id):match("^(%d+):"))
if line then
if toggle then
local _ = (itemimage == image.BOOKMARK and editor:BookmarkToggle(line-1, false)
or itemimage == image.BREAKPOINT and editor:BreakpointToggle(line-1, false))
ctrl:Delete(item_id)
return -- don't activate the editor when the breakpoint is toggled
end
editor:GotoLine(line-1)
end
ide:GetDocument(editor):SetActive()
end
end
end
end
local function activateByPosition(event)
local mask = (wx.wxTREE_HITTEST_ONITEMINDENT + wx.wxTREE_HITTEST_ONITEMLABEL
+ wx.wxTREE_HITTEST_ONITEMICON + wx.wxTREE_HITTEST_ONITEMRIGHT)
local item_id, flags = ctrl:HitTest(event:GetPosition())
if item_id and item_id:IsOk() and bit.band(flags, mask) > 0 then
ctrl:ActivateItem(item_id, bit.band(flags, wx.wxTREE_HITTEST_ONITEMICON) > 0)
else
event:Skip()
end
return true
end
ctrl:Connect(wx.wxEVT_TIMER, function() markersRefresh() end)
ctrl:Connect(wx.wxEVT_LEFT_DOWN, activateByPosition)
ctrl:Connect(wx.wxEVT_LEFT_DCLICK, activateByPosition)
ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED, function(event)
ctrl:ActivateItem(event:GetItem())
end)
ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_MENU,
function (event)
local item_id = event:GetItem()
local ID_BOOKMARKTOGGLE = ID("markers.bookmarktoggle")
local ID_BREAKPOINTTOGGLE = ID("markers.breakpointtoggle")
local menu = wx.wxMenu {
{ ID_BOOKMARKTOGGLE, TR("Toggle Bookmark"), TR("Toggle bookmark") },
{ ID_BREAKPOINTTOGGLE, TR("Toggle Breakpoint"), TR("Toggle breakpoint") },
}
local activate = function() ctrl:ActivateItem(item_id, true) end
menu:Enable(ID_BOOKMARKTOGGLE, ctrl:GetItemImage(item_id) == image.BOOKMARK)
menu:Connect(ID_BOOKMARKTOGGLE, wx.wxEVT_COMMAND_MENU_SELECTED, activate)
menu:Enable(ID_BREAKPOINTTOGGLE, ctrl:GetItemImage(item_id) == image.BREAKPOINT)
menu:Connect(ID_BREAKPOINTTOGGLE, wx.wxEVT_COMMAND_MENU_SELECTED, activate)
PackageEventHandle("onMenuMarkers", menu, ctrl, event)
ctrl:PopupMenu(menu)
end)
local function reconfigure(pane)
pane:TopDockable(false):BottomDockable(false)
:MinSize(150,-1):BestSize(300,-1):FloatingSize(200,300)
end
local layout = ide:GetSetting("/view", "uimgrlayout")
if not layout or not layout:find("markerspanel") then
ide:AddPanelDocked(ide:GetOutputNotebook(), ctrl, "markerspanel", TR("Markers"), reconfigure, false)
else
ide:AddPanel(ctrl, "markerspanel", TR("Markers"), reconfigure)
end
end
createMarkersWindow()
local package = ide:AddPackage('core.markers', {
-- save markers; remove tab from the list
onEditorClose = function(self, editor)
local cache = caches[editor]
if not cache then return end
if cache.fileitem then markers.markersCtrl:Delete(cache.fileitem) end
caches[editor] = nil
end,
-- schedule marker update if the change is for one of the editors with markers
onEditorUpdateUI = function(self, editor, event)
if not caches[editor] then return end
if bit.band(event:GetUpdated(), wxstc.wxSTC_UPDATE_CONTENT) == 0 then return end
needRefresh(editor)
end,
onEditorMarkerUpdate = function(self, editor)
-- if no marker, then all markers in a file need to be refreshed
if not caches[editor] then caches[editor] = {} end
needRefresh(editor)
markers:SaveMarkers(editor)
end,
onEditorSave = function(self, editor) markers:SaveMarkers(editor) end,
onEditorLoad = function(self, editor) markers:LoadMarkers(editor) end,
})
function markers:SaveSettings() package:SetSettings(self.settings) end
function markers:SaveMarkers(editor, force)
-- if the file has the name and has not been modified, save the breakpoints
-- this also works when the file is saved as the modified flag is already set to `false`
local doc = ide:GetDocument(editor)
local filepath = doc:GetFilePath()
if filepath and (force or not doc:IsModified()) then
-- remove it from the list if it has no breakpoints
local edmarkers = getMarkers(editor)
self.settings.markers[filepath] = #edmarkers > 0 and edmarkers or nil
self:SaveSettings()
end
end
function markers:LoadMarkers(editor)
local doc = ide:GetDocument(editor)
local filepath = doc:GetFilePath()
if filepath then
for _, edmarker in ipairs(self.settings.markers[filepath] or {}) do
local line, markertype = unpack(edmarker)
local _ = (markertype == "bookmark" and editor:BookmarkToggle(line, true)
or markertype == "breakpoint" and editor:BreakpointToggle(line, true))
end
end
end
MergeSettings(markers.settings, package:GetSettings())

View File

@ -0,0 +1,214 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- styles for comment markup
---------------------------------------------------------
local MD_MARK_ITAL = '_' -- italic
local MD_MARK_BOLD = '**' -- bold
local MD_MARK_LINK = '[' -- link description start
local MD_MARK_LINZ = ']' -- link description end
local MD_MARK_LINA = '(' -- link URL start
local MD_MARK_LINT = ')' -- link URL end
local MD_MARK_HEAD = '#' -- header
local MD_MARK_CODE = '`' -- code
local MD_MARK_BOXD = '|' -- highlight
local MD_MARK_MARK = ' ' -- separator
local MD_LINK_NEWWINDOW = '+' -- indicator to open a new window for links
local markup = {
[MD_MARK_BOXD] = {st=ide:AddIndicator("markup.boxd", 25), fg={127,0,127}, b=true},
[MD_MARK_CODE] = {st=ide:AddIndicator("markup.code", 26), fg={127,127,127}, fs=10},
[MD_MARK_HEAD] = {st=ide:AddIndicator("markup.head", 27), fn="Lucida Console", b=true},
[MD_MARK_LINK] = {st=ide:AddIndicator("markup.link", 28), u=true, hs={32,32,127}},
[MD_MARK_BOLD] = {st=ide:AddIndicator("markup.bold", 29), b=true},
[MD_MARK_ITAL] = {st=ide:AddIndicator("markup.ital", 30), i=true},
[MD_MARK_MARK] = {st=ide:AddIndicator("markup.mark", 31), v=false},
}
-- allow other editor features to recognize this special markup
function MarkupIsSpecial(style) return style == 31 end
function MarkupIsAny(style) return style >= 25 and style <= 31 end
function MarkupAddStyles(styles)
local comment = styles.comment or {}
for key,value in pairs(markup) do
local style = styles[key] or {}
-- copy all style features by value
for feature in pairs(value) do
style[feature] = style[feature] or value[feature]
end
style.fg = style.fg or comment.fg
style.bg = style.bg or comment.bg
styles[key] = style
end
end
local q = EscapeMagic
local MD_MARK_PTRN = '' -- combination of all markup marks that can start styling
for key in pairs(markup) do
if key ~= MD_MARK_MARK then MD_MARK_PTRN = MD_MARK_PTRN .. q(key) end
end
MarkupAddStyles(ide.config.styles)
function MarkupHotspotClick(pos, editor)
-- check if this is "our" hotspot event
if bit.band(editor:GetStyleAt(pos),31) ~= markup[MD_MARK_LINK].st then
-- not "our" style, so nothing to do for us here
return
end
local line = editor:LineFromPosition(pos)
local tx = editor:GetLineDyn(line)
pos = pos + #MD_MARK_LINK - editor:PositionFromLine(line) -- turn into relative position
-- extract the URL/command on the right side of the separator
local _,_,text = string.find(tx, q(MD_MARK_LINZ).."(%b"..MD_MARK_LINA..MD_MARK_LINT..")", pos)
if text then
text = text:gsub("^"..q(MD_MARK_LINA), ""):gsub(q(MD_MARK_LINT).."$", "")
local doc = ide:GetDocument(editor)
local filepath = doc and doc.filePath or FileTreeGetDir()
local _,_,http = string.find(text, [[^(https?:%S+)$]])
local _,_,command,code = string.find(text, [[^macro:(%w+)%((.*%S)%)$]])
if not command then _,_,command = string.find(text, [[^macro:(%w+)$]]) end
if command == 'shell' then
ShellExecuteCode(code)
elseif command == 'inline' then
ShellExecuteInline(code)
elseif command == 'run' then -- run the current file
ProjectRun()
elseif command == 'debug' then -- debug the current file
ProjectDebug()
elseif http then -- open the URL in a new browser window
wx.wxLaunchDefaultBrowser(http, 0)
elseif filepath then -- only check for saved files
-- check if requested to open in a new window
local newwindow = not doc or string.find(text, MD_LINK_NEWWINDOW, 1, true)
if newwindow then text = string.gsub(text, "^%" .. MD_LINK_NEWWINDOW, "") end
local filename = GetFullPathIfExists(
wx.wxFileName(filepath):GetPath(wx.wxPATH_GET_VOLUME), text)
if filename and
(newwindow or SaveModifiedDialog(editor, true) ~= wx.wxID_CANCEL) then
if not newwindow and ide.osname == 'Macintosh' then editor:GotoPos(0) end
LoadFile(filename,not newwindow and editor or nil,true)
end
end
end
return true
end
local function ismarkup (tx)
local start = 1
local marksep = "[%s!%?%.,;:%(%)]"
while true do
-- find a separator first
local st,_,sep,more = string.find(tx, "(["..MD_MARK_PTRN.."])(.)", start)
if not st then return end
-- check if this is a first character of a multi-character separator
if not markup[sep] then sep = sep .. (more or '') end
local s,e,cap
local qsep = q(sep)
local nonsep = ("[^%s]"):format(qsep)
local nonspace = ("[^%s]"):format(qsep.."%s")
if sep == MD_MARK_HEAD then
-- always search from the start of the line
-- [%w%p] set is needed to avoid continuing this markup to the next line
s,e,cap = string.find(tx,"^("..q(MD_MARK_HEAD)..".+[%w%p])")
elseif sep == MD_MARK_LINK then
-- allow everything based on balanced link separators
s,e,cap = string.find(tx,
"^(%b"..MD_MARK_LINK..MD_MARK_LINZ
.."%b"..MD_MARK_LINA..MD_MARK_LINT..")", st)
elseif markup[sep] then
-- try 2+ characters between separators first
-- if not found, try a single character
s,e,cap = string.find(tx,"^("..qsep..nonspace..nonsep.."-"..nonspace..qsep..")", st)
if not s then s,e,cap = string.find(tx,"^("..qsep..nonspace..qsep..")", st) end
end
if s and -- selected markup is surrounded by spaces or punctuation marks
(s == 1 or tx:sub(s-1, s-1):match(marksep)) and
(e == #tx or tx:sub(e+1, e+1):match(marksep))
then return s,e,cap,sep end
start = st+1
end
end
function MarkupStyle(editor, lines, linee)
local lines = lines or 0
if (lines < 0) then return end
-- if the current spec doesn't have any comments, nothing to style
if not next(editor.spec.iscomment) then return end
-- always style to the end as there may be comments that need re-styling
-- technically, this should be GetLineCount()-1, but we want to style
-- beyond the last line to make sure it is styled correctly
local linec = editor:GetLineCount()
local linee = linee or linec
local linecomment = editor.spec.linecomment
local iscomment = {}
for i,v in pairs(editor.spec.iscomment) do
iscomment[i] = v
end
local es = editor:GetEndStyled()
local needfix = false
for line=lines,linee do
local tx = editor:GetLineDyn(line)
local ls = editor:PositionFromLine(line)
local from = 1
local off = -1
-- doing WrapCount(line) when line == linec (which may be beyond
-- the last line) occasionally crashes the application on OSX.
local wrapped = line < linec and editor:WrapCount(line) or 0
while from do
tx = string.sub(tx,from)
local f,t,w,mark = ismarkup(tx)
if (f) then
local p = ls+f+off
local s = bit.band(editor:GetStyleAt(p), 31)
-- only style comments and only those that are not at the beginning
-- of the file to avoid styling shebang (#!) lines
-- also ignore matches for line comments (as defined in the spec)
if iscomment[s] and p > 0 and mark ~= linecomment then
local smark = #mark
local emark = #mark -- assumes end mark is the same length as start mark
if mark == MD_MARK_HEAD then
-- grab multiple MD_MARK_HEAD if present
local _,_,full = string.find(w,"^("..q(MD_MARK_HEAD).."+)")
smark,emark = #full,0
elseif mark == MD_MARK_LINK then
local lsep = w:find(q(MD_MARK_LINZ)..q(MD_MARK_LINA))
if lsep then emark = #w-lsep+#MD_MARK_LINT end
end
editor:StartStyling(p, 31)
editor:SetStyling(smark, markup[MD_MARK_MARK].st)
editor:SetStyling(t-f+1-smark-emark, markup[mark].st or markup[MD_MARK_MARK].st)
editor:SetStyling(emark, markup[MD_MARK_MARK].st)
end
off = off + t
end
from = t and (t+1)
end
-- has this line changed its wrapping because of invisible styling?
if wrapped > 1 and editor:WrapCount(line) < wrapped then needfix = true end
end
editor:StartStyling(es, 31)
-- if any wrapped lines have changed, then reset WrapMode to fix the drawing
if needfix then
-- this fixes an issue with duplicate lines in Scintilla when
-- invisible styles hide some of the content that would be wrapped.
local wrapmode = editor:GetWrapMode()
if wrapmode ~= wxstc.wxSTC_WRAP_NONE then editor:SetWrapMode(wrapmode) end
-- if some of the lines have folded, this can make not styled lines visible
MarkupStyle(editor, linee+1) -- style to the end in this case
end
end

View File

@ -0,0 +1,390 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
-- ---------------------------------------------------------------------------
-- Create the Edit menu and attach the callback functions
local frame = ide.frame
local menuBar = frame.menuBar
local editMenu = wx.wxMenu {
{ ID_CUT, TR("Cu&t")..KSC(ID_CUT), TR("Cut selected text to clipboard") },
{ ID_COPY, TR("&Copy")..KSC(ID_COPY), TR("Copy selected text to clipboard") },
{ ID_PASTE, TR("&Paste")..KSC(ID_PASTE), TR("Paste text from the clipboard") },
{ ID_SELECTALL, TR("Select &All")..KSC(ID_SELECTALL), TR("Select all text in the editor") },
{ },
{ ID_UNDO, TR("&Undo")..KSC(ID_UNDO), TR("Undo last edit") },
{ ID_REDO, TR("&Redo")..KSC(ID_REDO), TR("Redo last edit undone") },
{ },
{ ID_SHOWTOOLTIP, TR("Show &Tooltip")..KSC(ID_SHOWTOOLTIP), TR("Show tooltip for current position; place cursor after opening bracket of function") },
{ ID_AUTOCOMPLETE, TR("Complete &Identifier")..KSC(ID_AUTOCOMPLETE), TR("Complete the current identifier") },
{ ID_AUTOCOMPLETEENABLE, TR("Auto Complete Identifiers")..KSC(ID_AUTOCOMPLETEENABLE), TR("Auto complete while typing"), wx.wxITEM_CHECK },
{ },
}
editMenu:Append(ID_SOURCE, TR("Source"), wx.wxMenu {
{ ID_COMMENT, TR("C&omment/Uncomment")..KSC(ID_COMMENT), TR("Comment or uncomment current or selected lines") },
{ ID_REINDENT, TR("Correct &Indentation")..KSC(ID_REINDENT), TR("Re-indent selected lines") },
{ ID_FOLD, TR("&Fold/Unfold All")..KSC(ID_FOLD), TR("Fold or unfold all code folds") },
{ ID_SORT, TR("&Sort")..KSC(ID_SORT), TR("Sort selected lines") },
})
editMenu:Append(ID_BOOKMARK, TR("Bookmark"), wx.wxMenu {
{ ID_BOOKMARKTOGGLE, TR("Toggle Bookmark")..KSC(ID_BOOKMARKTOGGLE), TR("Toggle bookmark") },
{ ID_BOOKMARKNEXT, TR("Go To Next Bookmark")..KSC(ID_BOOKMARKNEXT) },
{ ID_BOOKMARKPREV, TR("Go To Previous Bookmark")..KSC(ID_BOOKMARKPREV) },
})
editMenu:AppendSeparator()
editMenu:Append(ID_PREFERENCES, TR("Preferences"), wx.wxMenu {
{ ID_PREFERENCESSYSTEM, TR("Settings: System")..KSC(ID_PREFERENCESSYSTEM) },
{ ID_PREFERENCESUSER, TR("Settings: User")..KSC(ID_PREFERENCESUSER) },
})
menuBar:Append(editMenu, TR("&Edit"))
editMenu:Check(ID_AUTOCOMPLETEENABLE, ide.config.autocomplete)
local function getCtrlWithFocus(edType)
local ctrl = ide:GetMainFrame():FindFocus()
return ctrl and ctrl:GetClassInfo():GetClassName() == edType and ctrl:DynamicCast(edType) or nil
end
local function onUpdateUIEditorInFocus(event)
event:Enable(GetEditorWithFocus(GetEditor()) ~= nil)
end
local function onUpdateUIEditMenu(event)
local menu_id = event:GetId()
local editor = GetEditorWithFocus()
if editor == nil then
local editor = getCtrlWithFocus("wxTextCtrl")
event:Enable(editor and (
menu_id == ID_PASTE and editor:CanPaste() or
menu_id == ID_UNDO and editor:CanUndo() or
menu_id == ID_REDO and editor:CanRedo() or
menu_id == ID_CUT and editor:CanCut() or
menu_id == ID_COPY and editor:CanCopy() or
menu_id == ID_SELECTALL and true
) or false)
return
end
local alwaysOn = {
[ID_SELECTALL] = true,
-- allow Cut and Copy commands as these work on a line if no selection
[ID_COPY] = true, [ID_CUT] = true,
}
local enable =
-- pasting is allowed when the document is not read-only and the selection
-- (if any) has no protected text; since pasting handles protected text,
-- use GetReadOnly() instead of CanPaste()
menu_id == ID_PASTE and (not editor:GetReadOnly()) or
menu_id == ID_UNDO and editor:CanUndo() or
menu_id == ID_REDO and editor:CanRedo() or
alwaysOn[menu_id]
event:Enable(enable)
end
local function onEditMenu(event)
local menu_id = event:GetId()
local editor = GetEditorWithFocus()
if editor == nil then
local editor = getCtrlWithFocus("wxTextCtrl")
if not editor or not (
menu_id == ID_PASTE and editor:Paste() or
menu_id == ID_UNDO and editor:Undo() or
menu_id == ID_REDO and editor:Redo() or
menu_id == ID_CUT and editor:Cut() or
menu_id == ID_COPY and editor:Copy() or
menu_id == ID_SELECTALL and editor:SetSelection(-1, -1) or
true
) then event:Skip() end
return
end
if PackageEventHandle("onEditorAction", editor, event) == false then
return
end
local copytext
if (menu_id == ID_CUT or menu_id == ID_COPY)
and ide.wxver >= "2.9.5" and editor:GetSelections() > 1 then
local main = editor:GetMainSelection()
copytext = editor:GetTextRangeDyn(editor:GetSelectionNStart(main), editor:GetSelectionNEnd(main))
for s = 0, editor:GetSelections()-1 do
if copytext ~= editor:GetTextRangeDyn(editor:GetSelectionNStart(s), editor:GetSelectionNEnd(s)) then
copytext = nil
break
end
end
end
local spos, epos = editor:GetSelectionStart(), editor:GetSelectionEnd()
if menu_id == ID_CUT then
if spos == epos then editor:LineCopy() else editor:CopyDyn() end
if spos == epos then
local line = editor:LineFromPosition(spos)
spos, epos = editor:PositionFromLine(line), editor:PositionFromLine(line+1)
editor:SetSelectionStart(spos)
editor:SetSelectionEnd(epos)
end
if spos ~= epos then editor:ClearAny() end
elseif menu_id == ID_COPY then
if spos == epos then editor:LineCopy() else editor:CopyDyn() end
elseif menu_id == ID_PASTE then
-- first clear the text in case there is any hidden markup
if spos ~= epos then editor:ClearAny() end
editor:PasteDyn()
elseif menu_id == ID_SELECTALL then editor:SelectAll()
elseif menu_id == ID_UNDO then editor:Undo()
elseif menu_id == ID_REDO then editor:Redo()
end
if copytext then editor:CopyText(#copytext, copytext) end
end
for _, event in pairs({ID_CUT, ID_COPY, ID_PASTE, ID_SELECTALL, ID_UNDO, ID_REDO}) do
frame:Connect(event, wx.wxEVT_COMMAND_MENU_SELECTED, onEditMenu)
frame:Connect(event, wx.wxEVT_UPDATE_UI, onUpdateUIEditMenu)
end
for _, event in pairs({
ID_BOOKMARKTOGGLE, ID_BOOKMARKNEXT, ID_BOOKMARKPREV,
ID_AUTOCOMPLETE, ID_SORT, ID_REINDENT, ID_SHOWTOOLTIP,
}) do
frame:Connect(event, wx.wxEVT_UPDATE_UI, onUpdateUIEditorInFocus)
end
frame:Connect(ID_COMMENT, wx.wxEVT_UPDATE_UI,
function(event)
local editor = GetEditorWithFocus(GetEditor())
event:Enable(editor ~= nil
and ide:IsValidProperty(editor, "spec") and editor.spec
and editor.spec.linecomment and true or false)
end)
local function generateConfigMessage(type)
return ([==[--[[--
Use this file to specify **%s** preferences.
Review [examples](+%s) or check [online documentation](%s) for details.
--]]--
]==])
:format(type, MergeFullPath(ide.editorFilename, "../cfg/user-sample.lua"),
"http://studio.zerobrane.com/documentation.html")
end
frame:Connect(ID_PREFERENCESSYSTEM, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
local editor = LoadFile(ide.configs.system)
if editor and editor:GetLength() == 0 then
editor:AddTextDyn(generateConfigMessage("System")) end
end)
frame:Connect(ID_PREFERENCESUSER, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
local editor = LoadFile(ide.configs.user)
if editor and editor:GetLength() == 0 then
editor:AddTextDyn(generateConfigMessage("User")) end
end)
frame:Connect(ID_PREFERENCESUSER, wx.wxEVT_UPDATE_UI,
function (event) event:Enable(ide.configs.user ~= nil) end)
frame:Connect(ID_CLEARDYNAMICWORDS, wx.wxEVT_COMMAND_MENU_SELECTED,
function () DynamicWordsReset() end)
frame:Connect(ID_SHOWTOOLTIP, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
if (editor:CallTipActive()) then
editor:CallTipCancel()
return
end
EditorCallTip(editor, editor:GetCurrentPos())
end)
frame:Connect(ID_AUTOCOMPLETE, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event) EditorAutoComplete(GetEditor()) end)
frame:Connect(ID_AUTOCOMPLETEENABLE, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event) ide.config.autocomplete = event:IsChecked() end)
frame:Connect(ID_COMMENT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
local lc = editor.spec.linecomment
if not lc then return end
-- for multi-line selection, always start the first line at the beginning
local ssel, esel = editor:GetSelectionStart(), editor:GetSelectionEnd()
local sline = editor:LineFromPosition(ssel)
local eline = editor:LineFromPosition(esel)
local sel = ssel ~= esel
local rect = editor:SelectionIsRectangle()
local qlc = lc:gsub(".", "%%%1")
-- figure out how to toggle comments; if there is at least one non-empty
-- line that doesn't start with a comment, need to comment
local comment = false
for line = sline, eline do
local pos = sel and (sline == eline or rect)
and ssel-editor:PositionFromLine(sline)+1 or 1
local text = editor:GetLineDyn(line)
local _, cpos = text:find("^%s*"..qlc, pos)
if not cpos and text:find("%S")
-- ignore last line when the end of selection is at the first position
and (line == sline or line < eline or esel-editor:PositionFromLine(line) > 0) then
comment = true
break
end
end
editor:BeginUndoAction()
-- go last to first as selection positions we captured may be affected
-- by text changes
for line = eline, sline, -1 do
local pos = sel and (sline == eline or rect) and ssel-editor:PositionFromLine(sline)+1 or 1
local text = editor:GetLineDyn(line)
local validline = (line == sline or line < eline or esel-editor:PositionFromLine(line) > 0)
local _, cpos = text:find("^%s*"..qlc, pos)
if not comment and cpos and validline then
editor:DeleteRange(cpos-#lc+editor:PositionFromLine(line), #lc)
elseif comment and text:find("%S") and validline then
editor:SetTargetStart(pos+editor:PositionFromLine(line)-1)
editor:SetTargetEnd(editor:GetTargetStart())
editor:ReplaceTarget(lc)
end
end
editor:EndUndoAction()
end)
local function processSelection(editor, func)
local text = editor:GetSelectedText()
local line = editor:GetCurrentLine()
local posinline = editor:GetCurrentPos() - editor:PositionFromLine(line)
if #text == 0 then
editor:SelectAll()
text = editor:GetSelectedText()
end
local wholeline = text:find("\n$")
local buf = {}
for line in string.gmatch(text..(wholeline and "" or "\n"), "(.-\r?\n)") do
table.insert(buf, line)
end
if #buf > 0 then
if func then func(buf) end
-- add new line at the end if it was there
local newtext = table.concat(buf, ""):gsub("(\r?\n)$", wholeline and "%1" or "")
-- straightforward editor:ReplaceSelection() doesn't work reliably as
-- it sometimes doubles the context when the entire file is selected.
-- this seems like Scintilla issue, so use ReplaceTarget instead.
-- Since this doesn't work with rectangular selection, which
-- ReplaceSelection should handle (after wxwidgets 3.x upgrade), this
-- will need to be revisited when ReplaceSelection is updated.
if newtext ~= text then
editor:BeginUndoAction()
-- if there is at least one marker, then use a different mechanism to preserve them
-- simply saving markers, replacing text, and reapplying markers doesn't work as
-- they get reset after `undo/redo` operations.
local ssel, esel = editor:GetSelectionStart(), editor:GetSelectionEnd()
local sline = editor:LineFromPosition(ssel)
local eline = editor:LineFromPosition(esel)
if #editor:MarkerGetAll(nil, sline, eline) > 0 then
for line = #buf, 1, -1 do
editor:SetTargetStart(line == 1 and ssel or editor:PositionFromLine(sline+line-1))
editor:SetTargetEnd(line == eline-sline+1 and esel or editor:GetLineEndPosition(sline+line-1))
editor:ReplaceTarget((buf[line]:gsub("\r?\n$", "")))
end
else
editor:TargetFromSelection()
editor:ReplaceTarget(newtext)
end
editor:EndUndoAction()
end
end
editor:GotoPosEnforcePolicy(math.min(
editor:PositionFromLine(line)+posinline, editor:GetLineEndPosition(line)))
end
frame:Connect(ID_SORT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event) processSelection(GetEditor(), table.sort) end)
local function reIndent(editor, buf)
local decindent, incindent = editor.spec.isdecindent, editor.spec.isincindent
if not (decindent and incindent) then return end
local edline = editor:LineFromPosition(editor:GetSelectionStart())
local indent = 0
local text = ""
-- find the last non-empty line in the previous block (if any)
for n = edline-1, 1, -1 do
indent = editor:GetLineIndentation(n)
text = editor:GetLineDyn(n)
if text:match("[^\r\n]") then break end
end
local ut = editor:GetUseTabs()
local tw = ut and editor:GetTabWidth() or editor:GetIndent()
local indents = {}
local isstatic = {}
for line = 1, #buf+1 do
local ls = editor:PositionFromLine(edline+line-1)
local style = bit.band(editor:GetStyleAt(ls), 31)
-- don't reformat multi-line comments or strings
isstatic[line] = (editor.spec.iscomment[style]
or editor.spec.isstring[style]
or (MarkupIsAny and MarkupIsAny(style)))
if not isstatic[line] or line == 1 or not isstatic[line-1] then
local closed, blockend = decindent(text)
local opened = incindent(text)
-- ignore impact from initial block endings as they are already indented
if line == 1 then blockend = 0 end
-- this only needs to be done for 2, #buf+1; do it and get out when done
if line > 1 then indents[line-1] = indents[line-1] - tw * closed end
if line > #buf then break end
indent = indent + tw * (opened - blockend)
if indent < 0 then indent = 0 end
end
indents[line] = indent
text = buf[line]
end
for line = 1, #buf do
if not isstatic[line] then
buf[line] = buf[line]:gsub("^[ \t]*",
not buf[line]:match("%S") and ""
or ut and ("\t"):rep(indents[line] / tw) or (" "):rep(indents[line]))
end
end
end
frame:Connect(ID_REINDENT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
processSelection(editor, function(buf) reIndent(editor, buf) end)
end)
frame:Connect(ID_FOLD, wx.wxEVT_UPDATE_UI,
function(event)
local editor = GetEditorWithFocus()
event:Enable(editor and editor:CanFold() or false)
end)
frame:Connect(ID_FOLD, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event) GetEditorWithFocus():FoldSome() end)
local BOOKMARK_MARKER = StylesGetMarker("bookmark")
frame:Connect(ID_BOOKMARKTOGGLE, wx.wxEVT_COMMAND_MENU_SELECTED,
function() GetEditor():BookmarkToggle() end)
frame:Connect(ID_BOOKMARKNEXT, wx.wxEVT_COMMAND_MENU_SELECTED,
function() GetEditor():MarkerGotoNext(BOOKMARK_MARKER) end)
frame:Connect(ID_BOOKMARKPREV, wx.wxEVT_COMMAND_MENU_SELECTED,
function() GetEditor():MarkerGotoPrev(BOOKMARK_MARKER) end)

View File

@ -0,0 +1,262 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local frame = ide.frame
local menuBar = frame.menuBar
local openDocuments = ide.openDocuments
local fileMenu = wx.wxMenu({
{ ID_NEW, TR("&New")..KSC(ID_NEW), TR("Create an empty document") },
{ ID_OPEN, TR("&Open...")..KSC(ID_OPEN), TR("Open an existing document") },
{ ID_CLOSE, TR("&Close Page")..KSC(ID_CLOSE), TR("Close the current editor window") },
{ },
{ ID_SAVE, TR("&Save")..KSC(ID_SAVE), TR("Save the current document") },
{ ID_SAVEAS, TR("Save &As...")..KSC(ID_SAVEAS), TR("Save the current document to a file with a new name") },
{ ID_SAVEALL, TR("Save A&ll")..KSC(ID_SAVEALL), TR("Save all open documents") },
{ },
{ ID_RECENTFILES, TR("Recent Files")..KSC(ID_RECENTFILES), TR("File history") },
{ ID_RECENTPROJECTS, TR("Recent Projects")..KSC(ID_RECENTPROJECTS), TR("Project history") },
{ },
{ ID_EXIT, TR("E&xit")..KSC(ID_EXIT), TR("Exit program") }})
menuBar:Append(fileMenu, TR("&File"))
local filehistorymenu = wx.wxMenu({
{ },
{ ID_RECENTFILESCLEAR, TR("Clear Items")..KSC(ID_RECENTFILESCLEAR), TR("Clear items from this list") },
})
local projecthistorymenu = wx.wxMenu({
{ },
{ ID_RECENTPROJECTSCLEAR, TR("Clear Items")..KSC(ID_RECENTPROJECTSCLEAR), TR("Clear items from this list") },
})
ide:AttachMenu(ID_RECENTFILES, filehistorymenu)
ide:AttachMenu(ID_RECENTPROJECTS, projecthistorymenu)
do -- recent file history
local iscaseinsensitive = wx.wxFileName("A"):SameAs(wx.wxFileName("a"))
local function isSameAs(f1, f2)
return f1 == f2 or iscaseinsensitive and f1:lower() == f2:lower()
end
local filehistory = {[0] = 1}
-- add file to the file history removing duplicates
local function addFileHistory(filename)
-- a new (empty) tab is opened; don't change the history
if not filename then return end
local fn = wx.wxFileName(filename)
if fn:Normalize() then filename = fn:GetFullPath() end
local index = filehistory[0]
-- special case: selecting the current file (or moving through the history)
if filehistory[index] and isSameAs(filename, filehistory[index].filename) then return end
-- something else is selected
-- (1) flip the history from 1 to the current index
for i = 1, math.floor(index/2) do
filehistory[i], filehistory[index-i+1] = filehistory[index-i+1], filehistory[i]
end
-- (2) if the file is in the history, remove it
for i = #filehistory, 1, -1 do
if isSameAs(filename, filehistory[i].filename) then
table.remove(filehistory, i)
end
end
-- (3) add the file to the top and update the index
table.insert(filehistory, 1, {filename=filename})
filehistory[0] = 1
-- (4) remove all entries that are no longer needed
while #filehistory>ide.config.filehistorylength do table.remove(filehistory) end
end
local function remFileHistory(filename)
if not filename then return end
local fn = wx.wxFileName(filename)
if fn:Normalize() then filename = fn:GetFullPath() end
local index = filehistory[0]
-- special case: removing the current file
if filehistory[index] and isSameAs(filename, filehistory[index].filename) then
-- (1) flip the history from 1 to the current index
for i = 1, math.floor(index/2) do
filehistory[i], filehistory[index-i+1] = filehistory[index-i+1], filehistory[i]
end
end
-- (2) if the file is in the history, remove it
for i = #filehistory, 1, -1 do
if isSameAs(filename, filehistory[i].filename) then
table.remove(filehistory, i)
end
end
-- (3) update index
filehistory[0] = 1
end
local updateRecentFiles -- need forward declaration because of recursive refs
local function loadRecent(event)
local id = event:GetId()
local item = filehistorymenu:FindItem(id)
local filename = item:GetLabel()
local index = filehistory[0]
filehistory[0] = (
(index > 1 and id == ID("file.recentfiles."..(index-1)) and index-1) or
(index < #filehistory) and id == ID("file.recentfiles."..(index+1)) and index+1 or
1)
if not LoadFile(filename, nil, true) then
wx.wxMessageBox(
TR("File '%s' no longer exists."):format(filename),
GetIDEString("editormessage"),
wx.wxOK + wx.wxCENTRE, ide.frame)
remFileHistory(filename)
updateRecentFiles(filehistory)
end
end
local items = 0
updateRecentFiles = function (list)
-- protect against recent files menu not being present
if not ide:FindMenuItem(ID_RECENTFILES) then return end
for i=1, #list do
local file = list[i].filename
local id = ID("file.recentfiles."..i)
local label = file..(
i == list[0]-1 and KSC(ID_RECENTFILESNEXT) or
i == list[0]+1 and KSC(ID_RECENTFILESPREV) or
"")
if i <= items then -- this is an existing item; update the label
filehistorymenu:FindItem(id):SetItemLabel(label)
else -- need to add an item
local item = wx.wxMenuItem(filehistorymenu, id, label, "")
filehistorymenu:Insert(i-1, item)
frame:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, loadRecent)
end
end
for i=items, #list+1, -1 do -- delete the rest if the list got shorter
filehistorymenu:Delete(filehistorymenu:FindItemByPosition(i-1))
end
items = #list -- update the number of items for the next refresh
-- enable if there are any recent files
fileMenu:Enable(ID_RECENTFILES, #list > 0)
end
-- public methods
function GetFileHistory() return filehistory end
function SetFileHistory(fh)
filehistory = fh
filehistory[0] = 1
updateRecentFiles(filehistory)
end
function AddToFileHistory(filename)
addFileHistory(filename)
updateRecentFiles(filehistory)
end
function FileRecentListUpdate(menu)
local list = filehistory
for i=#list, 1, -1 do
local id = ID("file.recentfiles."..i)
local label = list[i].filename
local item = wx.wxMenuItem(menu, id, label, "")
menu:Insert(0, item)
ide.frame:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, loadRecent)
end
end
end
frame:Connect(ID_NEW, wx.wxEVT_COMMAND_MENU_SELECTED, function() return NewFile() end)
frame:Connect(ID_OPEN, wx.wxEVT_COMMAND_MENU_SELECTED, OpenFile)
frame:Connect(ID_SAVE, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
local editor = ide.findReplace:CanSave(GetEditorWithFocus()) or GetEditor()
local doc = ide:GetDocument(editor)
SaveFile(editor, doc and doc:GetFilePath() or nil)
end)
frame:Connect(ID_SAVE, wx.wxEVT_UPDATE_UI,
function (event)
local doc = ide:GetDocument(GetEditor())
event:Enable(ide.findReplace:CanSave(GetEditorWithFocus()) and true
or doc and (doc:IsModified() or doc:IsNew()) or false)
end)
frame:Connect(ID_SAVEAS, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
SaveFileAs(GetEditor())
end)
frame:Connect(ID_SAVEAS, wx.wxEVT_UPDATE_UI,
function (event)
event:Enable(GetEditor() ~= nil)
end)
frame:Connect(ID_SAVEALL, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
SaveAll()
end)
frame:Connect(ID_SAVEALL, wx.wxEVT_UPDATE_UI,
function (event)
local atLeastOneModifiedDocument = false
for _, document in pairs(openDocuments) do
if document.isModified or not document.filePath then
atLeastOneModifiedDocument = true
break
end
end
event:Enable(atLeastOneModifiedDocument)
end)
frame:Connect(ID_CLOSE, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditorWithFocus()
local nb = ide:GetOutputNotebook()
local index = editor and nb:GetPageIndex(editor)
if index and ide.findReplace:IsPreview(editor) and index >= 0 then
nb:DeletePage(index) -- close preview tab
else
ClosePage() -- this will find the current editor tab
end
end)
frame:Connect(ID_CLOSE, wx.wxEVT_UPDATE_UI,
function (event)
event:Enable(ide.findReplace:IsPreview(GetEditorWithFocus()) or GetEditor() ~= nil)
end)
frame:Connect(ID_EXIT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
frame:Close() -- this will trigger wxEVT_CLOSE_WINDOW
end)
frame:Connect(ID_RECENTPROJECTSCLEAR, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event) FileTreeProjectListClear() end)
frame:Connect(ID_RECENTFILESCLEAR, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
SetFileHistory({})
local ed = ide:GetEditor()
if ed then AddToFileHistory(ide:GetDocument(ed):GetFilePath()) end
end)
local recentprojects = 0
frame:Connect(ID_RECENTPROJECTS, wx.wxEVT_UPDATE_UI,
function (event)
recentprojects = FileTreeProjectListUpdate(projecthistorymenu, recentprojects)
if not recentprojects then return end
local pos = 1 -- add shortcut for the previous project (if any)
if recentprojects > pos then
local item = projecthistorymenu:FindItemByPosition(pos)
item:SetItemLabel(item:GetItemLabelText()..KSC(ID_RECENTPROJECTSPREV))
end
event:Enable(recentprojects > 0)
end)

View File

@ -0,0 +1,118 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
---------------------------------------------------------
local ide = ide
-- ---------------------------------------------------------------------------
-- Create the Help menu and attach the callback functions
local frame = ide.frame
local menuBar = frame.menuBar
local mobdebug = require "mobdebug"
local product = GetIDEString("help", "zerobranestudio")
local url = "http://zerobrane.com/r/"..product.."-"
local urls = {
[ID_HELPPROJECT] = "main",
[ID_HELPDOCUMENTATION] = "documentation",
[ID_HELPGETTINGSTARTED] = "gettingstarted",
[ID_HELPTUTORIALS] = "tutorials",
[ID_HELPFAQ] = "faq",
[ID_HELPCOMMUNITY] = "community",
}
local helpMenu = wx.wxMenu{
{ ID_ABOUT, TR("&About")..KSC(ID_ABOUT), TR("About %s"):format(GetIDEString("editor")) },
{ ID_HELPPROJECT, TR("&Project Page")..KSC(ID_HELPPROJECT) },
{ ID_HELPDOCUMENTATION, TR("&Documentation")..KSC(ID_HELPDOCUMENTATION) },
{ ID_HELPGETTINGSTARTED, TR("&Getting Started Guide")..KSC(ID_HELPGETTINGSTARTED) },
{ ID_HELPTUTORIALS, TR("&Tutorials")..KSC(ID_HELPTUTORIALS) },
{ ID_HELPFAQ, TR("&Frequently Asked Questions")..KSC(ID_HELPFAQ) },
{ ID_HELPCOMMUNITY, TR("&Community")..KSC(ID_HELPCOMMUNITY) },
}
-- do not translate Help menu on Mac as it won't merge with "standard" menus
menuBar:Append(helpMenu, ide.osname == 'Macintosh' and "&Help" or TR("&Help"))
local function displayAbout(event)
local logo = ide:GetAppName().."/"..GetIDEString("logo")
local logoimg = wx.wxFileName(logo):FileExists() and
([[<tr><td><img src="%s"></td></tr>]]):format(logo) or ""
local page = ([[
<html>
<body text="#777777">
<table border="0" width="100%%">
%s
<tr><td>
<table cellspacing="3" cellpadding="3" width="100%%">
<tr>
<td>
<b>ZeroBrane Studio (%s; MobDebug %s)</b><br>
<b>Copyright &copy; 2011-2015 ZeroBrane LLC</b><br>
Paul Kulchenko<br>
Licensed under the MIT License.
</td>
</tr>
<tr>
<td>
<b>Based on Estrela Editor</b><br>
<b>Copyright &copy; 2008-2011 Luxinia DevTeam</b><br>
Christoph Kubisch, Eike Decker<br>
Licensed under the MIT License.
</td>
<td><img align="right" src="%s/res/estrela.png"></td>
</tr>
<tr>
<td>
<b>Based on wxLua editor</b><br>
<b>Copyright &copy; 2002-2005 Lomtick Software</b><br>
J. Winwood, John Labenski<br>
Licensed under wxWindows Library License, v3.
</td>
</tr>
<tr>
<td>
<b>Built with %s, %s</b>
</td>
</tr>
</table>
</td></tr></table>
</body>
</html>]])
:format(logoimg, ide.VERSION, mobdebug._VERSION, ide:GetAppName(),
wx.wxVERSION_STRING, wxlua.wxLUA_VERSION_STRING)
local dlg = wx.wxDialog(frame, wx.wxID_ANY, TR("About %s"):format(GetIDEString("editor")))
-- this is needed because wxLuaHtmlWindow only seems to take into account
-- the initial size, but not the one set with SetSize using
-- wxlua 2.8.12.2 and wxwidgets 2.9.5+.
local tmp = wx.wxLuaHtmlWindow(dlg, wx.wxID_ANY, wx.wxDefaultPosition, wx.wxSize(450, 260))
tmp:SetPage(page)
local w = tmp:GetInternalRepresentation():GetWidth()
local h = tmp:GetInternalRepresentation():GetHeight()
tmp:Destroy()
local html = wx.wxLuaHtmlWindow(dlg, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxSize(w, h), wx.wxHW_SCROLLBAR_NEVER)
html:SetBorders(0)
html:SetPage(page)
local line = wx.wxStaticLine(dlg, wx.wxID_ANY)
local button = wx.wxButton(dlg, wx.wxID_OK, "OK")
button:SetDefault()
local topsizer = wx.wxBoxSizer(wx.wxVERTICAL)
topsizer:Add(html, 1, wx.wxEXPAND + wx.wxALL, 10)
topsizer:Add(line, 0, wx.wxEXPAND + wx.wxLEFT + wx.wxRIGHT, 10)
topsizer:Add(button, 0, wx.wxALL + wx.wxALIGN_RIGHT, 10)
dlg:SetSizerAndFit(topsizer)
dlg:ShowModal()
dlg:Destroy()
end
frame:Connect(ID_ABOUT, wx.wxEVT_COMMAND_MENU_SELECTED, displayAbout)
for item, page in pairs(urls) do
frame:Connect(item, wx.wxEVT_COMMAND_MENU_SELECTED,
function() wx.wxLaunchDefaultBrowser(url..page, 0) end)
end

View File

@ -0,0 +1,474 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local frame = ide.frame
local menuBar = frame.menuBar
local openDocuments = ide.openDocuments
local debugger = ide.debugger
local bottomnotebook = frame.bottomnotebook
local uimgr = frame.uimgr
------------------------
-- Interpreters and Menu
local debugTab = {
{ ID_RUN, TR("&Run")..KSC(ID_RUN), TR("Execute the current project/file") },
{ ID_RUNNOW, TR("Run As Scratchpad")..KSC(ID_RUNNOW), TR("Execute the current project/file and keep updating the code to see immediate results"), wx.wxITEM_CHECK },
{ ID_COMPILE, TR("&Compile")..KSC(ID_COMPILE), TR("Compile the current file") },
{ ID_STARTDEBUG, TR("Start &Debugging")..KSC(ID_STARTDEBUG), TR("Start or continue debugging") },
{ ID_ATTACHDEBUG, TR("&Start Debugger Server")..KSC(ID_ATTACHDEBUG), TR("Allow external process to start debugging"), wx.wxITEM_CHECK },
{ },
{ ID_STOPDEBUG, TR("S&top Debugging")..KSC(ID_STOPDEBUG), TR("Stop the currently running process") },
{ ID_DETACHDEBUG, TR("Detach &Process")..KSC(ID_DETACHDEBUG), TR("Stop debugging and continue running the process") },
{ ID_STEP, TR("Step &Into")..KSC(ID_STEP), TR("Step into") },
{ ID_STEPOVER, TR("Step &Over")..KSC(ID_STEPOVER), TR("Step over") },
{ ID_STEPOUT, TR("Step O&ut")..KSC(ID_STEPOUT), TR("Step out of the current function") },
{ ID_RUNTO, TR("Run To Cursor")..KSC(ID_RUNTO), TR("Run to cursor") },
{ ID_TRACE, TR("Tr&ace")..KSC(ID_TRACE), TR("Trace execution showing each executed line") },
{ ID_BREAK, TR("&Break")..KSC(ID_BREAK), TR("Break execution at the next executed line of code") },
{ },
{ ID_BREAKPOINT, TR("Breakpoint")..KSC(ID_BREAKPOINT) },
{ },
{ ID_CLEAROUTPUT, TR("C&lear Output Window")..KSC(ID_CLEAROUTPUT), TR("Clear the output window before compiling or debugging"), wx.wxITEM_CHECK },
{ ID_COMMANDLINEPARAMETERS, TR("Command Line Parameters...")..KSC(ID_COMMANDLINEPARAMETERS), TR("Provide command line parameters") },
}
local targetDirMenu = wx.wxMenu{
{ID_PROJECTDIRCHOOSE, TR("Choose...")..KSC(ID_PROJECTDIRCHOOSE), TR("Choose a project directory")},
{ID_PROJECTDIRFROMFILE, TR("Set From Current File")..KSC(ID_PROJECTDIRFROMFILE), TR("Set project directory from current file")},
}
local targetMenu = wx.wxMenu({})
local debugMenu = wx.wxMenu(debugTab)
local debugMenuRun = {
start=TR("Start &Debugging")..KSC(ID_STARTDEBUG), continue=TR("Co&ntinue")..KSC(ID_STARTDEBUG)}
local debugMenuStop = {
debugging=TR("S&top Debugging")..KSC(ID_STOPDEBUG), process=TR("S&top Process")..KSC(ID_STOPDEBUG)}
debugMenu:Append(ID_PROJECTDIR, TR("Project Directory"), targetDirMenu, TR("Set the project directory to be used"))
debugMenu:Append(ID_INTERPRETER, TR("Lua &Interpreter"), targetMenu, TR("Set the interpreter to be used"))
menuBar:Append(debugMenu, TR("&Project"))
ide:AttachMenu(ID_BREAKPOINT, wx.wxMenu {
{ ID_BREAKPOINTTOGGLE, TR("Toggle Breakpoint")..KSC(ID_BREAKPOINTTOGGLE) },
{ ID_BREAKPOINTNEXT, TR("Go To Next Breakpoint")..KSC(ID_BREAKPOINTNEXT) },
{ ID_BREAKPOINTPREV, TR("Go To Previous Breakpoint")..KSC(ID_BREAKPOINTPREV) },
})
local interpreters
local function selectInterpreter(id)
for id in pairs(interpreters) do
menuBar:Check(id, false)
menuBar:Enable(id, true)
end
menuBar:Check(id, true)
menuBar:Enable(id, false)
local changed = ide.interpreter ~= interpreters[id]
if ide.interpreter and changed then
PackageEventHandle("onInterpreterClose", ide.interpreter)
end
if interpreters[id] and changed then
PackageEventHandle("onInterpreterLoad", interpreters[id])
end
ide.interpreter = interpreters[id]
DebuggerShutdown()
ide:SetStatus(ide.interpreter.name or "", 4)
if changed then ReloadLuaAPI() end
end
function ProjectSetInterpreter(name)
local id = IDget("debug.interpreter."..name)
if id and interpreters[id] then
selectInterpreter(id)
else
DisplayOutputLn(("Can't find interpreter '%s'; using the default interpreter instead.")
:format(name))
local id = (
-- interpreter is set and is (still) on the list of known interpreters
IDget("debug.interpreter."..(ide.config.interpreter or ide.config.default.interpreter)) or
-- otherwise use default interpreter
ID("debug.interpreter."..ide.config.default.interpreter)
)
selectInterpreter(id)
end
end
local function evSelectInterpreter(event)
selectInterpreter(event:GetId())
end
function ProjectUpdateInterpreters()
assert(ide.interpreters, "no interpreters defined")
-- delete all existing items (if any)
local items = targetMenu:GetMenuItemCount()
for i = items, 1, -1 do
targetMenu:Delete(targetMenu:FindItemByPosition(i-1))
end
local names = {}
for file in pairs(ide.interpreters) do table.insert(names, file) end
table.sort(names)
interpreters = {}
for _, file in ipairs(names) do
local inter = ide.interpreters[file]
local id = ID("debug.interpreter."..file)
inter.fname = file
interpreters[id] = inter
targetMenu:Append(
wx.wxMenuItem(targetMenu, id, inter.name, inter.description, wx.wxITEM_CHECK))
frame:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, evSelectInterpreter)
end
local id = (
-- interpreter is set and is (still) on the list of known interpreters
IDget("debug.interpreter."
..(ide.interpreter and ide.interpreters[ide.interpreter.fname] and ide.interpreter.fname
or ide.config.interpreter or ide.config.default.interpreter)) or
-- otherwise use default interpreter
ID("debug.interpreter."..ide.config.default.interpreter)
)
selectInterpreter(id)
end
-----------------------------
-- Project directory handling
function ProjectUpdateProjectDir(projdir,skiptree)
-- strip trailing spaces as this may create issues with "path/ " on Windows
projdir = projdir:gsub("%s+$","")
local dir = wx.wxFileName.DirName(FixDir(projdir))
dir:Normalize() -- turn into absolute path if needed
if not wx.wxDirExists(dir:GetFullPath()) then return end
projdir = dir:GetPath(wx.wxPATH_GET_VOLUME) -- no trailing slash
ide.config.path.projectdir = projdir ~= "" and projdir or nil
ide:SetStatus(projdir)
frame:SetTitle(ExpandPlaceholders(ide.config.format.apptitle))
if (not skiptree) then ide.filetree:updateProjectDir(projdir) end
return true
end
local function projChoose(event)
local editor = GetEditor()
local fn = wx.wxFileName(
editor and openDocuments[editor:GetId()].filePath or "")
fn:Normalize() -- want absolute path for dialog
local projectdir = ide:GetProject()
local filePicker = wx.wxDirDialog(frame, TR("Choose a project directory"),
projectdir ~= "" and projectdir or wx.wxGetCwd(), wx.wxDIRP_DIR_MUST_EXIST)
if filePicker:ShowModal(true) == wx.wxID_OK then
return ProjectUpdateProjectDir(filePicker:GetPath())
end
return false
end
frame:Connect(ID_PROJECTDIRCHOOSE, wx.wxEVT_COMMAND_MENU_SELECTED, projChoose)
local function projFromFile(event)
local editor = GetEditor()
if not editor then return end
local id = editor:GetId()
local filepath = openDocuments[id].filePath
if not filepath then return end
local fn = wx.wxFileName(filepath)
fn:Normalize() -- want absolute path for dialog
if ide.interpreter then
ProjectUpdateProjectDir(ide.interpreter:fprojdir(fn)) end
end
frame:Connect(ID_PROJECTDIRFROMFILE, wx.wxEVT_COMMAND_MENU_SELECTED, projFromFile)
frame:Connect(ID_PROJECTDIRFROMFILE, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
event:Enable(editor ~= nil and ide:GetDocument(editor):GetFilePath() ~= nil)
end)
----------------------
-- Interpreter Running
local function getNameToRun(skipcheck)
local editor = GetEditor()
if not editor then return end
-- test compile it before we run it, if successful then ask to save
-- only compile if lua api
if editor.spec.apitype and
editor.spec.apitype == "lua" and
(not skipcheck) and
(not ide.interpreter.skipcompile) and
(not CompileProgram(editor, { reportstats = false })) then
return
end
local doc = ide:GetDocument(editor)
local name = ide:GetProjectStartFile() or doc:GetFilePath()
if not name then doc:SetModified(true) end
if not SaveIfModified(editor) then return end
if ide.config.editor.saveallonrun then SaveAll(true) end
return wx.wxFileName(name or doc:GetFilePath())
end
function ActivateOutput()
if not ide.config.activateoutput then return end
-- show output/errorlog pane
if not uimgr:GetPane(bottomnotebook):IsShown() then
uimgr:GetPane(bottomnotebook):Show(true)
uimgr:Update()
end
-- activate output/errorlog window
local index = bottomnotebook:GetPageIndex(bottomnotebook.errorlog)
if bottomnotebook:GetSelection() ~= index then
bottomnotebook:SetSelection(index)
end
end
local function runInterpreter(wfilename, withdebugger)
ClearOutput()
ActivateOutput()
ClearAllCurrentLineMarkers()
if not wfilename then return end
debugger.pid = ide.interpreter:frun(wfilename, withdebugger)
if debugger.pid then OutputEnableInput() end
return debugger.pid
end
function ProjectRun(skipcheck)
local fname = getNameToRun(skipcheck)
if not fname then return end
return runInterpreter(fname)
end
local debuggers = {
debug = "require('mobdebug').loop('%s',%d)",
scratchpad = "require('mobdebug').scratchpad('%s',%d)"
}
function ProjectDebug(skipcheck, debtype)
if (debugger.server ~= nil) then
if (debugger.scratchpad and debugger.scratchpad.paused) then
debugger.scratchpad.paused = nil
debugger.scratchpad.updated = true
ShellSupportRemote(nil) -- disable remote while Scratchpad running
elseif (not debugger.running) then
debugger.run()
end
else
local debcall = (debuggers[debtype or "debug"]):
format(ide.debugger.hostname, ide.debugger.portnumber)
local fname = getNameToRun(skipcheck)
if not fname then return end
return runInterpreter(fname, debcall) -- this may be pid or nil
end
return true
end
-----------------------
-- Actions
local BREAKPOINT_MARKER = StylesGetMarker("breakpoint")
frame:Connect(ID_BREAKPOINTTOGGLE, wx.wxEVT_COMMAND_MENU_SELECTED,
function() GetEditor():BreakpointToggle() end)
frame:Connect(ID_BREAKPOINTTOGGLE, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditorWithFocus(GetEditor())
event:Enable((ide.interpreter) and (ide.interpreter.hasdebugger) and (editor ~= nil)
and (not debugger.scratchpad))
end)
frame:Connect(ID_BREAKPOINTNEXT, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
local BPNSC = KSC(ID_BREAKPOINTNEXT):gsub("\t","")
if not GetEditor():MarkerGotoNext(BREAKPOINT_MARKER) and BPNSC == "F9" then
local osx = ide.osname == "Macintosh"
DisplayOutputLn(("You used '%s' shortcut that has been changed from toggling a breakpoint to navigating to the next breakpoint in the document.")
:format(BPNSC))
DisplayOutputLn(("To toggle a breakpoint, use '%s' or click in the editor margin.")
:format(KSC(ID_BREAKPOINTTOGGLE):gsub("\t",""):gsub("Ctrl", osx and "Cmd" or "Ctrl")))
end
end)
frame:Connect(ID_BREAKPOINTPREV, wx.wxEVT_COMMAND_MENU_SELECTED,
function() GetEditor():MarkerGotoPrev(BREAKPOINT_MARKER) end)
frame:Connect(ID_BREAKPOINTNEXT, wx.wxEVT_UPDATE_UI,
function (event) event:Enable(GetEditor() ~= nil) end)
frame:Connect(ID_BREAKPOINTPREV, wx.wxEVT_UPDATE_UI,
function (event) event:Enable(GetEditor() ~= nil) end)
frame:Connect(ID_COMPILE, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
ActivateOutput()
CompileProgram(GetEditor(), {
keepoutput = ide:GetLaunchedProcess() ~= nil or ide:GetDebugger():IsConnected()
})
end)
frame:Connect(ID_COMPILE, wx.wxEVT_UPDATE_UI,
function (event) event:Enable(GetEditor() ~= nil) end)
frame:Connect(ID_RUN, wx.wxEVT_COMMAND_MENU_SELECTED, function () ProjectRun() end)
frame:Connect(ID_RUN, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
event:Enable((debugger.server == nil and debugger.pid == nil) and (editor ~= nil))
end)
frame:Connect(ID_RUNNOW, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
if debugger.scratchpad then
DebuggerScratchpadOff()
else
DebuggerScratchpadOn(GetEditor())
end
end)
frame:Connect(ID_RUNNOW, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
-- allow scratchpad if there is no server or (there is a server and it is
-- allowed to turn it into a scratchpad) and we are not debugging anything
event:Enable((ide.interpreter) and (ide.interpreter.hasdebugger) and
(ide.interpreter.scratchextloop ~= nil) and -- nil == no scratchpad support
(editor ~= nil) and ((debugger.server == nil or debugger.scratchable)
and debugger.pid == nil or debugger.scratchpad ~= nil))
local isscratchpad = debugger.scratchpad ~= nil
menuBar:Check(ID_RUNNOW, isscratchpad)
local tool = ide:GetToolBar():FindTool(ID_RUNNOW)
if tool and tool:IsSticky() ~= isscratchpad then
tool:SetSticky(isscratchpad)
ide:GetToolBar():Refresh()
end
end)
frame:Connect(ID_ATTACHDEBUG, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
if event:IsChecked() then
if (ide.interpreter.fattachdebug) then ide.interpreter:fattachdebug() end
else
debugger.listen(false) -- stop listening
end
end)
frame:Connect(ID_ATTACHDEBUG, wx.wxEVT_UPDATE_UI,
function (event)
event:Enable(ide.interpreter and ide.interpreter.fattachdebug and true or false)
ide.frame.menuBar:Check(event:GetId(), debugger.listening and true or false)
end)
frame:Connect(ID_STARTDEBUG, wx.wxEVT_COMMAND_MENU_SELECTED, function () ProjectDebug() end)
frame:Connect(ID_STARTDEBUG, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
event:Enable((ide.interpreter) and (ide.interpreter.hasdebugger) and
((debugger.server == nil and debugger.pid == nil and editor ~= nil) or
(debugger.server ~= nil and not debugger.running)) and
(not debugger.scratchpad or debugger.scratchpad.paused))
local label = (debugger.server ~= nil)
and debugMenuRun.continue or debugMenuRun.start
if debugMenu:GetLabel(ID_STARTDEBUG) ~= label then
debugMenu:SetLabel(ID_STARTDEBUG, label)
end
end)
frame:Connect(ID_STOPDEBUG, wx.wxEVT_COMMAND_MENU_SELECTED,
function () DebuggerShutdown() end)
frame:Connect(ID_STOPDEBUG, wx.wxEVT_UPDATE_UI,
function (event)
event:Enable(debugger.server ~= nil or debugger.pid ~= nil)
local label = (debugger.server == nil and debugger.pid ~= nil)
and debugMenuStop.process or debugMenuStop.debugging
if debugMenu:GetLabel(ID_STOPDEBUG) ~= label then
debugMenu:SetLabel(ID_STOPDEBUG, label)
end
end)
frame:Connect(ID_DETACHDEBUG, wx.wxEVT_COMMAND_MENU_SELECTED,
function () debugger.detach() end)
frame:Connect(ID_DETACHDEBUG, wx.wxEVT_UPDATE_UI,
function (event)
event:Enable((debugger.server ~= nil) and (not debugger.scratchpad))
end)
frame:Connect(ID_RUNTO, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
local editor = GetEditor()
debugger.runto(editor, editor:GetCurrentLine())
end)
frame:Connect(ID_RUNTO, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
event:Enable((debugger.server ~= nil) and (not debugger.running)
and (editor ~= nil) and (not debugger.scratchpad))
end)
frame:Connect(ID_STEP, wx.wxEVT_COMMAND_MENU_SELECTED,
function () debugger.step() end)
frame:Connect(ID_STEP, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
event:Enable((debugger.server ~= nil) and (not debugger.running)
and (editor ~= nil) and (not debugger.scratchpad))
end)
frame:Connect(ID_STEPOVER, wx.wxEVT_COMMAND_MENU_SELECTED,
function () debugger.over() end)
frame:Connect(ID_STEPOVER, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
event:Enable((debugger.server ~= nil) and (not debugger.running)
and (editor ~= nil) and (not debugger.scratchpad))
end)
frame:Connect(ID_STEPOUT, wx.wxEVT_COMMAND_MENU_SELECTED,
function () debugger.out() end)
frame:Connect(ID_STEPOUT, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
event:Enable((debugger.server ~= nil) and (not debugger.running)
and (editor ~= nil) and (not debugger.scratchpad))
end)
frame:Connect(ID_TRACE, wx.wxEVT_COMMAND_MENU_SELECTED,
function () debugger.trace() end)
frame:Connect(ID_TRACE, wx.wxEVT_UPDATE_UI,
function (event)
local editor = GetEditor()
event:Enable((debugger.server ~= nil) and (not debugger.running)
and (editor ~= nil) and (not debugger.scratchpad))
end)
frame:Connect(ID_BREAK, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
if debugger.server then
debugger.breaknow()
if debugger.scratchpad then
debugger.scratchpad.paused = true
ShellSupportRemote(debugger.shell)
end
end
end)
frame:Connect(ID_BREAK, wx.wxEVT_UPDATE_UI,
function (event)
event:Enable(debugger.server ~= nil
and (debugger.running
or (debugger.scratchpad and not debugger.scratchpad.paused)))
end)
frame:Connect(ID_COMMANDLINEPARAMETERS, wx.wxEVT_COMMAND_MENU_SELECTED,
function ()
local params = wx.wxGetTextFromUser(TR("Enter command line parameters (use Cancel to clear)"),
TR("Command line parameters"), ide.config.arg.any or "")
ide.config.arg.any = params and #params > 0 and params or nil
end)
frame:Connect(ID_COMMANDLINEPARAMETERS, wx.wxEVT_UPDATE_UI,
function (event)
event:Enable(ide.interpreter and ide.interpreter.takeparameters and true or false)
end)

View File

@ -0,0 +1,420 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local q = EscapeMagic
local unpack = table.unpack or unpack
local frame = ide.frame
local menuBar = frame.menuBar
local findReplace = ide.findReplace
local findMenu = wx.wxMenu{
{ ID_FIND, TR("&Find")..KSC(ID_FIND), TR("Find text") },
{ ID_FINDNEXT, TR("Find &Next")..KSC(ID_FINDNEXT), TR("Find the next text occurrence") },
{ ID_FINDPREV, TR("Find &Previous")..KSC(ID_FINDPREV), TR("Find the earlier text occurence") },
{ ID_FINDSELECTNEXT, TR("Select And Find Next")..KSC(ID_FINDSELECTNEXT), TR("Select the word under cursor and find its next occurrence") },
{ ID_FINDSELECTPREV, TR("Select And Find Previous")..KSC(ID_FINDSELECTPREV), TR("Select the word under cursor and find its previous occurrence") },
{ ID_REPLACE, TR("&Replace")..KSC(ID_REPLACE), TR("Find and replace text") },
{ },
{ ID_FINDINFILES, TR("Find &In Files")..KSC(ID_FINDINFILES), TR("Find text in files") },
{ ID_REPLACEINFILES, TR("Re&place In Files")..KSC(ID_REPLACEINFILES), TR("Find and replace text in files") },
{ },
}
findMenu:Append(ID_NAVIGATE, TR("Navigate"), wx.wxMenu {
{ ID_NAVIGATETOFILE, TR("Go To File...")..KSC(ID_NAVIGATETOFILE), TR("Go to file") },
{ ID_NAVIGATETOLINE, TR("Go To Line...")..KSC(ID_NAVIGATETOLINE), TR("Go to line") },
{ ID_NAVIGATETOSYMBOL, TR("Go To Symbol...")..KSC(ID_NAVIGATETOSYMBOL), TR("Go to symbol") },
{ ID_NAVIGATETOMETHOD, TR("Insert Library Function...")..KSC(ID_NAVIGATETOMETHOD), TR("Find and insert library function") },
})
menuBar:Append(findMenu, TR("&Search"))
local function onUpdateUISearchMenu(event) event:Enable(GetEditor() ~= nil) end
frame:Connect(ID_FIND, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
findReplace:Show(false)
end)
frame:Connect(ID_FIND, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
frame:Connect(ID_REPLACE, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
findReplace:Show(true)
end)
frame:Connect(ID_REPLACE, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
frame:Connect(ID_FINDINFILES, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
findReplace:Show(false,true)
end)
frame:Connect(ID_REPLACEINFILES, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
findReplace:Show(true,true)
end)
frame:Connect(ID_FINDNEXT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
if editor and ide.wxver >= "2.9.5" and editor:GetSelections() > 1 then
local selection = editor:GetMainSelection() + 1
if selection >= editor:GetSelections() then selection = 0 end
editor:SetMainSelection(selection)
editor:ShowPosEnforcePolicy(editor:GetCurrentPos())
else
if findReplace:SetFind(findReplace:GetFind() or findReplace:GetSelection()) then
findReplace:Find()
else
findReplace:Show(false)
end
end
end)
frame:Connect(ID_FINDNEXT, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
frame:Connect(ID_FINDPREV, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
if editor and ide.wxver >= "2.9.5" and editor:GetSelections() > 1 then
local selection = editor:GetMainSelection() - 1
if selection < 0 then selection = editor:GetSelections() - 1 end
editor:SetMainSelection(selection)
editor:ShowPosEnforcePolicy(editor:GetCurrentPos())
else
if findReplace:SetFind(findReplace:GetFind() or findReplace:GetSelection()) then
findReplace:Find(true) -- search up
else
findReplace:Show(false)
end
end
end)
frame:Connect(ID_FINDPREV, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
-- Select and Find behaves like Find if there is a current selection;
-- if not, it selects a word under cursor (if any) and does find.
frame:Connect(ID_FINDSELECTNEXT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
if findReplace:SetFind(findReplace:GetSelection() or findReplace:GetWordAtCaret()) then
findReplace:Find()
end
end)
frame:Connect(ID_FINDSELECTNEXT, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
frame:Connect(ID_FINDSELECTPREV, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
if findReplace:SetFind(findReplace:GetSelection() or findReplace:GetWordAtCaret()) then
findReplace:Find(true)
end
end)
frame:Connect(ID_FINDSELECTPREV, wx.wxEVT_UPDATE_UI, onUpdateUISearchMenu)
local markername = "commandbar.background"
local mac = ide.osname == 'Macintosh'
local win = ide.osname == 'Windows'
local special = {SYMBOL = '@', LINE = ':', METHOD = ';'}
local tabsep = "\0"
local function name2index(name)
local p = name:find(tabsep)
return p and tonumber(name:sub(p + #tabsep)) or nil
end
local function navigateTo(default, selected)
local styles = ide.config.styles
local marker = ide:AddMarker(markername,
wxstc.wxSTC_MARK_BACKGROUND, styles.text.fg, styles.caretlinebg.bg)
local nb = ide:GetEditorNotebook()
local selection = nb:GetSelection()
local maxitems = ide.config.commandbar.maxitems
local files, preview, origline, functions, methods
local function markLine(ed, toline)
ed:MarkerDefine(ide:GetMarker(markername))
ed:MarkerDeleteAll(marker)
ed:MarkerAdd(toline-1, marker)
-- store the original line if not stored yet
origline = origline or (ed:GetCurrentLine()+1)
ed:EnsureVisibleEnforcePolicy(toline-1)
end
CommandBarShow({
defaultText = default or "",
selectedText = selected or "",
onDone = function(t, enter, text)
if not mac then nb:Freeze() end
-- delete all current line markers if any; restore line position
local ed = ide:GetEditor()
if ed and origline then
ed:MarkerDeleteAll(marker)
-- only restore original line if Escape was used (enter == false)
if enter == false then ed:EnsureVisibleEnforcePolicy(origline-1) end
end
local pindex = preview and nb:GetPageIndex(preview)
if enter then
local fline, sline, tabindex = unpack(t or {})
-- jump to symbol; tabindex has the position of the symbol
if text and text:find(special.SYMBOL) then
if sline and tabindex then
local index = name2index(sline)
local editor = index and nb:GetPage(index):DynamicCast("wxStyledTextCtrl")
if not editor then
local doc = ide:FindDocument(sline)
-- reload the file (including the preview to refresh its symbols in the outline)
editor = LoadFile(sline, (not doc or doc:GetTabIndex() == pindex) and preview or nil)
end
if editor then
if pindex and pindex ~= ide:GetDocument(editor):GetTabIndex() then ClosePage(pindex) end
editor:SetFocus() -- in case the focus is on some other panel
editor:GotoPos(tabindex-1)
editor:EnsureVisibleEnforcePolicy(editor:LineFromPosition(tabindex-1))
end
end
-- insert selected method
elseif text and text:find('^%s*'..special.METHOD) then
if ed then -- clean up text and insert at the current location
local method = sline
local isfunc = methods.desc[method][1]:find(q(method).."%s*%(")
local text = method .. (isfunc and "()" or "")
local pos = ed:GetCurrentPos()
ed:InsertTextDyn(pos, text)
ed:EnsureVisibleEnforcePolicy(ed:LineFromPosition(pos))
ed:GotoPos(pos + #method + (isfunc and 1 or 0))
if isfunc then -- show the tooltip
ide.frame:AddPendingEvent(wx.wxCommandEvent(
wx.wxEVT_COMMAND_MENU_SELECTED, ID_SHOWTOOLTIP))
end
end
-- set line position in the (current) editor if requested
elseif text and text:find(special.LINE..'(%d+)%s*$') then
local toline = tonumber(text:match(special.LINE..'(%d+)'))
if toline and ed then
ed:GotoLine(toline-1)
ed:EnsureVisibleEnforcePolicy(toline-1)
ed:SetFocus() -- in case the focus is on some other panel
end
elseif tabindex then -- switch to existing tab
SetEditorSelection(tabindex)
if pindex and pindex ~= tabindex then ClosePage(pindex) end
-- load a new file (into preview if set)
elseif sline or text then
-- 1. use "text" if Ctrl/Cmd-Enter is used
-- 2. otherwise use currently selected file
-- 3. otherwise use "text"
local file = (wx.wxGetKeyState(wx.WXK_CONTROL) and text) or sline or text
local fullPath = MergeFullPath(ide:GetProject(), file)
local doc = ide:FindDocument(fullPath)
-- if the document is already opened (not in the preview)
-- or can't be opened as a file or folder, then close the preview
if doc and doc.index ~= pindex
or not LoadFile(fullPath, preview or nil) and not ProjectUpdateProjectDir(fullPath) then
if pindex then ClosePage(pindex) end
end
end
elseif enter == nil then -- changed focus
-- do nothing; keep everything as is
else
-- close preview
if pindex then ClosePage(pindex) end
-- restore original selection if canceled
if nb:GetSelection() ~= selection then nb:SetSelection(selection) end
end
preview = nil
if not mac then nb:Thaw() end
end,
onUpdate = function(text)
local lines = {}
local projdir = ide:GetProject()
-- delete all current line markers if any
-- restore the original position if search text is updated
local ed = ide:GetEditor()
if ed and origline then ed:MarkerDeleteAll(marker) end
-- reset cached functions if no symbol search
if text and not text:find(special.SYMBOL) then
functions = nil
if ed and origline then ed:EnsureVisibleEnforcePolicy(origline-1) end
end
-- reset cached methods if no method search
if text and not text:find(special.METHOD) then methods = nil end
if text and text:find(special.SYMBOL) then
local file, symbol = text:match('^(.*)'..special.SYMBOL..'(.*)')
if not functions then
local nums, paths = {}, {}
functions = {pos = {}, src = {}}
local function populateSymbols(path, symbols)
for _, func in ipairs(symbols) do
table.insert(functions, func.name)
nums[func.name] = (nums[func.name] or 0) + 1
local num = nums[func.name]
functions.src[func.name..num] = path
functions.pos[func.name..num] = func.pos
end
end
local currentonly = #file > 0 and ed
local outline = ide:GetOutline()
for _, doc in pairs(currentonly and {ide:GetDocument(ed)} or ide:GetDocuments()) do
local path, editor = doc:GetFilePath(), doc:GetEditor()
if path then paths[path] = true end
populateSymbols(path or doc:GetFileName()..tabsep..doc:GetTabIndex(), outline:GetEditorSymbols(editor))
end
-- now add all other files in the project
if not currentonly and ide.config.commandbar.showallsymbols then
local n = 0
outline:RefreshSymbols(projdir, function(path)
local symbols = outline:GetFileSymbols(path)
if not paths[path] and symbols then populateSymbols(path, symbols) end
if not symbols then n = n + 1 end
end)
if n > 0 then ide:SetStatusFor(TR("Queued %d files to index."):format(n)) end
end
end
local nums = {}
if #symbol > 0 then
local topscore
for _, item in ipairs(CommandBarScoreItems(functions, symbol, maxitems)) do
local func, score = unpack(item)
topscore = topscore or score
nums[func] = (nums[func] or 0) + 1
local num = nums[func]
if score > topscore / 4 and score > 1 then
table.insert(lines, {("%2d %s"):format(score, func),
functions.src[func..num], functions.pos[func..num]})
end
end
else
for n, name in ipairs(functions) do
if n > maxitems then break end
nums[name] = (nums[name] or 0) + 1
local num = nums[name]
lines[n] = {name, functions.src[name..num], functions.pos[name..num]}
end
end
elseif ed and text and text:find('^%s*'..special.METHOD) then
if not methods then
methods = {desc = {}}
local num = 1
if ed.api and ed.api.tip and ed.api.tip.shortfinfoclass then
for libname, lib in pairs(ed.api.tip.shortfinfoclass) do
for method, val in pairs(lib) do
local signature, desc = val:match('(.-)\n(.*)')
local m = libname..'.'..method
desc = desc and desc:gsub("\n", " ") or val
methods[num] = m
methods.desc[m] = {signature or (libname..'.'..method), desc}
num = num + 1
end
end
end
end
local method = text:match(special.METHOD..'(.*)')
if #method > 0 then
local topscore
for _, item in ipairs(CommandBarScoreItems(methods, method, maxitems)) do
local method, score = unpack(item)
topscore = topscore or score
if score > topscore / 4 and score > 1 then
table.insert(lines, { score, method })
end
end
end
elseif text and text:find(special.LINE..'(%d*)%s*$') then
local toline = tonumber(text:match(special.LINE..'(%d+)'))
if toline and ed then markLine(ed, toline) end
elseif text and #text > 0 and projdir and #projdir > 0 then
-- populate the list of files
files = files or FileSysGetRecursive(projdir, true, "*",
{sort = false, path = false, folder = false, skipbinary = true})
local topscore
for _, item in ipairs(CommandBarScoreItems(files, text, maxitems)) do
local file, score = unpack(item)
topscore = topscore or score
if score > topscore / 4 and score > 1 then
table.insert(lines, {
("%2d %s"):format(score, wx.wxFileName(file):GetFullName()),
file,
})
end
end
else
for _, doc in pairs(ide:GetDocuments()) do
lines[doc:GetTabIndex()+1] = {doc:GetFileName(), doc:GetFilePath(), doc:GetTabIndex()}
end
end
return lines
end,
onItem = function(t)
if methods then
local score, method = unpack(t)
return ("%2d %s"):format(score, methods.desc[method][1]), methods.desc[method][2]
else
return unpack(t)
end
end,
onSelection = function(t, text)
local _, file, tabindex = unpack(t)
local pos
if text and text:find(special.SYMBOL) then
pos, tabindex = tabindex, name2index(file)
elseif text and text:find(special.METHOD) then
return
end
if file then file = MergeFullPath(ide:GetProject(), file) end
-- disabling event handlers for the notebook and the editor
-- to minimize changes in the UI when editors are switched
-- or files in the preview are updated.
nb:SetEvtHandlerEnabled(false)
local doc = file and ide:FindDocument(file)
if doc and not tabindex then tabindex = doc:GetTabIndex() end
if tabindex then
local ed = nb:GetPage(tabindex)
ed:SetEvtHandlerEnabled(false)
if nb:GetSelection() ~= tabindex then nb:SetSelection(tabindex) end
ed:SetEvtHandlerEnabled(true)
elseif file then
-- skip binary files with unknown extensions
if #ide:GetKnownExtensions(GetFileExt(file)) > 0
or not IsBinary(FileRead(file, 2048)) then
preview = preview or NewFile()
preview:SetEvtHandlerEnabled(false)
LoadFile(file, preview, true, true)
preview:SetFocus()
-- force refresh since the panel covers the editor on OSX/Linux
-- this fixes the preview window not always redrawn on Linux
if not win then preview:Update() preview:Refresh() end
preview:SetEvtHandlerEnabled(true)
elseif preview then
ClosePage(nb:GetPageIndex(preview))
preview = nil
end
end
nb:SetEvtHandlerEnabled(true)
if text and text:find(special.SYMBOL) then
local ed = ide:GetEditor()
if ed then markLine(ed, ed:LineFromPosition(pos-1)+1) end
end
end,
})
end
frame:Connect(ID_NAVIGATETOFILE, wx.wxEVT_COMMAND_MENU_SELECTED,
function() navigateTo("") end)
frame:Connect(ID_NAVIGATETOLINE, wx.wxEVT_COMMAND_MENU_SELECTED,
function() navigateTo(special.LINE) end)
frame:Connect(ID_NAVIGATETOMETHOD, wx.wxEVT_COMMAND_MENU_SELECTED,
function() navigateTo(special.METHOD) end)
frame:Connect(ID_NAVIGATETOSYMBOL, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
local ed = GetEditor()
navigateTo(special.SYMBOL, ed and ed:ValueFromPosition(ed:GetCurrentPos()))
end)

View File

@ -0,0 +1,111 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local frame = ide.frame
local menuBar = frame.menuBar
local unpack = table.unpack or unpack
--[=[
-- tool definition
-- main entries are optional
tool = {
fnmenu = function(frame,menubar),
-- can be used for init
-- and custom menu
exec = {
-- quick exec action
name = "",
description = "",
fn = function(filename, projectdir),
}
}
]=]
local toolArgs = {}
local cnt = 1
local function name2id(name) return ID("tools.exec."..name) end
do
local maxcnt = 10
local tools = {}
for name,tool in pairs(ide.tools) do
if (tool.exec and tool.exec.name) then
tool.fname = name
table.insert(tools,tool)
end
end
table.sort(tools,function(a,b) return a.exec.name < b.exec.name end)
-- todo config specifc ignore/priority list
for _, tool in ipairs(tools) do
local exec = tool.exec
if (exec and cnt < maxcnt and exec.name and exec.fn and exec.description) then
local id = name2id(tool.fname)
table.insert(toolArgs,{id, TR(exec.name) .. KSC(id), exec.description})
-- flag it
tool._execid = id
cnt = cnt + 1
end
end
end
local function addHandler(menu, id, command, updateui)
menu:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local editor = GetEditor()
if (not editor) then return end
command(ide:GetDocument(editor):GetFilePath(), ide:GetProject())
return true
end)
menu:Connect(id, wx.wxEVT_UPDATE_UI,
updateui or function(event) event:Enable(GetEditor() ~= nil) end)
end
if (cnt > 1) then
-- Build Menu
local toolMenu = wx.wxMenu{
unpack(toolArgs)
}
menuBar:Append(toolMenu, "&Tools")
-- connect auto execs
for _, tool in pairs(ide.tools) do
if tool._execid then addHandler(toolMenu, tool._execid, tool.exec.fn) end
end
end
-- Generate Custom Menus/Init
for _, tool in pairs(ide.tools) do
if tool.fninit then tool.fninit(frame, menuBar) end
end
function ToolsAddTool(name, command, updateui)
local toolMenu = ide:FindTopMenu('&Tools')
if not toolMenu then
local helpMenu, helpindex = ide:FindTopMenu('&Help')
if not helpMenu then helpindex = ide:GetMenuBar():GetMenuCount() end
toolMenu = wx.wxMenu{}
menuBar:Insert(helpindex, toolMenu, "&Tools")
end
local id = name2id(name)
toolMenu:Append(id, name)
addHandler(toolMenu, id, command, updateui)
end
function ToolsRemoveTool(name)
ide:RemoveMenuItem(name2id(name))
local toolMenu, toolindex = ide:FindTopMenu('&Tools')
if toolMenu and toolMenu:GetMenuItemCount() == 0 then
ide:GetMenuBar():Remove(toolindex)
end
end

View File

@ -0,0 +1,113 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local frame = ide.frame
local menuBar = frame.menuBar
local uimgr = frame.uimgr
local viewMenu = wx.wxMenu {
{ ID_VIEWFILETREE, TR("Project/&FileTree Window")..KSC(ID_VIEWFILETREE), TR("View the project/filetree window"), wx.wxITEM_CHECK },
{ ID_VIEWOUTPUT, TR("&Output/Console Window")..KSC(ID_VIEWOUTPUT), TR("View the output/console window"), wx.wxITEM_CHECK },
{ ID_VIEWWATCHWINDOW, TR("&Watch Window")..KSC(ID_VIEWWATCHWINDOW), TR("View the watch window"), wx.wxITEM_CHECK },
{ ID_VIEWCALLSTACK, TR("&Stack Window")..KSC(ID_VIEWCALLSTACK), TR("View the stack window"), wx.wxITEM_CHECK },
{ ID_VIEWOUTLINE, TR("Outline Window")..KSC(ID_VIEWOUTLINE), TR("View the outline window"), wx.wxITEM_CHECK },
{ ID_VIEWMARKERS, TR("Markers Window")..KSC(ID_VIEWMARKERS), TR("View the markers window"), wx.wxITEM_CHECK },
{ },
{ ID_VIEWTOOLBAR, TR("&Tool Bar")..KSC(ID_VIEWTOOLBAR), TR("Show/Hide the toolbar"), wx.wxITEM_CHECK },
{ ID_VIEWSTATUSBAR, TR("&Status Bar")..KSC(ID_VIEWSTATUSBAR), TR("Show/Hide the status bar"), wx.wxITEM_CHECK },
{ },
{ ID_VIEWDEFAULTLAYOUT, TR("&Default Layout")..KSC(ID_VIEWDEFAULTLAYOUT), TR("Reset to default layout") },
{ ID_VIEWFULLSCREEN, TR("Full &Screen")..KSC(ID_VIEWFULLSCREEN), TR("Switch to or from full screen mode") },
}
do -- Add zoom submenu
local zoomMenu = wx.wxMenu{
{ID_ZOOMRESET, TR("Zoom to 100%")..KSC(ID_ZOOMRESET)},
{ID_ZOOMIN, TR("Zoom In")..KSC(ID_ZOOMIN)},
{ID_ZOOMOUT, TR("Zoom Out")..KSC(ID_ZOOMOUT)},
}
frame:Connect(ID_ZOOMRESET, wx.wxEVT_COMMAND_MENU_SELECTED,
function() local editor = GetEditorWithFocus()
if editor then editor:SetZoom(0) end end)
frame:Connect(ID_ZOOMIN, wx.wxEVT_COMMAND_MENU_SELECTED,
function() local editor = GetEditorWithFocus()
if editor then editor:SetZoom(editor:GetZoom()+1) end end)
frame:Connect(ID_ZOOMOUT, wx.wxEVT_COMMAND_MENU_SELECTED,
function() local editor = GetEditorWithFocus()
if editor then editor:SetZoom(editor:GetZoom()-1) end end)
-- only enable if there is an editor
local iseditor = function (event) event:Enable(GetEditorWithFocus() ~= nil) end
for _, id in ipairs({ID_ZOOMRESET, ID_ZOOMIN, ID_ZOOMOUT}) do
frame:Connect(id, wx.wxEVT_UPDATE_UI, iseditor)
end
viewMenu:Append(ID_ZOOM, TR("Zoom"), zoomMenu)
end
menuBar:Append(viewMenu, TR("&View"))
local panels = {
[ID_VIEWOUTPUT] = "bottomnotebook",
[ID_VIEWFILETREE] = "projpanel",
[ID_VIEWWATCHWINDOW] = "watchpanel",
[ID_VIEWCALLSTACK] = "stackpanel",
[ID_VIEWOUTLINE] = "outlinepanel",
[ID_VIEWMARKERS] = "markerspanel",
[ID_VIEWTOOLBAR] = "toolbar",
}
local function togglePanel(event)
local panel = panels[event:GetId()]
local pane = uimgr:GetPane(panel)
local shown = not pane:IsShown()
if not shown then pane:BestSize(pane.window:GetSize()) end
pane:Show(shown)
uimgr:Update()
return shown
end
local function checkPanel(event)
local pane = uimgr:GetPane(panels[event:GetId()])
event:Enable(pane:IsOk()) -- disable if doesn't exist
menuBar:Check(event:GetId(), pane:IsOk() and pane:IsShown())
end
frame:Connect(ID_VIEWDEFAULTLAYOUT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
uimgr:LoadPerspective(uimgr.defaultPerspective, true)
end)
frame:Connect(ID_VIEWMINIMIZE, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event) ide.frame:Iconize(true) end)
frame:Connect(ID_VIEWFULLSCREEN, wx.wxEVT_COMMAND_MENU_SELECTED, function ()
ShowFullScreen(not frame:IsFullScreen())
end)
frame:Connect(ID_VIEWFULLSCREEN, wx.wxEVT_UPDATE_UI,
function (event) event:Enable(GetEditor() ~= nil) end)
frame:Connect(ID_VIEWOUTPUT, wx.wxEVT_COMMAND_MENU_SELECTED, togglePanel)
frame:Connect(ID_VIEWFILETREE, wx.wxEVT_COMMAND_MENU_SELECTED, togglePanel)
frame:Connect(ID_VIEWTOOLBAR, wx.wxEVT_COMMAND_MENU_SELECTED, togglePanel)
frame:Connect(ID_VIEWOUTLINE, wx.wxEVT_COMMAND_MENU_SELECTED, togglePanel)
frame:Connect(ID_VIEWMARKERS, wx.wxEVT_COMMAND_MENU_SELECTED, togglePanel)
frame:Connect(ID_VIEWWATCHWINDOW, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event) if togglePanel(event) then DebuggerRefreshPanels() end end)
frame:Connect(ID_VIEWCALLSTACK, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event) if togglePanel(event) then DebuggerRefreshPanels() end end)
frame:Connect(ID_VIEWSTATUSBAR, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
frame:GetStatusBar():Show(menuBar:IsChecked(event:GetId()))
uimgr:Update()
end)
frame:Connect(ID_VIEWSTATUSBAR, wx.wxEVT_UPDATE_UI,
function (event) menuBar:Check(event:GetId(), frame:GetStatusBar():IsShown()) end)
for id in pairs(panels) do frame:Connect(id, wx.wxEVT_UPDATE_UI, checkPanel) end

View File

@ -0,0 +1,679 @@
-- Copyright 2014-15 Paul Kulchenko, ZeroBrane LLC
local ide = ide
ide.outline = {
outlineCtrl = nil,
imglist = ide:CreateImageList("OUTLINE", "FILE-NORMAL", "VALUE-LCALL",
"VALUE-GCALL", "VALUE-ACALL", "VALUE-SCALL", "VALUE-MCALL"),
settings = {
symbols = {},
ignoredirs = {},
},
needsaving = false,
indexqueue = {[0] = {}},
indexpurged = false, -- flag that the index has been purged from old records; once per session
}
local outline = ide.outline
local image = { FILE = 0, LFUNCTION = 1, GFUNCTION = 2, AFUNCTION = 3,
SMETHOD = 4, METHOD = 5,
}
local q = EscapeMagic
local caches = {}
local function setData(ctrl, item, value)
if ide.wxver >= "2.9.5" then
local data = wx.wxLuaTreeItemData()
data:SetData(value)
ctrl:SetItemData(item, data)
end
end
local function resetOutlineTimer()
if ide.config.outlineinactivity then
ide.timers.outline:Start(ide.config.outlineinactivity*1000, wx.wxTIMER_ONE_SHOT)
end
end
local function resetIndexTimer(interval)
if ide.config.symbolindexinactivity and not ide.timers.symbolindex:IsRunning() then
ide.timers.symbolindex:Start(interval or ide.config.symbolindexinactivity*1000, wx.wxTIMER_ONE_SHOT)
end
end
local function outlineRefresh(editor, force)
if not editor then return end
local tokens = editor:GetTokenList()
local sep = editor.spec.sep
local varname = "([%w_][%w_"..q(sep:sub(1,1)).."]*)"
local funcs = {updated = TimeGet()}
local var = {}
local outcfg = ide.config.outline or {}
local scopes = {}
local funcnum = 0
local SCOPENUM, FUNCNUM = 1, 2
local text
for _, token in ipairs(tokens) do
local op = token[1]
if op == 'Var' or op == 'Id' then
var = {name = token.name, fpos = token.fpos, global = token.context[token.name] == nil}
elseif outcfg.showcurrentfunction and op == 'Scope' then
local fundepth = #scopes
if token.name == '(' then -- a function starts a new scope
funcnum = funcnum + 1 -- increment function count
local nested = fundepth == 0 or scopes[fundepth][SCOPENUM] > 0
scopes[fundepth + (nested and 1 or 0)] = {1, funcnum}
elseif fundepth > 0 then
scopes[fundepth][SCOPENUM] = scopes[fundepth][SCOPENUM] + 1
end
elseif outcfg.showcurrentfunction and op == 'EndScope' then
local fundepth = #scopes
if fundepth > 0 and scopes[fundepth][SCOPENUM] > 0 then
scopes[fundepth][SCOPENUM] = scopes[fundepth][SCOPENUM] - 1
if scopes[fundepth][SCOPENUM] == 0 then
local funcnum = scopes[fundepth][FUNCNUM]
if funcs[funcnum] then
funcs[funcnum].poe = token.fpos + (token.name and #token.name or 0)
end
table.remove(scopes)
end
end
elseif op == 'Function' then
local depth = token.context['function'] or 1
local name, pos = token.name, token.fpos
text = text or editor:GetTextDyn()
local _, _, rname, params = text:find('([^%(]*)(%b())', pos)
if name and rname:find(token.name, 1, true) ~= 1 then
name = rname:gsub("%s+$","")
end
if not name then
local s = editor:PositionFromLine(editor:LineFromPosition(pos-1))
local rest
rest, pos, name = text:sub(s+1, pos-1):match('%s*(.-)()'..varname..'%s*=%s*function%s*$')
if rest then
pos = s + pos
-- guard against "foo, bar = function() end" as it would get "bar"
if #rest>0 and rest:find(',') then name = nil end
end
end
local ftype = image.LFUNCTION
if not name then
ftype = image.AFUNCTION
elseif outcfg.showmethodindicator and name:find('['..q(sep)..']') then
ftype = name:find(q(sep:sub(1,1))) and image.SMETHOD or image.METHOD
elseif var.name == name and var.fpos == pos
or var.name and name:find('^'..var.name..'['..q(sep)..']') then
ftype = var.global and image.GFUNCTION or image.LFUNCTION
end
name = name or outcfg.showanonymous
funcs[#funcs+1] = {
name = ((name or '~')..params):gsub("%s+", " "),
skip = (not name) and true or nil,
depth = depth,
image = ftype,
pos = name and pos or token.fpos,
}
end
end
if force == nil then return funcs end
local ctrl = outline.outlineCtrl
local cache = caches[editor] or {}
caches[editor] = cache
-- add file
local filename = ide:GetDocument(editor):GetTabText()
local fileitem = cache.fileitem
if not fileitem then
local root = ctrl:GetRootItem()
if not root or not root:IsOk() then return end
if outcfg.showonefile then
fileitem = root
else
fileitem = ctrl:AppendItem(root, filename, image.FILE)
setData(ctrl, fileitem, editor)
ctrl:SetItemBold(fileitem, true)
ctrl:SortChildren(root)
end
cache.fileitem = fileitem
end
do -- check if any changes in the cached function list
local prevfuncs = cache.funcs or {}
local nochange = #funcs == #prevfuncs
local resort = {} -- items that need to be re-sorted
if nochange then
for n, func in ipairs(funcs) do
func.item = prevfuncs[n].item -- carry over cached items
if func.depth ~= prevfuncs[n].depth then
nochange = false
elseif nochange and prevfuncs[n].item then
if func.name ~= prevfuncs[n].name then
ctrl:SetItemText(prevfuncs[n].item, func.name)
if outcfg.sort then resort[ctrl:GetItemParent(prevfuncs[n].item)] = true end
end
if func.image ~= prevfuncs[n].image then
ctrl:SetItemImage(prevfuncs[n].item, func.image)
end
end
end
end
cache.funcs = funcs -- set new cache as positions may change
if nochange and not force then -- return if no visible changes
if outcfg.sort then -- resort items for all parents that have been modified
for item in pairs(resort) do ctrl:SortChildren(item) end
end
return
end
end
-- refresh the tree
-- refreshing shouldn't change the focus of the current element,
-- but it appears that DeleteChildren (wxwidgets 2.9.5 on Windows)
-- moves the focus from the current element to wxTreeCtrl.
-- need to save the window having focus and restore after the refresh.
local win = ide:GetMainFrame():FindFocus()
ctrl:Freeze()
-- disabling event handlers is not strictly necessary, but it's expected
-- to fix a crash on Windows that had DeleteChildren in the trace (#442).
ctrl:SetEvtHandlerEnabled(false)
ctrl:DeleteChildren(fileitem)
ctrl:SetEvtHandlerEnabled(true)
local edpos = editor:GetCurrentPos()+1
local stack = {fileitem}
local resort = {} -- items that need to be re-sorted
for n, func in ipairs(funcs) do
local depth = outcfg.showflat and 1 or func.depth
local parent = stack[depth]
while not parent do depth = depth - 1; parent = stack[depth] end
if not func.skip then
local item = ctrl:AppendItem(parent, func.name, func.image)
if ide.config.outline.showcurrentfunction
and edpos >= func.pos and func.poe and edpos <= func.poe then
ctrl:SetItemBold(item, true)
end
if outcfg.sort then resort[parent] = true end
setData(ctrl, item, n)
func.item = item
stack[func.depth+1] = item
end
func.skip = nil
end
if outcfg.sort then -- resort items for all parents that have been modified
for item in pairs(resort) do ctrl:SortChildren(item) end
end
if outcfg.showcompact then ctrl:Expand(fileitem) else ctrl:ExpandAllChildren(fileitem) end
-- scroll to the fileitem, but only if it's not a root item (as it's hidden)
if fileitem:GetValue() ~= ctrl:GetRootItem():GetValue() then
ctrl:ScrollTo(fileitem)
ctrl:SetScrollPos(wx.wxHORIZONTAL, 0, true)
else -- otherwise, scroll to the top
ctrl:SetScrollPos(wx.wxVERTICAL, 0, true)
end
ctrl:Thaw()
if win and win ~= ide:GetMainFrame():FindFocus() then win:SetFocus() end
end
local function indexFromQueue()
if #outline.indexqueue == 0 then return end
local editor = ide:GetEditor()
local inactivity = ide.config.symbolindexinactivity
if editor and inactivity and editor.updated > TimeGet()-inactivity then
-- reschedule timer for later time
resetIndexTimer()
else
local fname = table.remove(outline.indexqueue, 1)
outline.indexqueue[0][fname] = nil
-- check if fname is already loaded
ide:SetStatusFor(TR("Indexing %d files: '%s'..."):format(#outline.indexqueue+1, fname))
local content, err = FileRead(fname)
if content then
local editor = ide:CreateBareEditor()
editor:SetupKeywords(GetFileExt(fname))
editor:SetTextDyn(content)
editor:Colourise(0, -1)
editor:ResetTokenList()
while IndicateAll(editor) do end
outline:UpdateSymbols(fname, outlineRefresh(editor))
editor:Destroy()
else
DisplayOutputLn(TR("Can't open file '%s': %s"):format(fname, err))
end
if #outline.indexqueue == 0 then
outline:SaveSettings()
ide:SetStatusFor(TR("Indexing completed."))
end
ide:DoWhenIdle(indexFromQueue)
end
return
end
local function createOutlineWindow()
local REFRESH, REINDEX = 1, 2
local width, height = 360, 200
local ctrl = wx.wxTreeCtrl(ide.frame, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxSize(width, height),
wx.wxTR_LINES_AT_ROOT + wx.wxTR_HAS_BUTTONS
+ wx.wxTR_HIDE_ROOT + wx.wxNO_BORDER)
outline.outlineCtrl = ctrl
ide.timers.outline = wx.wxTimer(ctrl, REFRESH)
ide.timers.symbolindex = wx.wxTimer(ctrl, REINDEX)
ctrl:AddRoot("Outline")
ctrl:SetImageList(outline.imglist)
ctrl:SetFont(ide.font.fNormal)
function ctrl:ActivateItem(item_id)
local data = ctrl:GetItemData(item_id)
if ctrl:GetItemImage(item_id) == image.FILE then
-- activate editor tab
local editor = data:GetData()
if not ide:GetEditorWithFocus(editor) then ide:GetDocument(editor):SetActive() end
else
-- activate tab and move cursor based on stored pos
-- get file parent
local onefile = (ide.config.outline or {}).showonefile
local parent = ctrl:GetItemParent(item_id)
if not onefile then -- find the proper parent
while parent:IsOk() and ctrl:GetItemImage(parent) ~= image.FILE do
parent = ctrl:GetItemParent(parent)
end
if not parent:IsOk() then return end
end
-- activate editor tab
local editor = onefile and GetEditor() or ctrl:GetItemData(parent):GetData()
local cache = caches[editor]
if editor and cache then
-- move to position in the file
editor:GotoPosEnforcePolicy(cache.funcs[data:GetData()].pos-1)
-- only set editor active after positioning as this may change focus,
-- which may regenerate the outline, which may invalidate `data` value
if not ide:GetEditorWithFocus(editor) then ide:GetDocument(editor):SetActive() end
end
end
end
local function activateByPosition(event)
local mask = (wx.wxTREE_HITTEST_ONITEMINDENT + wx.wxTREE_HITTEST_ONITEMLABEL
+ wx.wxTREE_HITTEST_ONITEMICON + wx.wxTREE_HITTEST_ONITEMRIGHT)
local item_id, flags = ctrl:HitTest(event:GetPosition())
if item_id and item_id:IsOk() and bit.band(flags, mask) > 0 then
ctrl:ActivateItem(item_id)
else
event:Skip()
end
return true
end
ctrl:Connect(wx.wxEVT_TIMER, function(event)
if event:GetId() == REFRESH then outlineRefresh(GetEditor(), false) end
if event:GetId() == REINDEX then ide:DoWhenIdle(indexFromQueue) end
end)
ctrl:Connect(wx.wxEVT_LEFT_DOWN, activateByPosition)
ctrl:Connect(wx.wxEVT_LEFT_DCLICK, activateByPosition)
ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED, function(event)
ctrl:ActivateItem(event:GetItem())
end)
ctrl:Connect(ID_OUTLINESORT, wx.wxEVT_COMMAND_MENU_SELECTED,
function()
ide.config.outline.sort = not ide.config.outline.sort
for editor, cache in pairs(caches) do
ide:SetStatus(("Refreshing '%s'..."):format(ide:GetDocument(editor):GetFileName()))
local isexpanded = ctrl:IsExpanded(cache.fileitem)
outlineRefresh(editor, true)
if not isexpanded then ctrl:Collapse(cache.fileitem) end
end
ide:SetStatus('')
end)
ctrl:Connect(wx.wxEVT_COMMAND_TREE_ITEM_MENU,
function (event)
local menu = wx.wxMenu {
{ ID_OUTLINESORT, TR("Sort By Name"), "", wx.wxITEM_CHECK },
}
menu:Check(ID_OUTLINESORT, ide.config.outline.sort)
PackageEventHandle("onMenuOutline", menu, ctrl, event)
ctrl:PopupMenu(menu)
end)
local function reconfigure(pane)
pane:TopDockable(false):BottomDockable(false)
:MinSize(150,-1):BestSize(300,-1):FloatingSize(200,300)
end
local layout = ide:GetSetting("/view", "uimgrlayout")
if not layout or not layout:find("outlinepanel") then
ide:AddPanelDocked(ide:GetProjectNotebook(), ctrl, "outlinepanel", TR("Outline"), reconfigure, false)
else
ide:AddPanel(ctrl, "outlinepanel", TR("Outline"), reconfigure)
end
end
local function eachNode(eachFunc, root, recursive)
local ctrl = outline.outlineCtrl
local item = ctrl:GetFirstChild(root or ctrl:GetRootItem())
while true do
if not item:IsOk() then break end
if eachFunc and eachFunc(ctrl, item) then break end
if recursive and ctrl:ItemHasChildren(item) then eachNode(eachFunc, item, recursive) end
item = ctrl:GetNextSibling(item)
end
end
createOutlineWindow()
local pathsep = GetPathSeparator()
local function isInSubDir(name, path)
return #name > #path and path..pathsep == name:sub(1, #path+#pathsep)
end
local function isIgnoredInIndex(name)
local ignoredirs = outline.settings.ignoredirs
if ignoredirs[name] then return true end
-- check through ignored dirs to see if any of them match the file
for path in pairs(ignoredirs) do
if isInSubDir(name, path) then return true end
end
return false
end
local function purgeIndex(path)
local symbols = outline.settings.symbols
for name in pairs(symbols) do
if isInSubDir(name, path) then outline:UpdateSymbols(name, nil) end
end
end
local function purgeQueue(path)
local curqueue = outline.indexqueue
local newqueue = {[0] = {}}
for _, name in ipairs(curqueue) do
if not isInSubDir(name, path) then
table.insert(newqueue, name)
newqueue[0][name] = true
end
end
outline.indexqueue = newqueue
end
local function disableIndex(path)
outline.settings.ignoredirs[path] = true
outline:SaveSettings(true)
-- purge the path from the index and the (current) queue
purgeIndex(path)
purgeQueue(path)
end
local function enableIndex(path)
outline.settings.ignoredirs[path] = nil
outline:SaveSettings(true)
outline:RefreshSymbols(path)
end
local package = ide:AddPackage('core.outline', {
-- remove the editor from the list
onEditorClose = function(self, editor)
local cache = caches[editor]
local fileitem = cache and cache.fileitem
caches[editor] = nil -- remove from cache
if (ide.config.outline or {}).showonefile then return end
if fileitem then outline.outlineCtrl:Delete(fileitem) end
end,
-- handle rename of the file in the current editor
onEditorSave = function(self, editor)
if (ide.config.outline or {}).showonefile then return end
local cache = caches[editor]
local fileitem = cache and cache.fileitem
local doc = ide:GetDocument(editor)
local ctrl = outline.outlineCtrl
if doc and fileitem and ctrl:GetItemText(fileitem) ~= doc:GetTabText() then
ctrl:SetItemText(fileitem, doc:GetTabText())
end
local path = doc and doc:GetFilePath()
if path and cache and cache.funcs then
outline:UpdateSymbols(path, cache.funcs.updated > editor.updated and cache.funcs or nil)
outline:SaveSettings()
end
end,
-- go over the file items to turn bold on/off or collapse/expand
onEditorFocusSet = function(self, editor)
if (ide.config.outline or {}).showonefile and ide.config.outlineinactivity then
outlineRefresh(editor, true)
return
end
local cache = caches[editor]
local fileitem = cache and cache.fileitem
local ctrl = outline.outlineCtrl
local itemname = ide:GetDocument(editor):GetTabText()
-- update file name if it changed in the editor
if fileitem and ctrl:GetItemText(fileitem) ~= itemname then
ctrl:SetItemText(fileitem, itemname)
end
-- if the editor is not in the cache, which may happen if the user
-- quickly switches between tabs that don't have outline generated,
-- regenerate it manually
if not cache then resetOutlineTimer() end
resetIndexTimer()
eachNode(function(ctrl, item)
local found = fileitem and item:GetValue() == fileitem:GetValue()
if not found and ctrl:IsBold(item) then
ctrl:SetItemBold(item, false)
ctrl:CollapseAllChildren(item)
end
end)
if fileitem and not ctrl:IsBold(fileitem) then
-- run the following changes on idle as doing them inline is causing a strange
-- issue on OSX when clicking on a tab may skip several tabs (#546);
-- this is somehow caused by `ExpandAllChildren` triggered from `SetFocus` inside
-- `PAGE_CHANGED` handler for the notebook.
ide:DoWhenIdle(function()
ctrl:SetItemBold(fileitem, true)
if (ide.config.outline or {}).showcompact then
ctrl:Expand(fileitem)
else
ctrl:ExpandAllChildren(fileitem)
end
ctrl:ScrollTo(fileitem)
ctrl:SetScrollPos(wx.wxHORIZONTAL, 0, true)
end)
end
end,
onMenuFiletree = function(self, menu, tree, event)
local item_id = event:GetItem()
local name = tree:GetItemFullName(item_id)
local symboldirmenu = wx.wxMenu {
{ID_SYMBOLDIRREFRESH, TR("Refresh Index"), TR("Refresh indexed symbols from files in the selected directory")},
{ID_SYMBOLDIRDISABLE, TR("Disable Indexing For '%s'"):format(name), TR("Ignore and don't index symbols from files in the selected directory")},
}
local _, _, projdirpos = ide:FindMenuItem(ID_PROJECTDIR, menu)
if projdirpos then
local ignored = isIgnoredInIndex(name)
local enabledirmenu = wx.wxMenu()
local paths = {}
for path in pairs(outline.settings.ignoredirs) do table.insert(paths, path) end
table.sort(paths)
for i, path in ipairs(paths) do
local id = ID("file.enablesymboldir."..i)
enabledirmenu:Append(id, path, "")
tree:Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED, function() enableIndex(path) end)
end
symboldirmenu:Append(wx.wxMenuItem(symboldirmenu, ID_SYMBOLDIRENABLE,
TR("Enable Indexing"), "", wx.wxITEM_NORMAL, enabledirmenu))
menu:Insert(projdirpos+1, wx.wxMenuItem(menu, ID_SYMBOLDIRINDEX,
TR("Symbol Index"), "", wx.wxITEM_NORMAL, symboldirmenu))
-- disable "enable" if it's empty
menu:Enable(ID_SYMBOLDIRENABLE, #paths > 0)
-- disable "refresh" and "disable" if the directory is ignored
-- or if any of the directories above it are ignored
menu:Enable(ID_SYMBOLDIRREFRESH, tree:IsDirectory(item_id) and not ignored)
menu:Enable(ID_SYMBOLDIRDISABLE, tree:IsDirectory(item_id) and not ignored)
tree:Connect(ID_SYMBOLDIRREFRESH, wx.wxEVT_COMMAND_MENU_SELECTED, function()
-- purge files in this directory as some might have been removed;
-- files will be purged based on time, but this is a good time to clean.
purgeIndex(name)
outline:RefreshSymbols(name)
resetIndexTimer(1) -- start after 1ms
end)
tree:Connect(ID_SYMBOLDIRDISABLE, wx.wxEVT_COMMAND_MENU_SELECTED, function()
disableIndex(name)
end)
end
end,
onEditorPainted = function(self, editor)
local ctrl = ide.outline.outlineCtrl
if not ide:IsWindowShown(ctrl) then return end
local cache = caches[editor]
if not cache or not ide.config.outline.showcurrentfunction then return end
local edpos = editor:GetCurrentPos()+1
local edline = editor:LineFromPosition(edpos-1)+1
if cache.pos and cache.pos == edpos then return end
if cache.line and cache.line == edline then return end
cache.pos = edpos
cache.line = edline
local n = 0
local MIN, MAX = 1, 2
local visible = {[MIN] = math.huge, [MAX] = 0}
local needshown = {[MIN] = math.huge, [MAX] = 0}
ctrl:Unselect()
-- scan all items recursively starting from the current file
eachNode(function(ctrl, item)
local func = cache.funcs[ctrl:GetItemData(item):GetData()]
local val = edpos >= func.pos and func.poe and edpos <= func.poe
if edline == editor:LineFromPosition(func.pos)+1
or (func.poe and edline == editor:LineFromPosition(func.poe)+1) then
cache.line = nil
end
ctrl:SetItemBold(item, val)
if val then ctrl:SelectItem(item, val) end
if not ide.config.outline.jumptocurrentfunction then return end
n = n + 1
-- check that this and the items around it are all visible;
-- this is to avoid the situation when the current item is only partially visible
local isvisible = ctrl:IsVisible(item) and ctrl:GetNextVisible(item):IsOk() and ctrl:GetPrevVisible(item):IsOk()
if val and not isvisible then
needshown[MIN] = math.min(needshown[MIN], n)
needshown[MAX] = math.max(needshown[MAX], n)
elseif isvisible then
visible[MIN] = math.min(visible[MIN], n)
visible[MAX] = math.max(visible[MAX], n)
end
end, cache.fileitem, true)
if not ide.config.outline.jumptocurrentfunction then return end
if needshown[MAX] > visible[MAX] then
ctrl:ScrollLines(needshown[MAX]-visible[MAX]) -- scroll forward to the last hidden line
elseif needshown[MIN] < visible[MIN] then
ctrl:ScrollLines(needshown[MIN]-visible[MIN]) -- scroll backward to the first hidden line
end
end,
})
local function queuePath(path)
-- only queue if symbols inactivity is set, so files will be indexed
if ide.config.symbolindexinactivity and not outline.indexqueue[0][path] then
outline.indexqueue[0][path] = true
table.insert(outline.indexqueue, 1, path)
end
end
function outline:GetFileSymbols(path)
local symbols = self.settings.symbols[path]
-- queue path to process when appropriate
if not symbols then queuePath(path) end
return symbols
end
function outline:GetEditorSymbols(editor)
-- force token refresh (as these may be not updated yet)
if #editor:GetTokenList() == 0 then
while IndicateAll(editor) do end
end
-- only refresh the functions when none is present
if not caches[editor] or #caches[editor].funcs == 0 then outlineRefresh(editor, true) end
return caches[editor].funcs
end
function outline:RefreshSymbols(path, callback)
if isIgnoredInIndex(path) then return end
local exts = {}
for _, ext in pairs(ide:GetKnownExtensions()) do
local spec = GetSpec(ext)
if spec and spec.marksymbols then table.insert(exts, ext) end
end
local opts = {sort = false, folder = false, skipbinary = true, yield = true,
-- skip those directories that are on the "ignore" list
ondirectory = function(name) return outline.settings.ignoredirs[name] == nil end
}
local nextfile = coroutine.wrap(function() FileSysGetRecursive(path, true, table.concat(exts, ";"), opts) end)
while true do
local file = nextfile()
if not file then break end
if not isIgnoredInIndex(file) then (callback or queuePath)(file) end
end
end
function outline:UpdateSymbols(fname, symb)
local symbols = self.settings.symbols
symbols[fname] = symb
-- purge outdated records
local threshold = TimeGet() - 60*60*24*7 -- cache for 7 days
if not self.indexpurged then
for k, v in pairs(symbols) do
if v.updated < threshold then symbols[k] = nil end
end
self.indexpurged = true
end
self.needsaving = true
end
function outline:SaveSettings(force)
if self.needsaving or force then
ide:PushStatus(TR("Updating symbol index and settings..."))
package:SetSettings(self.settings, {keyignore = {depth = true, image = true, poe = true, item = true, skip = true}})
ide:PopStatus()
self.needsaving = false
end
end
MergeSettings(outline.settings, package:GetSettings())

View File

@ -0,0 +1,498 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local frame = ide.frame
local bottomnotebook = frame.bottomnotebook
local errorlog = bottomnotebook.errorlog
-------
-- setup errorlog
local MESSAGE_MARKER = StylesGetMarker("message")
local PROMPT_MARKER = StylesGetMarker("prompt")
local PROMPT_MARKER_VALUE = 2^PROMPT_MARKER
errorlog:Show(true)
errorlog:SetFont(ide.font.oNormal)
errorlog:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.font.oNormal)
errorlog:SetBufferedDraw(not ide.config.hidpi and true or false)
errorlog:StyleClearAll()
errorlog:SetMarginWidth(1, 16) -- marker margin
errorlog:SetMarginType(1, wxstc.wxSTC_MARGIN_SYMBOL)
errorlog:MarkerDefine(StylesGetMarker("message"))
errorlog:MarkerDefine(StylesGetMarker("prompt"))
errorlog:SetReadOnly(true)
if (ide.config.outputshell.usewrap) then
errorlog:SetWrapMode(wxstc.wxSTC_WRAP_WORD)
errorlog:SetWrapStartIndent(0)
errorlog:SetWrapVisualFlags(wxstc.wxSTC_WRAPVISUALFLAG_END)
errorlog:SetWrapVisualFlagsLocation(wxstc.wxSTC_WRAPVISUALFLAGLOC_END_BY_TEXT)
end
StylesApplyToEditor(ide.config.stylesoutshell,errorlog,ide.font.oNormal,ide.font.oItalic)
function ClearOutput(force)
if not (force or ide:GetMenuBar():IsChecked(ID_CLEAROUTPUT)) then return end
errorlog:SetReadOnly(false)
errorlog:ClearAll()
errorlog:SetReadOnly(true)
end
local inputBound = 0 -- to track where partial output ends for input editing purposes
local function getInputLine()
return errorlog:MarkerPrevious(errorlog:GetLineCount()+1, PROMPT_MARKER_VALUE)
end
local function getInputText(bound)
return errorlog:GetTextRangeDyn(
errorlog:PositionFromLine(getInputLine())+(bound or 0), errorlog:GetLength())
end
local function updateInputMarker()
local lastline = errorlog:GetLineCount()-1
errorlog:MarkerDeleteAll(PROMPT_MARKER)
errorlog:MarkerAdd(lastline, PROMPT_MARKER)
inputBound = #getInputText()
end
function OutputEnableInput() updateInputMarker() end
function DisplayOutputNoMarker(...)
local message = ""
local cnt = select('#',...)
for i=1,cnt do
local v = select(i,...)
message = message..tostring(v)..(i<cnt and "\t" or "")
end
local promptLine = getInputLine()
local insertedAt = promptLine == -1 and errorlog:GetLength() or errorlog:PositionFromLine(promptLine) + inputBound
local current = errorlog:GetReadOnly()
errorlog:SetReadOnly(false)
errorlog:InsertTextDyn(insertedAt, errorlog.useraw and message or FixUTF8(message, "\022"))
errorlog:EmptyUndoBuffer()
errorlog:SetReadOnly(current)
errorlog:GotoPos(errorlog:GetLength())
if promptLine ~= -1 then updateInputMarker() end
end
function DisplayOutput(...)
errorlog:MarkerAdd(errorlog:GetLineCount()-1, MESSAGE_MARKER)
DisplayOutputNoMarker(...)
end
function DisplayOutputLn(...)
DisplayOutput(...)
DisplayOutputNoMarker("\n")
end
local streamins = {}
local streamerrs = {}
local streamouts = {}
local customprocs = {}
local textout = '' -- this is a buffer for any text sent to external scripts
function DetachChildProcess()
for _, custom in pairs(customprocs) do
-- since processes are detached, their END_PROCESS event is not going
-- to be called; call endcallback() manually if registered.
if custom.endcallback then custom.endcallback() end
if custom.proc then custom.proc:Detach() end
end
end
function CommandLineRunning(uid)
for pid, custom in pairs(customprocs) do
if (custom.uid == uid and custom.proc and custom.proc.Exists(tonumber(pid))) then
return pid, custom.proc
end
end
return
end
function CommandLineToShell(uid,state)
for pid,custom in pairs(customprocs) do
if (pid == uid or custom.uid == uid) and custom.proc and custom.proc.Exists(tonumber(pid)) then
if (streamins[pid]) then streamins[pid].toshell = state end
if (streamerrs[pid]) then streamerrs[pid].toshell = state end
return true
end
end
end
-- logic to "unhide" wxwidget window using winapi
pcall(require, 'winapi')
local checkstart, checknext, checkperiod
local pid = nil
local function unHideWindow(pidAssign)
-- skip if not configured to do anything
if not ide.config.unhidewindow then return end
if pidAssign then
pid = pidAssign > 0 and pidAssign or nil
end
if pid and winapi then
local now = TimeGet()
if pidAssign and pidAssign > 0 then
checkstart, checknext, checkperiod = now, now, 0.02
end
if now - checkstart > 1 and checkperiod < 0.5 then
checkperiod = checkperiod * 2
end
if now >= checknext then
checknext = now + checkperiod
else
return
end
local wins = winapi.find_all_windows(function(w)
return w:get_process():get_pid() == pid
end)
local any = ide.interpreter.unhideanywindow
local show, hide, ignore = 1, 2, 0
for _,win in pairs(wins) do
-- win:get_class_name() can return nil if the window is already gone
-- between getting the list and this check.
local action = ide.config.unhidewindow[win:get_class_name()]
or (any and show or ignore)
if action == show and not win:is_visible()
or action == hide and win:is_visible() then
-- use show_async call (ShowWindowAsync) to avoid blocking the IDE
-- if the app is busy or is being debugged
win:show_async(action == show and winapi.SW_SHOW or winapi.SW_HIDE)
pid = nil -- indicate that unhiding is done
end
end
end
end
local function nameTab(tab, name)
local index = bottomnotebook:GetPageIndex(tab)
if index ~= -1 then bottomnotebook:SetPageText(index, name) end
end
function OutputSetCallbacks(pid, proc, callback, endcallback)
local streamin = proc and proc:GetInputStream()
local streamerr = proc and proc:GetErrorStream()
if streamin then
streamins[pid] = {stream=streamin, callback=callback,
proc=proc, check=proc and proc.IsInputAvailable}
end
if streamerr then
streamerrs[pid] = {stream=streamerr, callback=callback,
proc=proc, check=proc and proc.IsErrorAvailable}
end
customprocs[pid] = {proc=proc, endcallback=endcallback}
end
function CommandLineRun(cmd,wdir,tooutput,nohide,stringcallback,uid,endcallback)
if (not cmd) then return end
-- expand ~ at the beginning of the command
if ide.oshome and cmd:find('~') then
cmd = cmd:gsub([[^(['"]?)~]], '%1'..ide.oshome:gsub('[\\/]$',''), 1)
end
-- try to extract the name of the executable from the command
-- the executable may not have the extension and may be in quotes
local exename = string.gsub(cmd, "\\", "/")
local _,_,fullname = string.find(exename,'^[\'"]([^\'"]+)[\'"]')
exename = fullname and string.match(fullname,'/?([^/]+)$')
or string.match(exename,'/?([^/]-)%s') or exename
uid = uid or exename
if (CommandLineRunning(uid)) then
DisplayOutputLn(TR("Program can't start because conflicting process is running as '%s'.")
:format(cmd))
return
end
DisplayOutputLn(TR("Program starting as '%s'."):format(cmd))
local proc = wx.wxProcess(errorlog)
if (tooutput) then proc:Redirect() end -- redirect the output if requested
-- set working directory if specified
local oldcwd
if (wdir and #wdir > 0) then -- directory can be empty; ignore in this case
oldcwd = wx.wxFileName.GetCwd()
oldcwd = wx.wxFileName.SetCwd(wdir) and oldcwd
end
-- launch process
local params = wx.wxEXEC_ASYNC + wx.wxEXEC_MAKE_GROUP_LEADER + (nohide and wx.wxEXEC_NOHIDE or 0)
local pid = wx.wxExecute(cmd, params, proc)
if oldcwd then wx.wxFileName.SetCwd(oldcwd) end
-- For asynchronous execution, the return value is the process id and
-- zero value indicates that the command could not be executed.
-- The return value of -1 in this case indicates that we didn't launch
-- a new process, but connected to the running one (e.g. DDE under Windows).
if not pid or pid == -1 or pid == 0 then
DisplayOutputLn(TR("Program unable to run as '%s'."):format(cmd))
return
end
DisplayOutputLn(TR("Program '%s' started in '%s' (pid: %d).")
:format(uid, (wdir and wdir or wx.wxFileName.GetCwd()), pid))
OutputSetCallbacks(pid, proc, stringcallback, endcallback)
customprocs[pid].uid=uid
customprocs[pid].started = TimeGet()
local streamout = proc and proc:GetOutputStream()
if streamout then streamouts[pid] = {stream=streamout, callback=stringcallback, out=true} end
unHideWindow(pid)
nameTab(errorlog, TR("Output (running)"))
return pid
end
local readonce = 4096
local maxread = readonce * 10 -- maximum number of bytes to read before pausing
local function getStreams()
local function readStream(tab)
for _,v in pairs(tab) do
-- periodically stop reading to get a chance to process other events
local processed = 0
while (v.check(v.proc) and processed <= maxread) do
local str = v.stream:Read(readonce)
-- the buffer has readonce bytes, so cut it to the actual size
str = str:sub(1, v.stream:LastRead())
processed = processed + #str
local pfn
if (v.callback) then
str,pfn = v.callback(str)
end
if not str then
-- skip if nothing to display
elseif (v.toshell) then
DisplayShell(str)
else
DisplayOutputNoMarker(str)
if str and (getInputLine() > -1 or errorlog:GetReadOnly()) then
ActivateOutput()
updateInputMarker()
end
end
pfn = pfn and pfn()
end
end
end
local function sendStream(tab)
local str = textout
if not str then return end
textout = nil
str = str .. "\n"
for _,v in pairs(tab) do
local pfn
if (v.callback) then
str,pfn = v.callback(str)
end
v.stream:Write(str, #str)
updateInputMarker()
pfn = pfn and pfn()
end
end
readStream(streamins)
readStream(streamerrs)
sendStream(streamouts)
end
errorlog:Connect(wx.wxEVT_END_PROCESS, function(event)
local pid = event:GetPid()
if (pid ~= -1) then
getStreams()
streamins[pid] = nil
streamerrs[pid] = nil
streamouts[pid] = nil
if not customprocs[pid] then return end
if customprocs[pid].endcallback then customprocs[pid].endcallback() end
-- if this wasn't started with CommandLineRun, skip the rest
if not customprocs[pid].uid then return end
-- delete markers and set focus to the editor if there is an input marker
if errorlog:MarkerPrevious(errorlog:GetLineCount(), PROMPT_MARKER_VALUE) > -1 then
errorlog:MarkerDeleteAll(PROMPT_MARKER)
local editor = GetEditor()
-- check if editor still exists; it may not if the window is closed
if editor then editor:SetFocus() end
end
unHideWindow(0)
DebuggerStop(true)
nameTab(errorlog, TR("Output"))
DisplayOutputLn(TR("Program completed in %.2f seconds (pid: %d).")
:format(TimeGet() - customprocs[pid].started, pid))
customprocs[pid] = nil
end
end)
errorlog:Connect(wx.wxEVT_IDLE, function()
if (#streamins or #streamerrs) then getStreams() end
if ide.osname == 'Windows' then unHideWindow() end
end)
local jumptopatterns = {
-- <filename>(line,linepos):
"^%s*(.-)%((%d+),(%d+)%)%s*:",
-- <filename>(line):
"^%s*(.-)%((%d+).*%)%s*:",
--[string "<filename>"]:line:
'^.-%[string "([^"]+)"%]:(%d+)%s*:',
-- <filename>:line:linepos
"^%s*(.-):(%d+):(%d+):",
-- <filename>:line:
"^%s*(.-):(%d+)%s*:",
}
errorlog:Connect(wxstc.wxEVT_STC_DOUBLECLICK,
function(event)
local line = errorlog:GetCurrentLine()
local linetx = errorlog:GetLineDyn(line)
-- try to detect a filename and line in linetx
local fname, jumpline, jumplinepos
for _,pattern in ipairs(jumptopatterns) do
fname,jumpline,jumplinepos = linetx:match(pattern)
if (fname and jumpline) then break end
end
if not (fname and jumpline) then return end
-- fname may include name of executable, as in "path/to/lua: file.lua";
-- strip it and try to find match again if needed.
-- try the stripped name first as if it doesn't match, the longer
-- name may have parts that may be interpreter as network path and
-- may take few seconds to check.
local name
local fixedname = fname:match(":%s+(.+)")
if fixedname then
name = GetFullPathIfExists(FileTreeGetDir(), fixedname)
or FileTreeFindByPartialName(fixedname)
end
name = name
or GetFullPathIfExists(FileTreeGetDir(), fname)
or FileTreeFindByPartialName(fname)
local editor = LoadFile(name or fname,nil,true)
if not editor then
local ed = GetEditor()
if ed and ide:GetDocument(ed):GetFileName() == (name or fname) then
editor = ed
end
end
if editor then
jumpline = tonumber(jumpline)
jumplinepos = tonumber(jumplinepos)
editor:GotoPos(editor:PositionFromLine(math.max(0,jumpline-1))
+ (jumplinepos and (math.max(0,jumplinepos-1)) or 0))
editor:EnsureVisibleEnforcePolicy(jumpline)
editor:SetFocus()
end
-- doubleclick can set selection, so reset it
local pos = event:GetPosition()
if pos == -1 then pos = errorlog:GetLineEndPosition(event:GetLine()) end
errorlog:SetSelection(pos, pos)
end)
local function positionInLine(line)
return errorlog:GetCurrentPos() - errorlog:PositionFromLine(line)
end
local function caretOnInputLine(disallowLeftmost)
local inputLine = getInputLine()
local boundary = inputBound + (disallowLeftmost and 0 or -1)
return (errorlog:GetCurrentLine() > inputLine
or errorlog:GetCurrentLine() == inputLine
and positionInLine(inputLine) > boundary)
end
errorlog:Connect(wx.wxEVT_KEY_DOWN,
function (event)
-- this loop is only needed to allow to get to the end of function easily
-- "return" aborts the processing and ignores the key
-- "break" aborts the processing and processes the key normally
while true do
-- no special processing if it's readonly
if errorlog:GetReadOnly() then break end
local key = event:GetKeyCode()
if key == wx.WXK_UP or key == wx.WXK_NUMPAD_UP then
if errorlog:GetCurrentLine() > getInputLine() then break
else return end
elseif key == wx.WXK_DOWN or key == wx.WXK_NUMPAD_DOWN then
break -- can go down
elseif key == wx.WXK_LEFT or key == wx.WXK_NUMPAD_LEFT then
if not caretOnInputLine(true) then return end
elseif key == wx.WXK_BACK then
if not caretOnInputLine(true) then return end
elseif key == wx.WXK_DELETE or key == wx.WXK_NUMPAD_DELETE then
if not caretOnInputLine()
or errorlog:LineFromPosition(errorlog:GetSelectionStart()) < getInputLine() then
return
end
elseif key == wx.WXK_PAGEUP or key == wx.WXK_NUMPAD_PAGEUP
or key == wx.WXK_PAGEDOWN or key == wx.WXK_NUMPAD_PAGEDOWN
or key == wx.WXK_END or key == wx.WXK_NUMPAD_END
or key == wx.WXK_HOME or key == wx.WXK_NUMPAD_HOME
or key == wx.WXK_RIGHT or key == wx.WXK_NUMPAD_RIGHT
or key == wx.WXK_SHIFT or key == wx.WXK_CONTROL
or key == wx.WXK_ALT then
break
elseif key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER then
if not caretOnInputLine()
or errorlog:LineFromPosition(errorlog:GetSelectionStart()) < getInputLine() then
return
end
errorlog:GotoPos(errorlog:GetLength()) -- move to the end
textout = (textout or '') .. getInputText(inputBound)
-- remove selection if any, otherwise the text gets replaced
errorlog:SetSelection(errorlog:GetSelectionEnd()+1,errorlog:GetSelectionEnd())
break -- don't need to do anything else with return
else
-- move cursor to end if not already there
if not caretOnInputLine() then
errorlog:GotoPos(errorlog:GetLength())
-- check if the selection starts before the input line and reset it
elseif errorlog:LineFromPosition(errorlog:GetSelectionStart()) < getInputLine(-1) then
errorlog:GotoPos(errorlog:GetLength())
errorlog:SetSelection(errorlog:GetSelectionEnd()+1,errorlog:GetSelectionEnd())
end
end
break
end
event:Skip()
end)
local function inputEditable(line)
local inputLine = getInputLine()
local currentLine = line or errorlog:GetCurrentLine()
return inputLine > -1 and
(currentLine > inputLine or
currentLine == inputLine and positionInLine(inputLine) >= inputBound) and
not (errorlog:LineFromPosition(errorlog:GetSelectionStart()) < getInputLine())
end
errorlog:Connect(wxstc.wxEVT_STC_UPDATEUI,
function () errorlog:SetReadOnly(not inputEditable()) end)
-- only allow copy/move text by dropping to the input line
errorlog:Connect(wxstc.wxEVT_STC_DO_DROP,
function (event)
if not inputEditable(errorlog:LineFromPosition(event:GetPosition())) then
event:SetDragResult(wx.wxDragNone)
end
end)
if ide.config.outputshell.nomousezoom then
-- disable zoom using mouse wheel as it triggers zooming when scrolling
-- on OSX with kinetic scroll and then pressing CMD.
errorlog:Connect(wx.wxEVT_MOUSEWHEEL,
function (event)
if wx.wxGetKeyState(wx.WXK_CONTROL) then return end
event:Skip()
end)
end

View File

@ -0,0 +1,735 @@
-- Copyright 2013-15 Paul Kulchenko, ZeroBrane LLC
---------------------------------------------------------
local ide = ide
local iscaseinsensitive = wx.wxFileName("A"):SameAs(wx.wxFileName("a"))
local unpack = table.unpack or unpack
local q = EscapeMagic
local function eventHandle(handlers, event, ...)
local success
for package, handler in pairs(handlers) do
local ok, res = pcall(handler, package, ...)
if ok then
if res == false then success = false end
else
DisplayOutputLn(TR("%s event failed: %s"):format(event, res))
end
end
return success
end
local function getEventHandlers(packages, event)
local handlers = {}
for _, package in pairs(packages) do
if package[event] then handlers[package] = package[event] end
end
return handlers
end
function PackageEventHandle(event, ...)
return eventHandle(getEventHandlers(ide.packages, event), event, ...)
end
function PackageEventHandleOnce(event, ...)
-- copy packages as the event that is handled only once needs to be removed
local handlers = getEventHandlers(ide.packages, event)
-- remove all handlers as they need to be called only once
-- this allows them to be re-installed if needed
for _, package in pairs(ide.packages) do package[event] = nil end
return eventHandle(handlers, event, ...)
end
local function PackageEventHandleOne(file, event, ...)
local package = ide.packages[file]
if package and type(package[event]) == 'function' then
local ok, res = pcall(package[event], package, ...)
if ok then
if res == false then return false end
else
DisplayOutputLn(TR("%s event failed: %s"):format(event, res))
end
end
end
function PackageUnRegister(file, ...)
PackageEventHandleOne(file, "onUnRegister", ...)
-- remove from the list of installed packages
local package = ide.packages[file]
ide.packages[file] = nil
return package
end
function PackageRegister(file, ...)
if not ide.packages[file] then
local packages = {}
local package = MergeFullPath(
GetPathWithSep(ide.editorFilename), "packages/"..file..".lua")
LoadLuaFileExt(packages, package, ide.proto.Plugin)
packages[file].fname = file
ide.packages[file] = packages[file]
end
return PackageEventHandleOne(file, "onRegister", ...)
end
function ide:GetRootPath(path)
return MergeFullPath(GetPathWithSep(self.editorFilename), path or '')
end
function ide:GetPackagePath(packname)
return MergeFullPath(
self.oshome and MergeFullPath(self.oshome, '.'..self:GetAppName()..'/') or self:GetRootPath(),
MergeFullPath('packages', packname or '')
)
end
function ide:GetApp() return self.editorApp end
function ide:GetAppName() return self.appname end
function ide:GetEditor(index) return GetEditor(index) end
function ide:GetEditorWithFocus(...) return GetEditorWithFocus(...) end
function ide:GetEditorWithLastFocus()
-- make sure ide.infocus is still a valid component and not "some" userdata
return (self:IsValidCtrl(self.infocus)
and self.infocus:GetClassInfo():GetClassName() == "wxStyledTextCtrl"
and self.infocus:DynamicCast("wxStyledTextCtrl") or nil)
end
function ide:GetMenuBar() return self.frame.menuBar end
function ide:GetStatusBar() return self.frame.statusBar end
function ide:GetToolBar() return self.frame.toolBar end
function ide:GetDebugger() return self.debugger end
function ide:GetMainFrame() return self.frame end
function ide:GetUIManager() return self.frame.uimgr end
function ide:GetDocument(ed) return ed and self.openDocuments[ed:GetId()] end
function ide:GetDocuments() return self.openDocuments end
function ide:GetKnownExtensions(ext)
local knownexts, extmatch = {}, ext and ext:lower()
for _, spec in pairs(self.specs) do
for _, ext in ipairs(spec.exts or {}) do
if not extmatch or extmatch == ext:lower() then
table.insert(knownexts, ext)
end
end
end
table.sort(knownexts)
return knownexts
end
function ide:DoWhenIdle(func) table.insert(self.onidle, func) end
function ide:FindTopMenu(item)
local index = self:GetMenuBar():FindMenu((TR)(item))
return self:GetMenuBar():GetMenu(index), index
end
function ide:FindMenuItem(itemid, menu)
local item, imenu = self:GetMenuBar():FindItem(itemid, menu)
if menu and not item then item = menu:FindItem(itemid) end
if not item then return end
menu = menu or imenu
for pos = 0, menu:GetMenuItemCount()-1 do
if menu:FindItemByPosition(pos):GetId() == itemid then
return item, menu, pos
end
end
return
end
function ide:AttachMenu(...)
-- AttachMenu([targetmenu,] id, submenu)
-- `targetmenu` is only needed for menus not attached to the main menubar
local menu, id, submenu = ...
if select('#', ...) == 2 then menu, id, submenu = nil, ... end
local item, menu, pos = self:FindMenuItem(id, menu)
if not item then return end
local menuitem = wx.wxMenuItem(menu, id, item:GetItemLabel(), item:GetHelp(), wx.wxITEM_NORMAL, submenu)
menu:Destroy(item)
return menu:Insert(pos, menuitem), pos
end
function ide:CloneMenu(menu)
if not menu then return end
local newmenu = wx.wxMenu()
local node = menu:GetMenuItems():GetFirst()
while node do
local item = node:GetData():DynamicCast("wxMenuItem")
newmenu:Append(item:GetId(), item:GetItemLabel(), item:GetHelp(), item:GetKind())
node = node:GetNext()
end
return newmenu
end
function ide:FindDocument(path)
local fileName = wx.wxFileName(path)
for _, doc in pairs(self:GetDocuments()) do
if doc.filePath and fileName:SameAs(wx.wxFileName(doc.filePath)) then
return doc
end
end
return
end
function ide:FindDocumentsByPartialPath(path)
local seps = "[\\/]"
-- add trailing path separator to make sure full directory match
if not path:find(seps.."$") then path = path .. GetPathSeparator() end
local pattern = "^"..q(path):gsub(seps, seps)
local lpattern = pattern:lower()
local docs = {}
for _, doc in pairs(self:GetDocuments()) do
if doc.filePath
and (doc.filePath:find(pattern)
or iscaseinsensitive and doc.filePath:lower():find(lpattern)) then
table.insert(docs, doc)
end
end
return docs
end
function ide:GetInterpreter() return self.interpreter end
function ide:GetInterpreters() return self.interpreters end
function ide:GetConfig() return self.config end
function ide:GetOutput() return self.frame.bottomnotebook.errorlog end
function ide:GetConsole() return self.frame.bottomnotebook.shellbox end
function ide:GetEditorNotebook() return self.frame.notebook end
function ide:GetOutputNotebook() return self.frame.bottomnotebook end
function ide:GetOutline() return self.outline end
function ide:GetProjectNotebook() return self.frame.projnotebook end
function ide:GetProject() return FileTreeGetDir() end
function ide:GetProjectStartFile()
local projectdir = FileTreeGetDir()
local startfile = self.filetree.settings.startfile[projectdir]
return MergeFullPath(projectdir, startfile), startfile
end
function ide:GetLaunchedProcess() return self.debugger and self.debugger.pid end
function ide:GetProjectTree() return self.filetree.projtreeCtrl end
function ide:GetOutlineTree() return self.outline.outlineCtrl end
function ide:GetWatch() return self.debugger and self.debugger.watchCtrl end
function ide:GetStack() return self.debugger and self.debugger.stackCtrl end
local statusreset
function ide:SetStatusFor(text, interval, field)
field = field or 0
interval = interval or 2
local statusbar = self:GetStatusBar()
if not ide.timers.status then
ide.timers.status = wx.wxTimer(statusbar)
statusbar:Connect(wx.wxEVT_TIMER, function(event) if statusreset then statusreset() end end)
end
statusreset = function()
if statusbar:GetStatusText(field) == text then statusbar:SetStatusText("", field) end
end
ide.timers.status:Start(interval*1000, wx.wxTIMER_ONE_SHOT)
statusbar:SetStatusText(text, field)
end
function ide:SetStatus(text, field) self:GetStatusBar():SetStatusText(text, field or 0) end
function ide:GetStatus(field) return self:GetStatusBar():GetStatusText(field or 0) end
function ide:PushStatus(text, field) self:GetStatusBar():PushStatusText(text, field or 0) end
function ide:PopStatus(field) self:GetStatusBar():PopStatusText(field or 0) end
function ide:Yield() wx.wxYield() end
function ide:CreateBareEditor() return CreateEditor(true) end
local rawMethods = {"AddTextDyn", "InsertTextDyn", "AppendTextDyn", "SetTextDyn",
"GetTextDyn", "GetLineDyn", "GetSelectedTextDyn", "GetTextRangeDyn"}
local useraw = nil
function ide:CreateStyledTextCtrl(...)
local editor = wxstc.wxStyledTextCtrl(...)
if not editor then return end
if useraw == nil then
useraw = true
for _, m in ipairs(rawMethods) do
if not pcall(function() return editor[m:gsub("Dyn", "Raw")] end) then useraw = false; break end
end
end
-- map all `GetTextDyn` to `GetText` or `GetTextRaw` if `*Raw` methods are present
editor.useraw = useraw
for _, m in ipairs(rawMethods) do
-- some `*Raw` methods return `nil` instead of `""` as their "normal" calls do
-- (for example, `GetLineRaw` and `GetTextRangeRaw` for parameters outside of text)
local def = m:find("^Get") and "" or nil
editor[m] = function(...) return editor[m:gsub("Dyn", useraw and "Raw" or "")](...) or def end
end
local suffix = "\1\0"
function editor:CopyDyn()
if not self.useraw then return self:Copy() end
-- check if selected fragment is a valid UTF-8 sequence
local text = self:GetSelectedTextRaw()
if text == "" or wx.wxString.FromUTF8(text) ~= "" then return self:Copy() end
local tdo = wx.wxTextDataObject()
-- append suffix as wxwidgets (3.1+ on Windows) truncate last char for odd-length strings
local workaround = ide.osname == "Windows" and (#text % 2 > 0) and suffix or ""
tdo:SetData(wx.wxDataFormat(wx.wxDF_TEXT), text..workaround)
local clip = wx.wxClipboard.Get()
clip:Open()
clip:SetData(tdo)
clip:Close()
end
function editor:PasteDyn()
if not self.useraw then return self:Paste() end
local tdo = wx.wxTextDataObject()
local clip = wx.wxClipboard.Get()
clip:Open()
clip:GetData(tdo)
clip:Close()
local ok, text = tdo:GetDataHere(wx.wxDataFormat(wx.wxDF_TEXT))
-- check if the fragment being pasted is a valid UTF-8 sequence
if not ok or text == "" or wx.wxString.FromUTF8(text) ~= "" then return self:Paste() end
if ide.osname == "Windows" then text = text:gsub(suffix.."+$","") end
self:AddTextRaw(text)
self:GotoPos(self:GetCurrentPos())
end
function editor:GotoPosEnforcePolicy(pos)
self:GotoPos(pos)
self:EnsureVisibleEnforcePolicy(self:LineFromPosition(pos))
end
function editor:CanFold()
local foldable = false
for m = 0, ide.MAXMARGIN do
if editor:GetMarginWidth(m) > 0
and editor:GetMarginMask(m) == wxstc.wxSTC_MASK_FOLDERS then
foldable = true
end
end
return foldable
end
-- circle through "fold all" => "hide base lines" => "unfold all"
function editor:FoldSome()
editor:Colourise(0, -1) -- update doc's folding info
local foldall = false -- at least on header unfolded => fold all
local hidebase = false -- at least one base is visible => hide all
local lines = editor:GetLineCount()
for ln = 0, lines-1 do
local foldRaw = editor:GetFoldLevel(ln)
local foldLvl = foldRaw % 4096
local foldHdr = (math.floor(foldRaw / 8192) % 2) == 1
-- at least one header is expanded
foldall = foldall or (foldHdr and editor:GetFoldExpanded(ln))
-- at least one base can be hidden
hidebase = hidebase or (
not foldHdr
and ln > 1 -- first line can't be hidden, so ignore it
and foldLvl == wxstc.wxSTC_FOLDLEVELBASE
and bit.band(foldRaw, wxstc.wxSTC_FOLDLEVELWHITEFLAG) == 0
and editor:GetLineVisible(ln))
end
-- shows lines; this doesn't change fold status for folded lines
if not foldall and not hidebase then editor:ShowLines(0, lines-1) end
for ln = 0, lines-1 do
local foldRaw = editor:GetFoldLevel(ln)
local foldLvl = foldRaw % 4096
local foldHdr = (math.floor(foldRaw / 8192) % 2) == 1
if foldall then
if foldHdr and editor:GetFoldExpanded(ln) then
editor:ToggleFold(ln)
end
elseif hidebase then
if not foldHdr and (foldLvl == wxstc.wxSTC_FOLDLEVELBASE) then
editor:HideLines(ln, ln)
end
else -- unfold all
if foldHdr and not editor:GetFoldExpanded(ln) then
editor:ToggleFold(ln)
end
end
end
editor:EnsureCaretVisible()
end
local function getMarginWidth(editor)
local width = 0
for m = 0, ide.MAXMARGIN do width = width + editor:GetMarginWidth(m) end
return width
end
function editor:ShowPosEnforcePolicy(pos)
local line = self:LineFromPosition(pos)
self:EnsureVisibleEnforcePolicy(line)
-- skip the rest if line wrapping is on
if editor:GetWrapMode() ~= wxstc.wxSTC_WRAP_NONE then return end
local xwidth = self:GetClientSize():GetWidth() - getMarginWidth(self)
local xoffset = self:GetTextExtent(self:GetLineDyn(line):sub(1, pos-self:PositionFromLine(line)+1))
self:SetXOffset(xoffset > xwidth and xoffset-xwidth or 0)
end
function editor:ClearAny()
local length = self:GetLength()
local selections = ide.wxver >= "2.9.5" and self:GetSelections() or 1
self:Clear() -- remove selected fragments
-- check if the modification has failed, which may happen
-- if there is "invisible" text in the selected fragment.
-- if there is only one selection, then delete manually.
if length == self:GetLength() and selections == 1 then
self:SetTargetStart(self:GetSelectionStart())
self:SetTargetEnd(self:GetSelectionEnd())
self:ReplaceTarget("")
end
end
function editor:MarkerGetAll(mask, from, to)
mask = mask or 2^24-1
local markers = {}
local line = editor:MarkerNext(from or 0, mask)
while line > -1 do
table.insert(markers, {line, editor:MarkerGet(line)})
if to and line > to then break end
line = editor:MarkerNext(line + 1, mask)
end
return markers
end
editor:Connect(wx.wxEVT_KEY_DOWN,
function (event)
local keycode = event:GetKeyCode()
local mod = event:GetModifiers()
if (keycode == wx.WXK_DELETE and mod == wx.wxMOD_SHIFT)
or (keycode == wx.WXK_INSERT and mod == wx.wxMOD_CONTROL)
or (keycode == wx.WXK_INSERT and mod == wx.wxMOD_SHIFT) then
local id = keycode == wx.WXK_DELETE and ID.CUT or mod == wx.wxMOD_SHIFT and ID.PASTE or ID.COPY
ide.frame:AddPendingEvent(wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, id))
elseif keycode == wx.WXK_CAPITAL and mod == wx.wxMOD_CONTROL then
-- ignore Ctrl+CapsLock
else
event:Skip()
end
end)
return editor
end
function ide:LoadFile(...) return LoadFile(...) end
function ide:CopyToClipboard(text)
if wx.wxClipboard:Get():Open() then
wx.wxClipboard:Get():SetData(wx.wxTextDataObject(text))
wx.wxClipboard:Get():Close()
return true
end
return false
end
function ide:GetSetting(path, setting)
local settings = self.settings
local curpath = settings:GetPath()
settings:SetPath(path)
local ok, value = settings:Read(setting)
settings:SetPath(curpath)
return ok and value or nil
end
function ide:RemoveMenuItem(id, menu)
local _, menu, pos = self:FindMenuItem(id, menu)
if menu then
self:GetMainFrame():Disconnect(id, wx.wxID_ANY, wx.wxEVT_COMMAND_MENU_SELECTED)
self:GetMainFrame():Disconnect(id, wx.wxID_ANY, wx.wxEVT_UPDATE_UI)
menu:Disconnect(id, wx.wxID_ANY, wx.wxEVT_COMMAND_MENU_SELECTED)
menu:Disconnect(id, wx.wxID_ANY, wx.wxEVT_UPDATE_UI)
menu:Remove(id)
local positem = menu:FindItemByPosition(pos)
if (not positem or positem:GetKind() == wx.wxITEM_SEPARATOR)
and pos > 0
and (menu:FindItemByPosition(pos-1):GetKind() == wx.wxITEM_SEPARATOR) then
menu:Destroy(menu:FindItemByPosition(pos-1))
end
return true
end
return false
end
function ide:ExecuteCommand(cmd, wdir, callback, endcallback)
local proc = wx.wxProcess(self:GetOutput())
proc:Redirect()
local cwd
if (wdir and #wdir > 0) then -- ignore empty directory
cwd = wx.wxFileName.GetCwd()
cwd = wx.wxFileName.SetCwd(wdir) and cwd
end
local pid = wx.wxExecute(cmd, wx.wxEXEC_ASYNC, proc)
pid = pid ~= -1 and pid ~= 0 and pid or nil
if cwd then wx.wxFileName.SetCwd(cwd) end -- restore workdir
if not pid then return pid, wx.wxSysErrorMsg() end
OutputSetCallbacks(pid, proc, callback or function() end, endcallback)
return pid
end
function ide:CreateImageList(group, ...)
local _ = wx.wxLogNull() -- disable error reporting in popup
local size = wx.wxSize(16,16)
local imglist = wx.wxImageList(16,16)
for i = 1, select('#', ...) do
local icon, file = self:GetBitmap(select(i, ...), group, size)
if imglist:Add(icon) == -1 then
DisplayOutputLn(("Failed to add image '%s' to the image list.")
:format(file or select(i, ...)))
end
end
return imglist
end
local tintdef = 100
local function iconFilter(bitmap, tint)
if type(tint) == 'function' then return tint(bitmap) end
if type(tint) ~= 'table' or #tint ~= 3 then return bitmap end
local tr, tg, tb = tint[1]/255, tint[2]/255, tint[3]/255
local pi = 0.299*tr + 0.587*tg + 0.114*tb -- pixel intensity
local perc = (tint[0] or tintdef)/tintdef
tr, tg, tb = tr*perc, tg*perc, tb*perc
local img = bitmap:ConvertToImage()
for x = 0, img:GetWidth()-1 do
for y = 0, img:GetHeight()-1 do
if not img:IsTransparent(x, y) then
local r, g, b = img:GetRed(x, y)/255, img:GetGreen(x, y)/255, img:GetBlue(x, y)/255
local gs = (r + g + b) / 3
local weight = 1-4*(gs-0.5)*(gs-0.5)
r = math.max(0, math.min(255, math.floor(255 * (gs + (tr-pi) * weight))))
g = math.max(0, math.min(255, math.floor(255 * (gs + (tg-pi) * weight))))
b = math.max(0, math.min(255, math.floor(255 * (gs + (tb-pi) * weight))))
img:SetRGB(x, y, r, g, b)
end
end
end
return wx.wxBitmap(img)
end
local icons = {} -- icon cache to avoid reloading the same icons
function ide:GetBitmap(id, client, size)
local im = self.config.imagemap
local width = size:GetWidth()
local key = width.."/"..id
local keyclient = key.."-"..client
local mapped = im[keyclient] or im[id.."-"..client] or im[key] or im[id]
-- mapped may be a file name/path or wxImage object; take that into account
if type(im[id.."-"..client]) == 'string' then keyclient = width.."/"..im[id.."-"..client]
elseif type(im[keyclient]) == 'string' then keyclient = im[keyclient]
elseif type(im[id]) == 'string' then
id = im[id]
key = width.."/"..id
keyclient = key.."-"..client
end
local fileClient = self:GetAppName() .. "/res/" .. keyclient .. ".png"
local fileKey = self:GetAppName() .. "/res/" .. key .. ".png"
local isImage = type(mapped) == 'userdata' and mapped:GetClassInfo():GetClassName() == 'wxImage'
local file
if mapped and (isImage or wx.wxFileName(mapped):FileExists()) then file = mapped
elseif wx.wxFileName(fileClient):FileExists() then file = fileClient
elseif wx.wxFileName(fileKey):FileExists() then file = fileKey
else return wx.wxArtProvider.GetBitmap(id, client, size) end
local icon = icons[file] or iconFilter(wx.wxBitmap(file), self.config.imagetint)
icons[file] = icon
return icon, file
end
function ide:AddPackage(name, package)
self.packages[name] = setmetatable(package, self.proto.Plugin)
self.packages[name].fname = name
return self.packages[name]
end
function ide:RemovePackage(name) self.packages[name] = nil end
function ide:AddWatch(watch, value)
local mgr = self.frame.uimgr
local pane = mgr:GetPane("watchpanel")
if (pane:IsOk() and not pane:IsShown()) then
pane:Show()
mgr:Update()
end
local watchCtrl = self.debugger.watchCtrl
if not watchCtrl then return end
local root = watchCtrl:GetRootItem()
if not root or not root:IsOk() then return end
local item = watchCtrl:GetFirstChild(root)
while true do
if not item:IsOk() then break end
if watchCtrl:GetItemExpression(item) == watch then
if value then watchCtrl:SetItemText(item, watch .. ' = ' .. tostring(value)) end
return item
end
item = watchCtrl:GetNextSibling(item)
end
item = watchCtrl:AppendItem(root, watch, 1)
watchCtrl:SetItemExpression(item, watch, value)
return item
end
function ide:AddInterpreter(name, interpreter)
self.interpreters[name] = setmetatable(interpreter, self.proto.Interpreter)
ProjectUpdateInterpreters()
end
function ide:RemoveInterpreter(name)
self.interpreters[name] = nil
ProjectUpdateInterpreters()
end
function ide:AddSpec(name, spec)
self.specs[name] = spec
UpdateSpecs()
end
function ide:RemoveSpec(name) self.specs[name] = nil end
function ide:AddAPI(type, name, api)
self.apis[type] = self.apis[type] or {}
self.apis[type][name] = api
end
function ide:RemoveAPI(type, name) self.apis[type][name] = nil end
function ide:AddConsoleAlias(alias, table) return ShellSetAlias(alias, table) end
function ide:RemoveConsoleAlias(alias) return ShellSetAlias(alias, nil) end
function ide:AddMarker(...) return StylesAddMarker(...) end
function ide:GetMarker(marker) return StylesGetMarker(marker) end
function ide:RemoveMarker(marker) StylesRemoveMarker(marker) end
local indicators = {}
function ide:AddIndicator(indic, num)
num = num or indicators[indic]
if not num then -- new indicator; find the smallest available number
local nums = {}
for _, indicator in pairs(indicators) do
if indicator >= wxstc.wxSTC_INDIC_CONTAINER then
nums[indicator-wxstc.wxSTC_INDIC_CONTAINER+1] = true
end
end
num = #nums + wxstc.wxSTC_INDIC_CONTAINER
if num > wxstc.wxSTC_INDIC_MAX then return end
end
indicators[indic] = num
return num
end
function ide:GetIndicator(indic) return indicators[indic] end
function ide:GetIndicators() return indicators end
function ide:RemoveIndicator(indic) indicators[indic] = nil end
-- this provides a simple stack for saving/restoring current configuration
local configcache = {}
function ide:AddConfig(name, files)
if not name or configcache[name] then return end -- don't overwrite existing slots
if type(files) ~= "table" then files = {files} end -- allow to pass one value
configcache[name] = {
config = require('mobdebug').dump(self.config, {nocode = true}),
configmeta = getmetatable(self.config),
packages = {},
}
-- build a list of existing packages
local packages = {}
for package in pairs(self.packages) do packages[package] = true end
-- load config file(s)
for _, file in pairs(files) do LoadLuaConfig(MergeFullPath(name, file)) end
-- register newly added packages (if any)
for package in pairs(self.packages) do
if not packages[package] then -- this is a newly added package
PackageEventHandleOne(package, "onRegister")
configcache[name].packages[package] = true
end
end
ReApplySpecAndStyles() -- apply current config to the UI
end
function ide:RemoveConfig(name)
if not name or not configcache[name] then return end
-- unregister cached packages
for package in pairs(configcache[name].packages) do PackageUnRegister(package) end
-- load original config
local ok, res = LoadSafe(configcache[name].config)
if ok then
self.config = res
if configcache[name].configmeta then setmetatable(self.config, configcache[name].configmeta) end
else
DisplayOutputLn(("Error while restoring configuration: '%s'."):format(res))
end
configcache[name] = nil -- clear the slot after use
ReApplySpecAndStyles() -- apply current config to the UI
end
local panels = {}
function ide:AddPanel(ctrl, panel, name, conf)
local width, height = 360, 200
local notebook = wxaui.wxAuiNotebook(self.frame, wx.wxID_ANY,
wx.wxDefaultPosition, wx.wxDefaultSize,
wxaui.wxAUI_NB_DEFAULT_STYLE + wxaui.wxAUI_NB_TAB_EXTERNAL_MOVE
- wxaui.wxAUI_NB_CLOSE_ON_ACTIVE_TAB + wx.wxNO_BORDER)
notebook:AddPage(ctrl, name, true)
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_BG_DCLICK,
function() PaneFloatToggle(notebook) end)
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_PAGE_CLOSE,
function(event) event:Veto() end)
local mgr = self.frame.uimgr
mgr:AddPane(notebook, wxaui.wxAuiPaneInfo():
Name(panel):Float():CaptionVisible(false):PaneBorder(false):
MinSize(width/2,height/2):
BestSize(width,height):FloatingSize(width,height):
PinButton(true):Hide())
if type(conf) == "function" then conf(mgr:GetPane(panel)) end
mgr.defaultPerspective = mgr:SavePerspective() -- resave default perspective
panels[name] = {ctrl, panel, name, conf}
return mgr:GetPane(panel), notebook
end
function ide:RemovePanel(panel)
local mgr = self.frame.uimgr
local pane = mgr:GetPane(panel)
if pane:IsOk() then
local win = pane.window
mgr:DetachPane(win)
win:Destroy()
mgr:Update()
end
end
function ide:AddPanelDocked(notebook, ctrl, panel, name, conf, activate)
notebook:AddPage(ctrl, name, activate ~= false)
panels[name] = {ctrl, panel, name, conf}
return notebook
end
function ide:IsPanelDocked(panel)
local layout = self:GetSetting("/view", "uimgrlayout")
return layout and not layout:find(panel)
end
function ide:IsValidCtrl(ctrl)
return ctrl and pcall(function() ctrl:GetId() end)
end
function ide:IsValidProperty(ctrl, prop)
return ide:IsValidCtrl(ctrl) and pcall(function() return ctrl[prop] end)
end
function ide:IsWindowShown(win)
while win do
if not win:IsShown() then return false end
win = win:GetParent()
end
return true
end
function ide:RestorePanelByLabel(name)
if not panels[name] then return end
return self:AddPanel(unpack(panels[name]))
end
function ide:AddTool(name, command, updateui)
return ToolsAddTool(name, command, updateui)
end
function ide:RemoveTool(name)
return ToolsRemoveTool(name)
end

View File

@ -0,0 +1,195 @@
-- Copyright 2015 Paul Kulchenko, ZeroBrane LLC
---------------------------------------------------------
local frame = ide:GetMainFrame()
local margin = {top = 10, left = 10, bottom = 10, right = 10}
local function printScaling(dc, printOut)
local pageSizeMM_x, pageSizeMM_y = printOut:GetPageSizeMM()
local ppiScr_x, ppiScr_y = printOut:GetPPIScreen()
local ppiPrn_x, ppiPrn_y = printOut:GetPPIPrinter()
local ppi_scale_x = ppiPrn_x/ppiScr_x
local ppi_scale_y = ppiPrn_y/ppiScr_y
-- get the size of DC in pixels and the number of pixels in the page
local dcSize_x, dcSize_y = dc:GetSize()
local pagePixSize_x, pagePixSize_y = printOut:GetPageSizePixels()
local dc_pagepix_scale_x = dcSize_x/pagePixSize_x
local dc_pagepix_scale_y = dcSize_y/pagePixSize_y
local dc_scale_x = ppi_scale_x * dc_pagepix_scale_x
local dc_scale_y = ppi_scale_y * dc_pagepix_scale_y
-- calculate the pixels / mm (25.4 mm = 1 inch)
local ppmm_x = ppiScr_x / 25.4
local ppmm_y = ppiScr_y / 25.4
-- adjust the page size for the pixels / mm scaling factor
local page_x = math.floor(pageSizeMM_x * ppmm_x)
local page_y = math.floor(pageSizeMM_y * ppmm_y)
local pageRect = wx.wxRect(0, 0, page_x, page_y)
-- get margins informations and convert to printer pixels
local top = math.floor(margin.top * ppmm_y)
local bottom = math.floor(margin.bottom * ppmm_y)
local left = math.floor(margin.left * ppmm_x)
local right = math.floor(margin.right * ppmm_x)
dc:SetUserScale(dc_scale_x, dc_scale_y)
local printRect = wx.wxRect(left, top, page_x-(left+right), page_y-(top+bottom))
return printRect, pageRect
end
local function connectPrintEvents(printer, printOut)
local editor = ide:GetEditorWithFocus()
local cfg = ide.config.print
local pages
function printOut:OnPrintPage(pageNum)
local dc = self:GetDC()
local printRect, pageRect = printScaling(dc, printOut)
-- print to an area smaller by the height of the header/footer
dc:SetFont(editor:GetFont())
local _, headerHeight = dc:GetTextExtent("qH")
local textRect = wx.wxRect(printRect)
if cfg.header then
textRect:SetY(textRect:GetY() + headerHeight*1.5)
textRect:SetHeight(textRect:GetHeight() - headerHeight*1.5)
end
if cfg.footer then
textRect:SetHeight(textRect:GetHeight() - headerHeight*1.5)
end
local selection = printer:GetPrintDialogData():GetSelection()
local spos = selection and editor:GetSelectionStart() or 1
local epos = selection and editor:GetSelectionEnd() or editor:GetLength()
if pageNum == nil then
pages = {}
ide:PushStatus("")
printOut.startTime = wx.wxNow()
local pos = spos
while pos < epos do
table.insert(pages, pos)
pos = editor:FormatRange(false, pos, epos, dc, dc, textRect, pageRect)
ide:PopStatus()
ide:PushStatus(TR("%s%% formatted..."):format(math.floor((pos-spos)*100.0/(epos-spos))))
end
if #pages == 0 then pages = {0} end
ide:PopStatus()
else
ide:SetStatusFor(TR("Formatting page %d..."):format(pageNum))
editor:FormatRange(true, pages[pageNum], epos, dc, dc, textRect, pageRect)
local c = wx.wxColour(127, 127, 127)
dc:SetPen(wx.wxPen(c, 1, wx.wxSOLID))
dc:SetTextForeground(c)
local doc = ide:GetDocument(editor)
local format = "([^\t]*)\t?([^\t]*)\t?([^\t]*)"
local placeholders = {
D = printOut.startTime,
p = pageNum,
P = #pages,
S = doc and doc:GetFileName() or "",
}
dc:SetFont(editor:GetFont())
if cfg.header then
local left, center, right = ExpandPlaceholders(cfg.header, placeholders):match(format)
dc:DrawText(left, printRect.X, printRect.Y)
dc:DrawText(center, printRect.Left + (printRect.Left + printRect.Width - dc:GetTextExtentSize(center).Width)/2, printRect.Y)
dc:DrawText(right, printRect.Left + printRect.Width - dc:GetTextExtentSize(right).Width, printRect.Y)
dc:DrawLine(printRect.X, printRect.Y + headerHeight, printRect.Left + printRect.Width, printRect.Y + headerHeight)
end
if cfg.footer then
local footerY = printRect.Y + printRect.Height - headerHeight
local left, center, right = ExpandPlaceholders(cfg.footer, placeholders):match(format)
dc:DrawText(left, printRect.X, footerY)
dc:DrawText(center, printRect.Left + (printRect.Left + printRect.Width - dc:GetTextExtentSize(center).Width)/2, footerY)
dc:DrawText(right, printRect.Left + printRect.Width - dc:GetTextExtentSize(right).Width, footerY)
dc:DrawLine(printRect.X, footerY, printRect.Left + printRect.Width, footerY)
end
end
return true
end
function printOut:HasPage(pageNum) return pages[pageNum] ~= nil end
function printOut:GetPageInfo()
-- on Linux `GetPageInfo` is called before the canvas is initialized, which prevents
-- proper calculation of the number of pages (wx2.9.5).
-- Return defaults here as it's going to be called once more in the right place.
if ide.osname == "Unix" and not pages then return 1, 9999, 1, 9999 end
local printDD = printer:GetPrintDialogData()
-- due to wxwidgets bug (http://trac.wxwidgets.org/ticket/17200), if `to` page is not set explicitly,
-- only one page is being printed when `selection` option is selected in the print dialog.
if printDD:GetSelection() then printDD:SetToPage(#pages) end -- set the page as a workaround
local tofrom = not printDD:GetSelection() and not printDD:GetAllPages()
return 1, #pages, tofrom and printDD:GetFromPage() or 1, tofrom and printDD:GetToPage() or #pages
end
function printOut:OnPreparePrinting() self:OnPrintPage() end
end
frame:Connect(ID_PAGESETUP, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local pageSetupDD = wx.wxPageSetupDialogData()
pageSetupDD.MarginTopLeft = wx.wxPoint(margin.left, margin.top)
pageSetupDD.MarginBottomRight = wx.wxPoint(margin.right, margin.bottom)
pageSetupDD:EnableOrientation(false)
pageSetupDD:EnablePaper(false)
local pageSetupDialog = wx.wxPageSetupDialog(frame, pageSetupDD)
pageSetupDialog:ShowModal()
pageSetupDD = pageSetupDialog:GetPageSetupDialogData()
margin.top, margin.left = pageSetupDD.MarginTopLeft.y, pageSetupDD.MarginTopLeft.x
margin.bottom, margin.right = pageSetupDD.MarginBottomRight.y, pageSetupDD.MarginBottomRight.x
end)
frame:Connect(ID_PRINT, wx.wxEVT_COMMAND_MENU_SELECTED,
function (event)
local cfg = ide.config.print
local editor = ide:GetEditorWithFocus()
editor:SetPrintMagnification(cfg.magnification)
editor:SetPrintColourMode(cfg.colourmode)
editor:SetPrintWrapMode(cfg.wrapmode)
-- only enable selection if there is something selected in the editor (ignore multiple selections)
local printDD = wx.wxPrintDialogData()
printDD:EnableSelection(editor:GetSelectionStart() ~= editor:GetSelectionEnd())
local printer = wx.wxPrinter(printDD)
local luaPrintout = wx.wxLuaPrintout()
connectPrintEvents(printer, luaPrintout)
-- save and hide indicators
local indics = {}
for _, num in pairs(ide:GetIndicators()) do
indics[num] = editor:IndicatorGetStyle(num)
editor:IndicatorSetStyle(num, wxstc.wxSTC_INDIC_HIDDEN)
end
-- bold keywords
local keywords = {}
for _, num in ipairs(ide:IsValidProperty(editor, 'spec') and editor.spec.lexerstyleconvert and editor.spec.lexerstyleconvert.keywords0 or {}) do
keywords[num] = editor:StyleGetBold(num)
editor:StyleSetBold(num, true)
end
local ok = printer:Print(frame, luaPrintout, true)
-- restore indicators
for n, style in pairs(indics) do editor:IndicatorSetStyle(n, style) end
for n, style in pairs(keywords) do editor:StyleSetBold(n, style) end
if not ok and printer:GetLastError() == wx.wxPRINTER_ERROR then
ReportError("There was a problem while printing.\nCheck if your current printer is set correctly.")
end
end)
frame:Connect(ID_PRINT, wx.wxEVT_UPDATE_UI, function(event) event:Enable(ide:GetEditorWithFocus() ~= nil) end)
local _, menu, epos = ide:FindMenuItem(ID.EXIT)
-- disable printing on Unix/Linux as it generates incorrect layout (wx2.9.5, wx3.1)
if ide.osname ~= "Unix" and menu and epos then
-- insert Print-repated menu items (going in the opposite order)
menu:Insert(epos-1, ID_PAGESETUP, TR("Page Setup..."), "")
menu:Insert(epos-1, ID_PRINT, TR("&Print..."), TR("Print the current document"))
menu:InsertSeparator(epos-1)
end

View File

@ -0,0 +1,68 @@
-- Copyright 2013-15 Paul Kulchenko, ZeroBrane LLC
---------------------------------------------------------
local q = EscapeMagic
local modpref = ide.MODPREF
ide.proto.Document = {__index = {
GetFileName = function(self) return self.fileName end,
GetFilePath = function(self) return self.filePath end,
GetFileExt = function(self) return GetFileExt(self.fileName) end,
GetModTime = function(self) return self.modTime end,
GetEditor = function(self) return self.editor end,
GetTabIndex = function(self) return self.index end,
IsModified = function(self) return self.isModified end,
IsNew = function(self) return self.filePath == nil end,
SetModified = function(self, modified)
self.isModified = modified
self:SetTabText()
end,
SetTabText = function(self, text)
ide:GetEditorNotebook():SetPageText(self.index,
(self.isModified and modpref or '')..(text or self:GetTabText()))
end,
GetTabText = function(self)
if self.index == nil then return self.fileName end
return ide:GetEditorNotebook():GetPageText(self.index):gsub("^"..q(modpref), "")
end,
SetActive = function(self) SetEditorSelection(self.index) end,
Save = function(self) return SaveFile(self.editor, self.filePath) end,
Close = function(self) return ClosePage(self.index) end,
CloseAll = function(self) return CloseAllPagesExcept(-1) end,
CloseAllExcept = function(self) return CloseAllPagesExcept(self.index) end,
}}
ide.proto.Plugin = {__index = {
GetName = function(self) return self.name end,
GetFileName = function(self) return self.fname end,
GetFilePath = function(self) return MergeFullPath(GetPathWithSep(ide.editorFilename), self.fpath) end,
GetConfig = function(self) return ide.config[self.fname] or {} end,
GetSettings = function(self) return SettingsRestorePackage(self.fname) end,
SetSettings = function(self, settings, opts) SettingsSavePackage(self.fname, settings, opts) end,
}}
ide.proto.Interpreter = {__index = {
GetName = function(self) return self.name end,
GetFileName = function(self) return self.fname end,
GetExePath = function(self, ...) return self:fexepath(...) end,
GetAPI = function(self) return self.api end,
fprojdir = function(self,wfilename)
return wfilename:GetPath(wx.wxPATH_GET_VOLUME)
end,
fworkdir = function (self,wfilename)
local proj = ide:GetProject()
return proj and proj:gsub("[\\/]$","") or wfilename:GetPath(wx.wxPATH_GET_VOLUME)
end,
}}
ide.proto.Debugger = {__index = {
IsRunning = function(self) return self.running end,
IsConnected = function(self) return self.server end,
GetHostName = function(self) return self.hostname end,
GetPortNumber = function(self) return self.portnumber end,
}}
ide.proto.ID = {
__index = function(_, id) return _G['ID_'..id] end,
__call = function(_, id) return IDgen(id) end,
}

View File

@ -0,0 +1,593 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Lomtik Software (J. Winwood & John Labenski)
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local layoutlabel = {
UIMANAGER = "uimgrlayout",
NOTEBOOK = "nblayout",
NOTEBOOKOUTPUT = "nbbtmlayout",
NOTEBOOKPROJECT = "nbprojlayout",
DOCKNOTEBOOK = "nbdocklayout",
DOCKNOTEBOOKOUTPUT = "nbbtmdocklayout",
DOCKNOTEBOOKPROJECT = "nbprojdocklayout",
STATUSBAR = "statusbar",
}
-- ----------------------------------------------------------------------------
-- Initialize the wxConfig for loading/saving the preferences
local ini = ide.config.ini
-- if ini path is relative and includes a directory name, make it relative to the IDE location
ini = ini and (not wx.wxIsAbsolutePath(ini) and wx.wxFileName(ini):GetDirCount() > 0
and MergeFullPath(GetPathWithSep(ide.editorFilename), ini) or ini)
-- check that the ini file doesn't point to a directory
if ini and (wx.wxFileName(ini):IsDir() or wx.wxIsAbsolutePath(ini) and wx.wxDirExists(ini)) then
print(("Can't use 'ini' configuration setting '%s' that points to a directory instead of a file; ignored.")
:format(ini))
ini = nil
end
-- check that the directory is writable
if ini and wx.wxIsAbsolutePath(ini) and not wx.wxFileName(ini):IsDirWritable() then
print(("Can't use 'ini' configuration setting '%s' that points to a non-writable directory; ignored.")
:format(ini))
ini = nil
end
local settings = wx.wxFileConfig(GetIDEString("settingsapp"), GetIDEString("settingsvendor"), ini or "")
ide.settings = settings
local function settingsReadSafe(settings,what,default)
local cr,out = settings:Read(what,default)
return cr and out or default
end
-- ----------------------------------------------------------------------------
-- wxConfig load/save preferences functions
function SettingsRestoreFramePosition(window, windowName)
local path = settings:GetPath()
settings:SetPath("/"..windowName)
local s = tonumber(select(2,settings:Read("s", -1)))
local x = tonumber(select(2,settings:Read("x", 0)))
local y = tonumber(select(2,settings:Read("y", 0)))
local w = tonumber(select(2,settings:Read("w", 1100)))
local h = tonumber(select(2,settings:Read("h", 700)))
if (s ~= -1) and (s ~= 1) and (s ~= 2) then
local clientX, clientY, clientWidth, clientHeight = wx.wxClientDisplayRect()
if x < clientX then x = clientX end
if y < clientY then y = clientY end
if w > clientWidth then w = clientWidth end
if h > clientHeight then h = clientHeight end
window:SetSize(x, y, w, h)
elseif s == 1 then
window:Maximize(true)
end
settings:SetPath(path)
end
function SettingsSaveFramePosition(window, windowName)
local path = settings:GetPath()
settings:SetPath("/"..windowName)
local s = 0
local w, h = window:GetSizeWH()
local x, y = window:GetPositionXY()
if window:IsMaximized() then
s = 1
elseif window:IsIconized() then
s = 2
end
settings:Write("s", s==2 and 0 or s) -- iconized maybe - but that shouldnt be saved
if s == 0 then
settings:Write("x", x)
settings:Write("y", y)
settings:Write("w", w)
settings:Write("h", h)
end
settings:SetPath(path)
end
---
-- (table) SettingsRestoreFileHistory (function)
-- restores a list of recently loaded documents from the settings table
-- a table is returned which contains tables each with a filename key, pointing to
-- the filename
function SettingsRestoreFileHistory(fntab)
local path = settings:GetPath()
local listname = "/filehistory"
settings:SetPath(listname)
local outtab = {}
local inlist = {}
for id=1,ide.config.filehistorylength do
local couldread, name = settings:Read(tostring(id), "")
if not couldread or name == "" then break end
if not inlist[name] then
inlist[name] = true
table.insert(outtab,{filename = name})
end
end
if fntab then fntab(outtab) end
settings:SetPath(path)
return outtab
end
function SettingsSaveFileHistory (filehistory)
local listname = "/filehistory"
local path = settings:GetPath()
settings:DeleteGroup(listname)
settings:SetPath(listname)
for i,doc in ipairs(filehistory) do
settings:Write(tostring(i), doc.filename)
end
settings:SetPath(path)
end
---
-- () SettingsRestoreFileSession (function [, string section])
-- restores a list of opened files from the file settings
-- calls the given function with the restored table, a list
-- of tables containing tables like
-- {filename = "filename", cursorpos = <cursor position>}
function SettingsRestoreFileSession(fntab, section)
local listname = section or "/session"
local path = settings:GetPath()
settings:SetPath(listname)
local outtab = {}
local params = {}
local ismore, key, index = settings:GetFirstEntry("", 0)
while (ismore) do
local couldread, value = settings:Read(key, "")
if tonumber(key) then
local fname,cursorpos = value:match("^(.+);(.-)$")
if (couldread and value ~= "") then
outtab[tonumber(key)] =
{filename = fname or value, cursorpos = tonumber(cursorpos) or 0}
end
else
params[key] = tonumber(value) or value
end
ismore, key, index = settings:GetNextEntry(index)
end
if fntab then fntab(outtab, params) end
settings:SetPath(path)
return outtab
end
---
-- () SettingsSaveFileSession (table opendocs, table params [, string section])
-- saves the list of currently opened documents (passed in the opendocs table)
-- in the settings.
function SettingsSaveFileSession(opendocs, params, section)
local listname = section or "/session"
local path = settings:GetPath()
settings:DeleteGroup(listname)
settings:SetPath(listname)
for i,doc in ipairs(opendocs) do
settings:Write(tostring(i), doc.filename..";"..doc.cursorpos)
end
-- save all other parameters
for k,v in pairs(params) do settings:Write(k, v) end
settings:SetPath(path)
end
---
-- () SettingsRestoreProjectSession (function)
function SettingsRestoreProjectSession(fntab)
local listname = "/projectsession"
local path = settings:GetPath()
settings:SetPath(listname)
local outtab = {}
local couldread = true
local id = 1
local name
while (couldread) do
couldread, name = settings:Read(tostring(id), "")
couldread = couldread and name ~= ""
if (couldread) then
if (wx.wxDirExists(name)) then
table.insert(outtab,name)
local function projsession(...) ProjectConfig(name, {...}) end
SettingsRestoreFileSession(projsession, listname .. "/" .. tostring(id))
end
id = id + 1
end
end
if fntab then fntab(outtab) end
settings:SetPath(path)
return outtab
end
---
-- () SettingsSaveProjectSession (table projdirs)
-- saves the list of currently active projects
-- in the settings.
function SettingsSaveProjectSession(projdirs)
local listname = "/projectsession"
local path = settings:GetPath()
settings:DeleteGroup(listname)
settings:SetPath(listname)
for i,dir in ipairs(projdirs) do
settings:Write(tostring(i), dir)
local opendocs, params = ProjectConfig(dir)
if opendocs then
SettingsSaveFileSession(opendocs, params, listname .. "/" .. tostring(i))
end
end
settings:SetPath(path)
end
function SettingsRestorePackage(package)
local packagename = "/package/"..package
local path = settings:GetPath()
settings:SetPath(packagename)
local outtab = {}
local ismore, key, index = settings:GetFirstEntry("", 0)
while (ismore) do
local couldread, value = settings:Read(key, "")
if couldread then
local ok, res = LoadSafe("return "..value)
if ok then outtab[key] = res
else
outtab[key] = nil
ide:Print(("Couldn't load and ignored '%s' settings for package '%s': %s")
:format(key, package, res))
end
end
ismore, key, index = settings:GetNextEntry(index)
end
settings:SetPath(path)
return outtab
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
function SettingsSavePackage(package, values, opts)
local packagename = "/package/"..package
local path = settings:GetPath()
settings:DeleteGroup(packagename)
settings:SetPath(packagename)
for k,v in pairs(values or {}) do settings:Write(k, plaindump(v, opts)) end
settings:SetPath(path)
end
-----------------------------------
local function saveNotebook(nb)
local cnt = nb:GetPageCount()
local function addTo(tab,key,value)
local out = tab[key] or {}
table.insert(out,value)
tab[key] = out
end
local pagesX = {}
local pagesY = {}
local str = "nblayout|"
for i=1,cnt do
local id = nb:GetPageText(i-1)
local pg = nb:GetPage(i-1)
local x,y = pg:GetPosition():GetXY()
addTo(pagesX,x,id)
addTo(pagesY,y,id)
end
local function sortedPages(tab)
local t = {}
for i in pairs(tab) do
table.insert(t,i)
end
table.sort(t)
return t
end
local sortedX = sortedPages(pagesX)
local sortedY = sortedPages(pagesY)
-- for now only support "1D" splits and prefer
-- dimension which has more, anything else
-- requires a more complex algorithm, yet to do
local pagesUse
local sortedUse
local split
if ( #sortedX >= #sortedY) then
pagesUse = pagesX
sortedUse = sortedX
split = "<X>"
else
pagesUse = pagesY
sortedUse = sortedY
split = "<Y>"
end
for _, v in ipairs(sortedUse) do
local pages = pagesUse[v]
for _, id in ipairs(pages) do
str = str..id.."|"
end
str = str..split.."|"
end
return str
end
local function loadNotebook(nb,str,fnIdConvert)
str = str:match("nblayout|(.+)")
if (not str) then return end
local cnt = nb:GetPageCount()
local sel = nb:GetSelection()
-- store old pages
local currentpages, order = {}, {}
for i=1,cnt do
local id = nb:GetPageText(i-1)
local newid = fnIdConvert and fnIdConvert(id) or id
currentpages[newid] = currentpages[newid] or {}
table.insert(currentpages[newid], {page = nb:GetPage(i-1), text = id, index = i-1})
order[i] = newid
end
-- remove them
for i=cnt,1,-1 do nb:RemovePage(i-1) end
-- read them and perform splits
local t = 0
local newsel
local function finishPage(page)
if (page.index == sel) then
newsel = t
end
t = t + 1
end
local direction
local splits = { X = wx.wxRIGHT, Y = wx.wxBOTTOM }
for cmd in str:gmatch("([^|]+)") do
local instr = cmd:match("<(%w)>")
if (not instr) then
local id = fnIdConvert and fnIdConvert(cmd) or cmd
local pageind = next(currentpages[id] or {})
if (pageind) then
local page = currentpages[id][pageind]
currentpages[id][pageind] = nil
nb:AddPage(page.page, page.text)
if (direction) then nb:Split(t, direction) end
finishPage(page)
end
end
direction = instr and splits[instr]
end
-- add anything we forgot; make sure page groups are in the order specified
for i=1,cnt do
local pagelist = currentpages[order[i]]
for _,page in pairs(pagelist) do
nb:AddPage(page.page, page.text)
finishPage(page)
end
end
-- set the active page as it was before
if (newsel) then nb:SetSelection(newsel) end
end
function SettingsRestoreView()
local listname = "/view"
local path = settings:GetPath()
settings:SetPath(listname)
local frame = ide.frame
local uimgr = frame.uimgr
local layoutcur = uimgr:SavePerspective()
local layout = settingsReadSafe(settings,layoutlabel.UIMANAGER,"")
if (layout ~= layoutcur) then
-- save the current toolbar besth and re-apply after perspective is loaded
-- bestw and besth has two separate issues:
-- (1) layout includes bestw that is only as wide as the toolbar size,
-- this leaves default background on the right side of the toolbar;
-- fix it by explicitly replacing with the screen width.
-- (2) besth may be wrong after icon size changes.
local toolbar = frame.uimgr:GetPane("toolbar")
local besth = toolbar:IsOk() and tonumber(uimgr:SavePaneInfo(toolbar):match("besth=([^;]+)"))
-- reload the perspective if the saved one is not empty as it's different from the default
if #layout > 0 then uimgr:LoadPerspective(layout, false) end
local screenw = frame:GetClientSize():GetWidth()
if toolbar:IsOk() and screenw > 0 then toolbar:BestSize(screenw, besth or -1) end
-- check if debugging panes are not mentioned and float them
for _, name in pairs({"stackpanel", "watchpanel"}) do
local pane = frame.uimgr:GetPane(name)
if pane:IsOk() and not layout:find(name) then pane:Float() end
end
-- check if the toolbar is not mentioned in the layout and show it
for _, name in pairs({"toolbar"}) do
local pane = frame.uimgr:GetPane(name)
if pane:IsOk() and not layout:find(name) then pane:Show() end
end
-- remove captions from all panes
local panes = frame.uimgr:GetAllPanes()
for index = 0, panes:GetCount()-1 do
uimgr:GetPane(panes:Item(index).name):CaptionVisible(false)
end
end
frame:GetStatusBar():Show(settingsReadSafe(settings,layoutlabel.STATUSBAR,true))
uimgr:Update()
layoutcur = saveNotebook(ide:GetOutputNotebook())
layout = settingsReadSafe(settings,layoutlabel.NOTEBOOKOUTPUT,layoutcur)
if (layout ~= layoutcur) then
loadNotebook(ide:GetOutputNotebook(),layout,
-- treat "Output (running)" same as "Output"
function(name) return
name:match(TR("Output")) or name:match("Output") or name end)
end
layoutcur = saveNotebook(ide:GetProjectNotebook())
layout = settingsReadSafe(settings,layoutlabel.NOTEBOOKPROJECT,layoutcur)
if (layout ~= layoutcur) then
loadNotebook(ide:GetProjectNotebook(),layout)
end
-- always select Output tab
local bottomnotebook = ide:GetOutputNotebook()
local index = bottomnotebook:GetPageIndex(bottomnotebook.errorlog)
if index >= 0 then bottomnotebook:SetSelection(index) end
layoutcur = saveNotebook(frame.notebook)
layout = settingsReadSafe(settings,layoutlabel.NOTEBOOK,layoutcur)
if (layout ~= layoutcur) then
loadNotebook(ide.frame.notebook,layout)
local openDocuments = ide.openDocuments
local nb = frame.notebook
local cnt = nb:GetPageCount()
for i=0,cnt-1 do
openDocuments[nb:GetPage(i):GetId()].index = i
end
end
-- restore configuration for notebook pages that have been split;
-- load saved dock_size values and update current values with saved ones
-- where dock_size configuration matches
for l, m in pairs({
[layoutlabel.DOCKNOTEBOOK] = ide:GetEditorNotebook():GetAuiManager(),
[layoutlabel.DOCKNOTEBOOKOUTPUT] = ide:GetOutputNotebook():GetAuiManager(),
[layoutlabel.DOCKNOTEBOOKPROJECT] = ide:GetProjectNotebook():GetAuiManager(),
}) do
-- ...|dock_size(5,0,0)=20|dock_size(2,1,0)=200|...
local prevlayout = settingsReadSafe(settings, l, "")
local curlayout = m:SavePerspective()
local newlayout = curlayout:gsub('(dock_size[^=]+=)(%d+)', function(t,v)
local val = prevlayout:match(EscapeMagic(t)..'(%d+)')
return t..(val or v)
end)
if newlayout ~= curlayout then m:LoadPerspective(newlayout) end
end
local editor = GetEditor()
if editor then editor:SetFocus() end
settings:SetPath(path)
end
function SettingsSaveView()
local listname = "/view"
local path = settings:GetPath()
settings:DeleteGroup(listname)
settings:SetPath(listname)
local frame = ide.frame
local uimgr = frame.uimgr
settings:Write(layoutlabel.UIMANAGER, uimgr:SavePerspective())
settings:Write(layoutlabel.NOTEBOOK, saveNotebook(ide:GetEditorNotebook()))
settings:Write(layoutlabel.NOTEBOOKOUTPUT, saveNotebook(ide:GetOutputNotebook()))
settings:Write(layoutlabel.NOTEBOOKPROJECT, saveNotebook(ide:GetProjectNotebook()))
settings:Write(layoutlabel.DOCKNOTEBOOK, ide:GetEditorNotebook():GetAuiManager():SavePerspective())
settings:Write(layoutlabel.DOCKNOTEBOOKOUTPUT, ide:GetOutputNotebook():GetAuiManager():SavePerspective())
settings:Write(layoutlabel.DOCKNOTEBOOKPROJECT, ide:GetProjectNotebook():GetAuiManager():SavePerspective())
settings:Write(layoutlabel.STATUSBAR, frame:GetStatusBar():IsShown())
settings:SetPath(path)
end
function SettingsRestoreEditorSettings()
local listname = "/editor"
local path = settings:GetPath()
settings:SetPath(listname)
local interpreter = settingsReadSafe(settings, "interpreter",
ide.config.interpreter or ide.config.default.interpreter)
ProjectSetInterpreter(interpreter)
settings:SetPath(path)
end
function SettingsSaveEditorSettings()
local listname = "/editor"
local path = settings:GetPath()
settings:DeleteGroup(listname)
settings:SetPath(listname)
settings:Write("interpreter", ide.interpreter and ide.interpreter.fname or ide.config.default.interpreter)
settings:SetPath(path)
end
function SettingsSaveAll()
SettingsSaveFileSession(GetOpenFiles())
SettingsSaveEditorSettings()
SettingsSaveProjectSession(FileTreeGetProjects())
SettingsSaveFileHistory(GetFileHistory())
SettingsSaveView()
SettingsSaveFramePosition(ide.frame, "MainFrame")
end

View File

@ -0,0 +1,574 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
local unpack = table.unpack or unpack
local bottomnotebook = ide.frame.bottomnotebook
local out = bottomnotebook.shellbox
local remotesend
local PROMPT_MARKER = StylesGetMarker("prompt")
local PROMPT_MARKER_VALUE = 2^PROMPT_MARKER
local ERROR_MARKER = StylesGetMarker("error")
local OUTPUT_MARKER = StylesGetMarker("output")
local MESSAGE_MARKER = StylesGetMarker("message")
local ANY_MARKER_VALUE = 2^25-1 -- marker numbers 0 to 24 have no pre-defined function
out:SetFont(ide.font.oNormal)
out:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.font.oNormal)
out:SetBufferedDraw(not ide.config.hidpi and true or false)
out:StyleClearAll()
out:SetTabWidth(ide.config.editor.tabwidth or 2)
out:SetIndent(ide.config.editor.tabwidth or 2)
out:SetUseTabs(ide.config.editor.usetabs and true or false)
out:SetViewWhiteSpace(ide.config.editor.whitespace and true or false)
out:SetIndentationGuides(true)
out:SetWrapMode(wxstc.wxSTC_WRAP_WORD)
out:SetWrapStartIndent(0)
out:SetWrapVisualFlagsLocation(wxstc.wxSTC_WRAPVISUALFLAGLOC_END_BY_TEXT)
out:SetWrapVisualFlags(wxstc.wxSTC_WRAPVISUALFLAG_END)
out:MarkerDefine(StylesGetMarker("prompt"))
out:MarkerDefine(StylesGetMarker("error"))
out:MarkerDefine(StylesGetMarker("output"))
out:MarkerDefine(StylesGetMarker("message"))
out:SetReadOnly(false)
SetupKeywords(out,"lua",nil,ide.config.stylesoutshell,ide.font.oNormal,ide.font.oItalic)
local function getPromptLine()
local totalLines = out:GetLineCount()
return out:MarkerPrevious(totalLines+1, PROMPT_MARKER_VALUE)
end
local function getPromptText()
local prompt = getPromptLine()
return out:GetTextRangeDyn(out:PositionFromLine(prompt), out:GetLength())
end
local function setPromptText(text)
local length = out:GetLength()
out:SetSelectionStart(length - string.len(getPromptText()))
out:SetSelectionEnd(length)
out:ClearAny()
out:AddTextDyn(text)
-- refresh the output window to force recalculation of wrapped lines;
-- otherwise a wrapped part of the last line may not be visible.
out:Update(); out:Refresh()
out:GotoPos(out:GetLength())
end
local function positionInLine(line)
return out:GetCurrentPos() - out:PositionFromLine(line)
end
local function caretOnPromptLine(disallowLeftmost, line)
local promptLine = getPromptLine()
local currentLine = line or out:GetCurrentLine()
local boundary = disallowLeftmost and 0 or -1
return (currentLine > promptLine
or currentLine == promptLine and positionInLine(promptLine) > boundary)
end
local function chomp(line) return (line:gsub("%s+$", "")) end
local function getInput(line)
local nextMarker = line
local count = out:GetLineCount()
repeat -- check until we find at least some marker
nextMarker = nextMarker+1
until out:MarkerGet(nextMarker) > 0 or nextMarker > count-1
return chomp(out:GetTextRangeDyn(
out:PositionFromLine(line), out:PositionFromLine(nextMarker)))
end
function ConsoleSelectCommand(point)
local cpos = out:ScreenToClient(point or wx.wxGetMousePosition())
local position = out:PositionFromPoint(cpos)
if position == wxstc.wxSTC_INVALID_POSITION then return end
local promptline = out:MarkerPrevious(out:LineFromPosition(position), PROMPT_MARKER_VALUE)
if promptline == wxstc.wxSTC_INVALID_POSITION then return end
local nextline = out:MarkerNext(promptline+1, ANY_MARKER_VALUE)
local epos = nextline ~= wxstc.wxSTC_INVALID_POSITION and out:PositionFromLine(nextline) or out:GetLength()
out:SetSelection(out:PositionFromLine(promptline), epos)
return true
end
local currentHistory
local function getNextHistoryLine(forward, promptText)
local count = out:GetLineCount()
if currentHistory == nil then currentHistory = count end
if forward then
currentHistory = out:MarkerNext(currentHistory+1, PROMPT_MARKER_VALUE)
if currentHistory == -1 then
currentHistory = count
return ""
end
else
currentHistory = out:MarkerPrevious(currentHistory-1, PROMPT_MARKER_VALUE)
if currentHistory == -1 then
return ""
end
end
-- need to skip the current prompt line
-- or skip repeated commands
if currentHistory == getPromptLine()
or getInput(currentHistory) == promptText then
return getNextHistoryLine(forward, promptText)
end
return getInput(currentHistory)
end
local function getNextHistoryMatch(promptText)
local count = out:GetLineCount()
if currentHistory == nil then currentHistory = count end
local current = currentHistory
while true do
currentHistory = out:MarkerPrevious(currentHistory-1, PROMPT_MARKER_VALUE)
if currentHistory == -1 then -- restart search from the last item
currentHistory = count
elseif currentHistory ~= getPromptLine() then -- skip current prompt
local input = getInput(currentHistory)
if input:find(promptText, 1, true) == 1 then return input end
end
-- couldn't find anything and made a loop; get out
if currentHistory == current then return end
end
assert(false, "getNextHistoryMatch coudn't find a proper match")
end
local function shellPrint(marker, ...)
local cnt = select('#',...)
if cnt == 0 then return end -- return if nothing to print
local isPrompt = marker and (getPromptLine() > -1)
local text = ''
for i=1,cnt do
local x = select(i,...)
text = text .. tostring(x)..(i < cnt and "\t" or "")
end
-- split the text into smaller chunks as one large line
-- is difficult to handle for the editor
local prev, maxlength = 0, ide.config.debugger.maxdatalength
if #text > maxlength and not text:find("\n.") then
text = text:gsub("()(%s+)", function(p, s)
if p-prev >= maxlength then
prev = p
return "\n"
else
return s
end
end)
end
-- add "\n" if it is missing
text = text:gsub("\n+$", "") .. "\n"
local lines = out:GetLineCount()
local promptLine = isPrompt and getPromptLine() or nil
local insertLineAt = isPrompt and getPromptLine() or out:GetLineCount()-1
local insertAt = isPrompt and out:PositionFromLine(getPromptLine()) or out:GetLength()
out:InsertTextDyn(insertAt, out.useraw and text or FixUTF8(text, function (s) return '\\'..string.byte(s) end))
local linesAdded = out:GetLineCount() - lines
if marker then
if promptLine then out:MarkerDelete(promptLine, PROMPT_MARKER) end
for line = insertLineAt, insertLineAt + linesAdded - 1 do
out:MarkerAdd(line, marker)
end
if promptLine then out:MarkerAdd(promptLine+linesAdded, PROMPT_MARKER) end
end
out:EmptyUndoBuffer() -- don't allow the user to undo shell text
out:GotoPos(out:GetLength())
out:EnsureVisibleEnforcePolicy(out:GetLineCount()-1)
end
DisplayShell = function (...)
shellPrint(OUTPUT_MARKER, ...)
end
DisplayShellErr = function (...)
shellPrint(ERROR_MARKER, ...)
end
DisplayShellMsg = function (...)
shellPrint(MESSAGE_MARKER, ...)
end
DisplayShellDirect = function (...)
shellPrint(nil, ...)
end
DisplayShellPrompt = function (...)
-- don't print anything; just mark the line with a prompt mark
out:MarkerAdd(out:GetLineCount()-1, PROMPT_MARKER)
end
local function filterTraceError(err, addedret)
local err = err:match("(.-:%d+:.-)\n[^\n]*\n[^\n]*\n[^\n]*src/editor/shellbox.lua:.*in function 'executeShellCode'")
or err
err = err:gsub("stack traceback:.-\n[^\n]+\n?","")
if addedret then err = err:gsub('^%[string "return ', '[string "') end
err = err:match("(.*)\n[^\n]*%(tail call%): %?$") or err
return err
end
local function createenv ()
local env = {}
setmetatable(env,{__index = _G})
local function luafilename(level)
level = level and level + 1 or 2
local src
while (true) do
src = debug.getinfo(level)
if (src == nil) then return nil,level end
if (string.byte(src.source) == string.byte("@")) then
return string.sub(src.source,2),level
end
level = level + 1
end
end
local function luafilepath(level)
local src,level = luafilename(level)
if (src == nil) then return src,level end
src = string.gsub(src,"[\\/][^\\//]*$","")
return src,level
end
local function relativeFilename(file)
assert(type(file)=='string',"String as filename expected")
local name = file
local level = 3
while (name) do
if (wx.wxFileName(name):FileExists()) then return name end
name,level = luafilepath(level)
if (name == nil) then break end
name = name .. "/" .. file
end
return file
end
local function relativeFilepath(file)
local name = luafilepath(3)
return (file and name) and name.."/"..file or file or name
end
local _loadfile = loadfile
local function loadfile(file)
assert(type(file)=='string',"String as filename expected")
local name = relativeFilename(file)
return _loadfile(name)
end
local function dofile(file, ...)
assert(type(file) == 'string',"String as filename expected")
local fn,err = loadfile(file)
local args = {...}
if not fn then
DisplayShellErr(err)
else
setfenv(fn,env)
return fn(unpack(args))
end
end
local os = { exit = function()
ide.frame:AddPendingEvent(wx.wxCommandEvent(
wx.wxEVT_COMMAND_MENU_SELECTED, ID_EXIT))
end }
env.os = setmetatable(os, {__index = _G.os})
env.print = DisplayShell
env.dofile = dofile
env.loadfile = loadfile
env.RELFILE = relativeFilename
env.RELPATH = relativeFilepath
return env
end
local env = createenv()
function ShellSetAlias(alias, table)
local value = env[alias]
env[alias] = table
return value
end
local function packResults(status, ...) return status, {...} end
local function executeShellCode(tx)
if tx == nil or tx == '' then return end
local forcelocalprefix = '^!'
local forcelocal = tx:find(forcelocalprefix)
tx = tx:gsub(forcelocalprefix, '')
DisplayShellPrompt('')
-- try to compile as statement
local _, err = loadstring(tx)
local isstatement = not err
if remotesend and not forcelocal then remotesend(tx, isstatement); return end
local addedret, forceexpression = true, tx:match("^%s*=%s*")
tx = tx:gsub("^%s*=%s*","")
local fn
fn, err = loadstring("return "..tx)
if not forceexpression and err then
fn, err = loadstring(tx)
addedret = false
end
if fn == nil and err then
DisplayShellErr(filterTraceError(err, addedret))
elseif fn then
setfenv(fn,env)
-- set the project dir as the current dir to allow "require" calls
-- to work from shell
local projectDir, cwd = FileTreeGetDir(), nil
if projectDir and #projectDir > 0 then
cwd = wx.wxFileName.GetCwd()
wx.wxFileName.SetCwd(projectDir)
end
local ok, res = packResults(xpcall(fn,
function(err)
DisplayShellErr(filterTraceError(debug.traceback(err), addedret))
end))
-- restore the current dir
if cwd then wx.wxFileName.SetCwd(cwd) end
if ok and (addedret or #res > 0) then
if addedret then
local mobdebug = require "mobdebug"
for i,v in pairs(res) do -- stringify each of the returned values
res[i] = (forceexpression and i > 1 and '\n' or '') ..
mobdebug.line(v, {nocode = true, comment = 1,
-- if '=' is used, then use multi-line serialized output
indent = forceexpression and ' ' or nil})
end
-- add nil only if we are forced (using =) or if this is not a statement
-- this is needed to print 'nil' when asked for 'foo',
-- and don't print it when asked for 'print(1)'
if #res == 0 and (forceexpression or not isstatement) then
res = {'nil'}
end
end
DisplayShell(unpack(res))
end
end
end
function ShellSupportRemote(client)
remotesend = client
local index = bottomnotebook:GetPageIndex(out)
if index then
bottomnotebook:SetPageText(index,
client and TR("Remote console") or TR("Local console"))
end
end
function ShellExecuteFile(wfilename)
if (not wfilename) then return end
local cmd = 'dofile([['..wfilename:GetFullPath()..']])'
ShellExecuteCode(cmd)
end
ShellExecuteInline = executeShellCode
function ShellExecuteCode(code)
local index = bottomnotebook:GetPageIndex(bottomnotebook.shellbox)
if ide.config.activateoutput and bottomnotebook:GetSelection() ~= index then
bottomnotebook:SetSelection(index)
end
DisplayShellDirect(code)
executeShellCode(code)
end
local function displayShellIntro()
DisplayShellMsg(TR("Welcome to the interactive Lua interpreter.").." "
..TR("Enter Lua code and press Enter to run it.").."\n"
..TR("Use Shift-Enter for multiline code.").." "
..TR("Use 'clear' to clear the shell output and the history.").."\n"
..TR("Prepend '=' to show complex values on multiple lines.").." "
..TR("Prepend '!' to force local execution."))
DisplayShellPrompt('')
end
out:Connect(wx.wxEVT_KEY_DOWN,
function (event)
-- this loop is only needed to allow to get to the end of function easily
-- "return" aborts the processing and ignores the key
-- "break" aborts the processing and processes the key normally
while true do
local key = event:GetKeyCode()
if key == wx.WXK_UP or key == wx.WXK_NUMPAD_UP then
-- if we are below the prompt line, then allow to go up
-- through multiline entry
if out:GetCurrentLine() > getPromptLine() then break end
-- if we are not on the caret line, move normally
if not caretOnPromptLine() then break end
local promptText = getPromptText()
setPromptText(getNextHistoryLine(false, promptText))
return
elseif key == wx.WXK_DOWN or key == wx.WXK_NUMPAD_DOWN then
-- if we are above the last line, then allow to go down
-- through multiline entry
local totalLines = out:GetLineCount()-1
if out:GetCurrentLine() < totalLines then break end
-- if we are not on the caret line, move normally
if not caretOnPromptLine() then break end
local promptText = getPromptText()
setPromptText(getNextHistoryLine(true, promptText))
return
elseif key == wx.WXK_TAB then
-- if we are above the prompt line, then don't move
local promptline = getPromptLine()
if out:GetCurrentLine() < promptline then return end
local promptText = getPromptText()
-- save the position in the prompt text to restore
local pos = out:GetCurrentPos()
local text = promptText:sub(1, positionInLine(promptline))
if #text == 0 then return end
-- find the next match and set the prompt text
local match = getNextHistoryMatch(text)
if match then
setPromptText(match)
-- restore the position to make it easier to find the next match
out:GotoPos(pos)
end
return
elseif key == wx.WXK_ESCAPE then
setPromptText("")
return
elseif key == wx.WXK_BACK then
if not caretOnPromptLine(true) then return end
elseif key == wx.WXK_DELETE or key == wx.WXK_NUMPAD_DELETE then
if not caretOnPromptLine()
or out:LineFromPosition(out:GetSelectionStart()) < getPromptLine() then
return
end
elseif key == wx.WXK_PAGEUP or key == wx.WXK_NUMPAD_PAGEUP
or key == wx.WXK_PAGEDOWN or key == wx.WXK_NUMPAD_PAGEDOWN
or key == wx.WXK_END or key == wx.WXK_NUMPAD_END
or key == wx.WXK_HOME or key == wx.WXK_NUMPAD_HOME
or key == wx.WXK_LEFT or key == wx.WXK_NUMPAD_LEFT
or key == wx.WXK_RIGHT or key == wx.WXK_NUMPAD_RIGHT
or key == wx.WXK_SHIFT or key == wx.WXK_CONTROL
or key == wx.WXK_ALT then
break
elseif key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER then
if not caretOnPromptLine()
or out:LineFromPosition(out:GetSelectionStart()) < getPromptLine() then
return
end
-- allow multiline entry for shift+enter
if caretOnPromptLine(true) and event:ShiftDown() then break end
local promptText = getPromptText()
if #promptText == 0 then return end -- nothing to execute, exit
if promptText == 'clear' then
out:Erase()
else
DisplayShellDirect('\n')
executeShellCode(promptText)
end
currentHistory = getPromptLine() -- reset history
return -- don't need to do anything else with return
else
-- move cursor to end if not already there
if not caretOnPromptLine() then
out:GotoPos(out:GetLength())
-- check if the selection starts before the prompt line and reset it
elseif out:LineFromPosition(out:GetSelectionStart()) < getPromptLine() then
out:GotoPos(out:GetLength())
out:SetSelection(out:GetSelectionEnd()+1,out:GetSelectionEnd())
end
end
break
end
event:Skip()
end)
local function inputEditable(line)
return caretOnPromptLine(false, line) and
not (out:LineFromPosition(out:GetSelectionStart()) < getPromptLine())
end
-- new Scintilla (3.2.1) changed the way markers move when the text is updated
-- ticket: http://sourceforge.net/p/scintilla/bugs/939/
-- discussion: https://groups.google.com/forum/?hl=en&fromgroups#!topic/scintilla-interest/4giFiKG4VXo
if ide.wxver >= "2.9.5" then
-- this is a workaround that stores a position of the last prompt marker
-- before insert and restores the same position after (as the marker)
-- could have moved if the text is added at the beginning of the line.
local promptAt
out:Connect(wxstc.wxEVT_STC_MODIFIED,
function (event)
local evtype = event:GetModificationType()
if bit.band(evtype, wxstc.wxSTC_MOD_BEFOREINSERT) ~= 0 then
local promptLine = getPromptLine()
if promptLine and event:GetPosition() == out:PositionFromLine(promptLine)
then promptAt = promptLine end
end
if bit.band(evtype, wxstc.wxSTC_MOD_INSERTTEXT) ~= 0 then
local promptLine = getPromptLine()
if promptLine and promptAt then
out:MarkerDelete(promptLine, PROMPT_MARKER)
out:MarkerAdd(promptAt, PROMPT_MARKER)
promptAt = nil
end
end
end)
end
out:Connect(wxstc.wxEVT_STC_UPDATEUI,
function (event) out:SetReadOnly(not inputEditable()) end)
-- only allow copy/move text by dropping to the input line
out:Connect(wxstc.wxEVT_STC_DO_DROP,
function (event)
if not inputEditable(out:LineFromPosition(event:GetPosition())) then
event:SetDragResult(wx.wxDragNone)
end
end)
if ide.config.outputshell.nomousezoom then
-- disable zoom using mouse wheel as it triggers zooming when scrolling
-- on OSX with kinetic scroll and then pressing CMD.
out:Connect(wx.wxEVT_MOUSEWHEEL,
function (event)
if wx.wxGetKeyState(wx.WXK_CONTROL) then return end
event:Skip()
end)
end
displayShellIntro()
function out:Erase()
self:ClearAll()
displayShellIntro()
end

View File

@ -0,0 +1,98 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
local ide = ide
--[[ single instance
open an UDP port - if it fails it is either because
- IDE is running already
- an application is already blocking that port
if it fails it tries to contact the running application
- if it confirms being the IDE we let that instance open it, finish our application
- otherwise we throw an error message on the user and start like normal
probably a pitfal: an instance is running but is not visible
(because it was finished though the UDP thing still runs)
]]
if not ide.config.singleinstance then return end
local socket = require "socket"
local port = ide.config.singleinstanceport
local delay = tonumber(ide.config.singleinstance) or 1000 -- in ms
local svr = socket.udp()
local success = svr:setsockname("127.0.0.1",port) -- bind on local host
local protocol = {client = {}, server = {}}
protocol.client.greeting = "Is this you, my IDE? It's me, a new instance."
protocol.server.greeting = "Yes it is me, running as: %s"
protocol.client.requestloading = "Could you please load this file for me: %s"
protocol.server.answerok = "Sure. You may now leave."
if success then -- ok, server was started, we are solo
--TODO: if multiple files are to be opened, each file is handled one by one - we could create a single string instead...
ide.idletimer = wx.wxTimer(wx.wxGetApp())
ide.idletimer:Start(delay,false)
svr:settimeout(0) -- don't block
wx.wxGetApp():Connect(wx.wxEVT_TIMER, function()
if ide.exitingProgram then -- if exiting, terminate the timer loop
wx.wxGetApp():Disconnect(wx.wxEVT_TIMER)
return
end
local msg, ip, port = svr:receivefrom()
if msg then
if msg == protocol.client.greeting then
svr:sendto(protocol.server.greeting:format(wx.wxGetUserName()),ip,port)
elseif msg:match(protocol.client.requestloading:gsub("%%s",".+$")) then -- ok we need to open something
svr:sendto(protocol.server.answerok,ip,port)
local filename = msg:match(protocol.client.requestloading:gsub("%%s","(.+)$"))
if filename then
RequestAttention()
if wx.wxDirExists(filename) then
ProjectUpdateProjectDir(filename)
elseif not ActivateFile(filename) then
DisplayOutputLn(TR("Can't open file '%s': %s"):format(filename, wx.wxSysErrorMsg()))
end
end
end
end
end)
else -- something different is running on our port
local cln = socket.udp()
cln:setpeername("127.0.0.1",port)
cln:settimeout(2)
cln:send(protocol.client.greeting)
local msg = cln:receive()
local arg = ide.arg
if msg and msg:match(protocol.server.greeting:gsub("%%s",".+$")) then
local username = msg:match(protocol.server.greeting:gsub("%%s","(.+)$"))
if username ~= wx.wxGetUserName() then
print(("Another instance is running under user '%s' and can't be activated. This instance will continue running, which may cause interference with the debugger."):format(username))
else
local failed = false
for index = 2, #arg do
local fileName = arg[index]
if fileName ~= "--"
-- on OSX, the command line includes -psn parameter, so ignore it
and (ide.osname ~= 'Macintosh' or not fileName:find("^-psn")) then
cln:send(protocol.client.requestloading:format(fileName))
local msg, err = cln:receive()
if msg ~= protocol.server.answerok then
failed = true
print(err,msg)
end
end
end
if failed then
print("The server instance failed to open the files, this instance will continue running.")
else -- done
os.exit(0)
end
end
else
print("The single instance communication has failed; there may be another instance running, which may cause interference with the debugger.")
end
end

View File

@ -0,0 +1,441 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
----------
-- Style
--
-- common style attributes
-- ---------------------------
-- fg foreground - {r,g,b} 0-255
-- bg background - {r,g,b} 0-255
-- alpha translucency - 0-255 (0 - transparent, 255 - opaque, 256 - opaque/faster)
-- sel color of the selected block - {r,g,b} 0-255 (only applies to folds)
-- u underline - boolean
-- b bold - boolean
-- i italic - boolean
-- fill fill to end - boolean
-- fn font Face Name - string ("Lucida Console")
-- fs font size - number (11)
-- hs turn hotspot on - true or {r,g,b} 0-255
-- v visibility for symbols of the current style - boolean
local unpack = table.unpack or unpack
function StylesGetDefault()
return {
-- lexer specific (inherit fg/bg from text)
lexerdef = {fg = {160, 160, 160}},
comment = {fg = {128, 128, 128}},
stringtxt = {fg = {128, 32, 16}},
stringeol = {fg = {128, 32, 16}, bg = {224, 192, 224}, fill = true},
preprocessor = {fg = {128, 128, 0}},
operator = {fg = {64, 64, 64}},
number = {fg = {80, 112, 255}},
keywords0 = {fg = {32, 32, 192}},
keywords1 = {fg = {127, 32, 96}},
keywords2 = {fg = {32, 127, 96}},
keywords3 = {fg = {64, 32, 96}},
keywords4 = {fg = {127, 0, 95}},
keywords5 = {fg = {35, 95, 175}},
keywords6 = {fg = {0, 127, 127}},
keywords7 = {fg = {240, 255, 255}},
-- common (inherit fg/bg from text)
text = {fg = {64, 64, 64}, bg = {250, 250, 250}},
linenumber = {fg = {128, 128, 128}, bg = {250, 250, 250}},
bracematch = {fg = {32, 128, 255}, b = true},
bracemiss = {fg = {255, 128, 32}, b = true},
ctrlchar = nil,
indent = {fg = {192, 192, 230}, bg = {255, 255, 255}},
calltip = nil,
-- common special (need custom fg & bg)
sel = {bg = {208, 208, 208}},
caret = {fg = {0, 0, 0}},
caretlinebg = {bg = {240, 240, 230}},
fold = {fg = {192, 192, 192}, bg = {250, 250, 250}, sel = {160, 128, 224}},
whitespace = nil,
edge = {},
-- deprecated; allowed for backward compatibility in case someone does
-- fncall.fg = {...}
fncall = {},
-- markup
['|'] = {fg = {127, 0, 127}},
['`'] = {fg = {64, 128, 64}},
['['] = {hs = {32, 32, 127}},
-- markers
marker = {
currentline = {},
breakpoint = {},
message = {},
output = {},
prompt = {},
error = {},
searchmatchfile = {},
},
-- indicators
indicator = {
fncall = {},
varlocal = {},
varglobal = {},
varmasking = {},
varmasked = {},
searchmatch = {},
},
}
end
local markers = {
breakpoint = {0, wxstc.wxSTC_MARK_CIRCLE, wx.wxColour(196, 64, 64), wx.wxColour(220, 64, 64)},
bookmark = {1, wxstc.wxSTC_MARK_SHORTARROW, wx.wxColour(16, 96, 128), wx.wxColour(96, 160, 220)},
currentline = {2, wxstc.wxSTC_MARK_ARROW, wx.wxColour(16, 128, 16), wx.wxColour(64, 220, 64)},
message = {3, wxstc.wxSTC_MARK_CHARACTER+(' '):byte(), wx.wxBLACK, wx.wxColour(220, 220, 220)},
output = {4, wxstc.wxSTC_MARK_BACKGROUND, wx.wxBLACK, wx.wxColour(240, 240, 240)},
prompt = {5, wxstc.wxSTC_MARK_ARROWS, wx.wxBLACK, wx.wxColour(220, 220, 220)},
error = {6, wxstc.wxSTC_MARK_BACKGROUND, wx.wxBLACK, wx.wxColour(255, 220, 220)},
searchmatchfile = {7, wxstc.wxSTC_MARK_EMPTY, wx.wxBLACK, wx.wxColour(196, 0, 0)},
}
function StylesGetMarker(marker) return unpack(markers[marker] or {}) end
function StylesRemoveMarker(marker) markers[marker] = nil end
function StylesAddMarker(marker, ch, fg, bg)
local num = (markers[marker] or {})[1]
if not num then -- new marker; find the smallest available marker number
local nums = {}
for _, mark in pairs(markers) do nums[mark[1]] = true end
num = #nums + 1
if num > 24 then return end -- 24 markers with no pre-defined functions
end
markers[marker] = {num, ch, wx.wxColour(unpack(fg)), wx.wxColour(unpack(bg))}
return num
end
local function applymarker(editor,marker,clrfg,clrbg,clrsel)
if (clrfg) then editor:MarkerSetForeground(marker,clrfg) end
if (clrbg) then editor:MarkerSetBackground(marker,clrbg) end
if (ide.wxver >= "2.9.5" and clrsel) then editor:MarkerSetBackgroundSelected(marker,clrsel) end
end
local specialmapping = {
sel = function(editor,style)
if (style.fg) then
editor:SetSelForeground(1,wx.wxColour(unpack(style.fg)))
else
editor:SetSelForeground(0,wx.wxWHITE)
end
if (style.bg) then
editor:SetSelBackground(1,wx.wxColour(unpack(style.bg)))
else
editor:SetSelBackground(0,wx.wxWHITE)
end
if (style.alpha and ide.wxver >= "2.9.5") then
editor:SetSelAlpha(style.alpha)
end
-- set alpha for additional selecton: 0 - transparent, 255 - opaque
if ide.wxver >= "2.9.5" then editor:SetAdditionalSelAlpha(127) end
end,
seladd = function(editor,style)
if ide.wxver >= "2.9.5" then
if (style.fg) then
editor:SetAdditionalSelForeground(wx.wxColour(unpack(style.fg)))
end
if (style.bg) then
editor:SetAdditionalSelBackground(wx.wxColour(unpack(style.bg)))
end
if (style.alpha) then
editor:SetAdditionalSelAlpha(style.alpha)
end
end
end,
caret = function(editor,style)
if (style.fg) then
editor:SetCaretForeground(wx.wxColour(unpack(style.fg)))
end
end,
caretlinebg = function(editor,style)
if (style.bg) then
editor:SetCaretLineBackground(wx.wxColour(unpack(style.bg)))
end
if (style.alpha and ide.wxver >= "2.9.5") then
editor:SetCaretLineBackAlpha(style.alpha)
end
end,
whitespace = function(editor,style)
if (style.fg) then
editor:SetWhitespaceForeground(1,wx.wxColour(unpack(style.fg)))
else
--editor:SetWhitespaceForeground(0)
end
if (style.bg) then
editor:SetWhitespaceBackground(1,wx.wxColour(unpack(style.bg)))
else
--editor:SetWhitespaceBackground(0)
end
end,
fold = function(editor,style)
local clrfg = style.fg and wx.wxColour(unpack(style.fg))
local clrbg = style.bg and wx.wxColour(unpack(style.bg))
local clrhi = style.hi and wx.wxColour(unpack(style.hi))
local clrsel = style.sel and wx.wxColour(unpack(style.sel))
-- if selected background is set then enable support for it
if ide.wxver >= "2.9.5" and clrsel then editor:MarkerEnableHighlight(true) end
if (clrfg or clrbg or clrsel) then
-- foreground and background are defined as opposite to what I'd expect
-- for fold markers in Scintilla's terminilogy:
-- background is the color of fold lines/boxes and foreground is the color
-- of everything around fold lines or inside fold boxes.
-- in the following code fg and bg are simply reversed
local clrfg, clrbg = clrbg, clrfg
applymarker(editor,wxstc.wxSTC_MARKNUM_FOLDEROPEN, clrfg, clrbg, clrsel)
applymarker(editor,wxstc.wxSTC_MARKNUM_FOLDER, clrfg, clrbg, clrsel)
applymarker(editor,wxstc.wxSTC_MARKNUM_FOLDERSUB, clrfg, clrbg, clrsel)
applymarker(editor,wxstc.wxSTC_MARKNUM_FOLDERTAIL, clrfg, clrbg, clrsel)
applymarker(editor,wxstc.wxSTC_MARKNUM_FOLDEREND, clrfg, clrbg, clrsel)
applymarker(editor,wxstc.wxSTC_MARKNUM_FOLDEROPENMID, clrfg, clrbg, clrsel)
applymarker(editor,wxstc.wxSTC_MARKNUM_FOLDERMIDTAIL, clrfg, clrbg, clrsel)
end
if clrbg then
-- the earlier calls only color the actual markers, but not the
-- overall fold background; SetFoldMargin calls below do this.
-- http://community.activestate.com/forum-topic/fold-margin-colors
-- http://www.scintilla.org/ScintillaDoc.html#SCI_SETFOLDMARGINCOLOUR
editor:SetFoldMarginColour(true, clrbg)
editor:SetFoldMarginHiColour(true, clrbg)
end
if clrhi then
editor:SetFoldMarginHiColour(true, clrhi)
end
end,
edge = function(editor,style)
if style.fg or style.col or style.mode then
editor:SetEdgeColour(wx.wxColour(unpack(style.fg or {220, 220, 220})))
editor:SetEdgeMode(style.mode or wxstc.wxSTC_EDGE_LINE)
editor:SetEdgeColumn(style.col or 80)
end
end,
marker = function(editor,markers)
for m, style in pairs(markers) do
local id, ch, fg, bg = StylesGetMarker(m)
if style.ch then ch = style.ch end
if style.fg then fg = wx.wxColour(unpack(style.fg)) end
if style.bg then bg = wx.wxColour(unpack(style.bg)) end
editor:MarkerDefine(id, ch, fg, bg)
end
end,
auxwindow = function(editor,style)
if not style then return end
-- don't color toolbars as they have their own color/style
local skipcolor = {wxAuiToolBar = true, wxToolBar = true}
local default = wxstc.wxSTC_STYLE_DEFAULT
local bg = style.bg and wx.wxColour(unpack(style.bg)) or editor:StyleGetBackground(default)
local fg = style.fg and wx.wxColour(unpack(style.fg)) or editor:StyleGetForeground(default)
local uimgr = ide.frame.uimgr
local panes = uimgr:GetAllPanes()
for index = 0, panes:GetCount()-1 do
local wind = uimgr:GetPane(panes:Item(index).name).window
-- wxlua compiled with STL doesn't provide GetChildren() method
-- as per http://sourceforge.net/p/wxlua/mailman/message/32500995/
local ok, children = pcall(function() return wind:GetChildren() end)
if not ok then break end
for child = 0, children:GetCount()-1 do
local data = children:Item(child):GetData()
local _, window = pcall(function() return data:DynamicCast("wxWindow") end)
if window and not skipcolor[window:GetClassInfo():GetClassName()] then
window:SetBackgroundColour(bg)
window:SetForegroundColour(fg)
window:Refresh()
end
end
end
end,
}
local defaultmapping = {
text = wxstc.wxSTC_STYLE_DEFAULT,
linenumber = wxstc.wxSTC_STYLE_LINENUMBER,
bracematch = wxstc.wxSTC_STYLE_BRACELIGHT,
bracemiss = wxstc.wxSTC_STYLE_BRACEBAD,
ctrlchar = wxstc.wxSTC_STYLE_CONTROLCHAR,
indent = wxstc.wxSTC_STYLE_INDENTGUIDE,
calltip = wxstc.wxSTC_STYLE_CALLTIP,
}
function StylesApplyToEditor(styles,editor,font,fontitalic,lexerconvert)
local defaultfg = styles.text and styles.text.fg and wx.wxColour(unpack(styles.text.fg)) or nil
local defaultbg = styles.text and styles.text.bg and wx.wxColour(unpack(styles.text.bg)) or nil
local function applystyle(style,id)
editor:StyleSetFont(id, style.i and fontitalic or font)
editor:StyleSetBold(id, style.b or false)
editor:StyleSetUnderline(id, style.u or false)
editor:StyleSetEOLFilled(id, style.fill or false)
if style.fn then editor:StyleSetFaceName(id, style.fn) end
if style.fs then editor:StyleSetSize(id, style.fs) end
if style.v ~= nil then editor:StyleSetVisible(id, style.v) end
if style.hs then
editor:StyleSetHotSpot(id, 1)
-- if passed a color (table) as value, set it as foreground
if type(style.hs) == 'table' then
local color = wx.wxColour(unpack(style.hs))
editor:SetHotspotActiveForeground(1, color)
end
editor:SetHotspotActiveUnderline(1)
editor:SetHotspotSingleLine(1)
end
if (style.fg or defaultfg) then
editor:StyleSetForeground(id, style.fg and wx.wxColour(unpack(style.fg)) or defaultfg)
end
if (style.bg or defaultbg) then
editor:StyleSetBackground(id, style.bg and wx.wxColour(unpack(style.bg)) or defaultbg)
end
end
editor:StyleResetDefault()
editor:SetFont(font)
if (styles.text) then
applystyle(styles.text,defaultmapping["text"])
else
applystyle({},defaultmapping["text"])
end
editor:StyleClearAll()
-- set the default linenumber font size based on the editor font size
if styles.linenumber and not styles.linenumber.fs then
styles.linenumber.fs = ide.config.editor.fontsize and (ide.config.editor.fontsize - 1) or nil
end
for name,style in pairs(styles) do
if (specialmapping[name]) then
specialmapping[name](editor,style)
elseif (defaultmapping[name]) then
applystyle(style,defaultmapping[name])
end
if (lexerconvert and lexerconvert[name]) then
local targets = lexerconvert[name]
for _, outid in pairs(targets) do
applystyle(style,outid)
end
-- allow to specify style numbers, but exclude those styles
-- that may conflict with indicator numbers
elseif (style.st and style.st > 8 and style.st < wxstc.wxSTC_STYLE_DEFAULT) then
applystyle(style,style.st)
end
end
-- additional selection (seladd) attributes can only be set after
-- normal selection (sel) attributes are set, so handle them again
if styles.seladd then specialmapping.seladd(editor, styles.seladd) end
-- calltip has a special style that needs to be enabled
if styles.calltip then editor:CallTipUseStyle(2) end
do
local defaultfg = {127,127,127}
local indic = styles.indicator or {}
-- use styles.fncall if not empty and if indic.fncall is empty
-- for backward compatibility
if type(styles.fncall) == 'table' and next(styles.fncall)
and not (type(indic.fncall) == 'table' and next(indic.fncall)) then indic.fncall = styles.fncall end
local fncall = ide:AddIndicator("core.fncall")
local varlocal = ide:AddIndicator("core.varlocal")
local varglobal = ide:AddIndicator("core.varglobal")
local varmasking = ide:AddIndicator("core.varmasking")
local varmasked = ide:AddIndicator("core.varmasked")
local searchmatch = ide:AddIndicator("core.searchmatch")
editor:IndicatorSetStyle(fncall, indic.fncall and indic.fncall.st or ide.wxver >= "2.9.5" and wxstc.wxSTC_INDIC_ROUNDBOX or wxstc.wxSTC_INDIC_TT)
editor:IndicatorSetForeground(fncall, wx.wxColour(unpack(indic.fncall and indic.fncall.fg or {128, 128, 255})))
editor:IndicatorSetStyle(varlocal, indic.varlocal and indic.varlocal.st or wxstc.wxSTC_INDIC_DOTS or wxstc.wxSTC_INDIC_TT)
editor:IndicatorSetForeground(varlocal, wx.wxColour(unpack(indic.varlocal and indic.varlocal.fg or defaultfg)))
editor:IndicatorSetStyle(varglobal, indic.varglobal and indic.varglobal.st or wxstc.wxSTC_INDIC_PLAIN)
editor:IndicatorSetForeground(varglobal, wx.wxColour(unpack(indic.varglobal and indic.varglobal.fg or defaultfg)))
editor:IndicatorSetStyle(varmasking, indic.varmasking and indic.varmasking.st or wxstc.wxSTC_INDIC_DASH or wxstc.wxSTC_INDIC_DIAGONAL)
editor:IndicatorSetForeground(varmasking, wx.wxColour(unpack(indic.varmasking and indic.varmasking.fg or defaultfg)))
editor:IndicatorSetStyle(varmasked, indic.varmasked and indic.varmasked.st or wxstc.wxSTC_INDIC_STRIKE)
editor:IndicatorSetForeground(varmasked, wx.wxColour(unpack(indic.varmasked and indic.varmasked.fg or defaultfg)))
editor:IndicatorSetStyle(searchmatch, indic.searchmatch and indic.searchmatch.st or wxstc.wxSTC_INDIC_BOX)
editor:IndicatorSetForeground(searchmatch, wx.wxColour(unpack(indic.searchmatch and indic.searchmatch.fg or {196, 0, 0})))
end
end
function ReApplySpecAndStyles()
-- re-register markup styles as they are special:
-- these styles need to be updated as they are based on comment styles
if MarkupAddStyles then MarkupAddStyles(ide.config.styles) end
local errorlog = ide.frame.bottomnotebook.errorlog
local shellbox = ide.frame.bottomnotebook.shellbox
SetupKeywords(shellbox,"lua",nil,ide.config.stylesoutshell,ide.font.oNormal,ide.font.oItalic)
StylesApplyToEditor(ide.config.stylesoutshell,errorlog,ide.font.oNormal,ide.font.oItalic)
for _, doc in pairs(ide:GetDocuments()) do
if doc.editor.spec then doc.editor:SetupKeywords(nil, doc.editor.spec) end
end
end
function ApplyStyleConfig(config, style)
if not wx.wxIsAbsolutePath(config)
then config = MergeFullPath(GetPathWithSep(ide.editorFilename), config) end
local cfg = {wxstc = wxstc, math = math, print = DisplayOutputLn,
path = {}, editor = {}, view ={}, acandtip = {}, outputshell = {}, debugger={}}
local cfgfn, err = loadfile(config)
if not cfgfn then
DisplayOutputLn(TR("Error while loading configuration file: %s"):format(err))
return
end
setfenv(cfgfn,cfg)
cfgfn, err = pcall(cfgfn,style)
if not cfgfn then
DisplayOutputLn(TR("Error while processing configuration file: %s"):format(err))
return
end
-- if no style assigned explicitly, but a table is returned, use it
if not (cfg.styles or cfg.stylesoutshell) and type(err) == 'table' then
cfg.styles = err
end
if cfg.styles or cfg.stylesoutshell then
if (cfg.styles) then
ide.config.styles = StylesGetDefault()
-- copy
for i,s in pairs(cfg.styles) do
ide.config.styles[i] = s
end
end
if (cfg.stylesoutshell) then
ide.config.stylesoutshell = StylesGetDefault()
-- copy
for i,s in pairs(cfg.stylesoutshell) do
ide.config.stylesoutshell[i] = s
end
end
ReApplySpecAndStyles()
end
end

View File

@ -0,0 +1,55 @@
-- Copyright 2014-15 Paul Kulchenko, ZeroBrane LLC
local TR = function(...) return ... end
ide.config.toolbar.icons = {
ID.NEW, ID.OPEN, ID.SAVE, ID.SAVEALL, ID.PROJECTDIRFROMFILE, ID.PROJECTDIRCHOOSE,
ID.SEPARATOR,
ID.FIND, ID.REPLACE, ID.FINDINFILES,
ID.SEPARATOR,
ID.RUN, ID.STARTDEBUG, ID.RUNNOW, ID.STOPDEBUG, ID.DETACHDEBUG, ID.BREAK,
ID.STEP, ID.STEPOVER, ID.STEPOUT, ID.RUNTO,
ID.SEPARATOR,
ID.BREAKPOINTTOGGLE, ID.BOOKMARKTOGGLE, ID.VIEWCALLSTACK, ID.VIEWWATCHWINDOW,
[ID.FINDINFILES] = false,
}
ide.config.toolbar.iconmap = {
[ID.NEW] = {"FILE-NEW", TR("Create an empty document")},
[ID.OPEN] = {"FILE-OPEN", TR("Open an existing document")},
[ID.SAVE] = {"FILE-SAVE", TR("Save the current document")},
[ID.SAVEALL] = {"FILE-SAVE-ALL", TR("Save all open documents")},
[ID.PROJECTDIRFROMFILE]= {"DIR-SETUP-FILE", TR("Set project directory from current file")},
[ID.PROJECTDIRCHOOSE] = {"DIR-SETUP", TR("Choose a project directory")},
[ID.FIND] = {"FIND", TR("Find text")},
[ID.REPLACE] = {"FIND-AND-REPLACE", TR("Find and replace text")},
[ID.FINDINFILES] = {"FIND-IN-FILES", TR("Find in files")},
[ID.RUN] = {"RUN", TR("Execute the current project/file")},
[ID.RUNNOW] = {"RUN-NOW", TR("Run as Scratchpad")},
[ID.STARTDEBUG] = {"DEBUG-START", TR("Start or continue debugging")},
[ID.STOPDEBUG] = {"DEBUG-STOP", TR("Stop the currently running process")},
[ID.DETACHDEBUG]= {"DEBUG-DETACH", TR("Stop debugging and continue running the process")},
[ID.BREAK] = {"DEBUG-BREAK", TR("Break execution at the next executed line of code")},
[ID.RUNTO] = {"DEBUG-RUN-TO", TR("Run to cursor")},
[ID.STEP] = {"DEBUG-STEP-INTO", TR("Step into")},
[ID.STEPOVER] = {"DEBUG-STEP-OVER", TR("Step over")},
[ID.STEPOUT] = {"DEBUG-STEP-OUT", TR("Step out of the current function")},
[ID.BREAKPOINTTOGGLE] = {"DEBUG-BREAKPOINT-TOGGLE", TR("Toggle breakpoint")},
[ID.BOOKMARKTOGGLE] = {"BOOKMARK-TOGGLE", TR("Toggle bookmark")},
[ID.VIEWCALLSTACK] = {"DEBUG-CALLSTACK", TR("View the stack window")},
[ID.VIEWWATCHWINDOW] = {"DEBUG-WATCH", TR("View the watch window")},
-- search toolbar
[ID.FINDNEXT] = {"FIND", TR("Find")},
[ID.FINDREPLACENEXT] = {"FIND-REPLACE-NEXT", TR("Replace next instance")},
[ID.FINDREPLACEALL] = {"FIND-AND-REPLACE", TR("Replace all")},
[ID.FINDSETDIR] = {"FIND-OPT-SETDIR", TR("Set search directory")},
[ID.FINDOPTDIRECTION] = {"FIND-OPT-DOWN", TR("Search direction")},
[ID.FINDOPTWRAPWROUND] = {"FIND-OPT-WRAP-AROUND", TR("Wrap around")},
[ID.FINDOPTSELECTION] = {"FIND-OPT-SELECTION", TR("Search in selection")},
[ID.FINDOPTWORD] = {"FIND-OPT-WORD", TR("Match whole word")},
[ID.FINDOPTCASE] = {"FIND-OPT-CASE-SENSITIVE", TR("Match case")},
[ID.FINDOPTREGEX] = {"FIND-OPT-REGEX", TR("Regular expression")},
[ID.FINDOPTCONTEXT] = {"FIND-OPT-CONTEXT", TR("Show context")},
[ID.FINDOPTSUBDIR] = {"FIND-OPT-SUBDIR", TR("Search in subdirectories")},
[ID.FINDOPTMULTIRESULTS] = {"FIND-OPT-MULTI-RESULTS", TR("Show multiple result windows")},
}

View File

@ -0,0 +1,774 @@
-- Copyright 2011-15 Paul Kulchenko, ZeroBrane LLC
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
---------------------------------------------------------
-- put bin/ and lualibs/ first to avoid conflicts with included modules
-- that may have other versions present somewhere else in path/cpath.
local function isproc()
local file = io.open("/proc")
if file then file:close() end
return file ~= nil
end
local iswindows = os.getenv('WINDIR') or (os.getenv('OS') or ''):match('[Ww]indows')
local islinux = not iswindows and isproc()
local arch = "x86" -- use 32bit by default
local unpack = table.unpack or unpack
if islinux then
local file = io.popen("uname -m")
if file then
local machine=file:read("*l")
local archtype= { x86_64="x64", armv7l="armhf" }
arch = archtype[machine] or "x86"
file:close()
end
end
package.cpath = (
iswindows and 'bin/?.dll;bin/clibs/?.dll;' or
islinux and ('bin/linux/%s/lib?.so;bin/linux/%s/clibs/?.so;'):format(arch,arch) or
--[[isosx]] 'bin/lib?.dylib;bin/clibs/?.dylib;')
.. package.cpath
package.path = 'lualibs/?.lua;lualibs/?/?.lua;lualibs/?/init.lua;lualibs/?/?/?.lua;lualibs/?/?/init.lua;'
.. package.path
require("wx")
require("bit")
require("mobdebug")
if jit and jit.on then jit.on() end -- turn jit "on" as "mobdebug" may turn it off for LuaJIT
dofile "src/util.lua"
-----------
-- IDE
--
local pendingOutput = {}
ide = {
MODPREF = "* ",
MAXMARGIN = 4,
config = {
path = {
projectdir = "",
app = nil,
},
editor = {
autoactivate = false,
foldcompact = true,
checkeol = true,
saveallonrun = false,
caretline = true,
showfncall = false,
autotabs = false,
usetabs = false,
tabwidth = 2,
usewrap = true,
wrapmode = wxstc.wxSTC_WRAP_WORD,
calltipdelay = 500,
smartindent = true,
fold = true,
autoreload = true,
indentguide = true,
backspaceunindent = true,
},
debugger = {
verbose = false,
hostname = nil,
port = nil,
runonstart = nil,
redirect = nil,
maxdatalength = 400,
maxdatanum = 400,
maxdatalevel = 3,
},
default = {
name = 'untitled',
fullname = 'untitled.lua',
interpreter = 'luadeb',
},
outputshell = {
usewrap = true,
},
filetree = {
mousemove = true,
},
outline = {
jumptocurrentfunction = true,
showanonymous = '~',
showcurrentfunction = true,
showcompact = false,
showflat = false,
showmethodindicator = false,
showonefile = false,
sort = false,
},
commandbar = {
prefilter = 250, -- number of records after which to apply filtering
maxitems = 30, -- max number of items to show
width = 0.35, -- <1 -- size in proportion to the app frame width; >1 -- size in pixels
showallsymbols = true,
},
staticanalyzer = {
infervalue = false, -- off by default as it's a slower mode
},
search = {
autocomplete = true,
contextlinesbefore = 2,
contextlinesafter = 2,
showaseditor = false,
zoom = 0,
autohide = false,
},
print = {
magnification = -3,
wrapmode = wxstc.wxSTC_WRAP_WORD,
colourmode = wxstc.wxSTC_PRINT_BLACKONWHITE,
header = "%S\t%D\t%p/%P",
footer = nil,
},
toolbar = {
icons = {},
iconmap = {},
},
keymap = {},
imagemap = {
['VALUE-MCALL'] = 'VALUE-SCALL',
},
messages = {},
language = "en",
styles = nil,
stylesoutshell = nil,
autocomplete = true,
autoanalyzer = true,
acandtip = {
shorttip = true,
nodynwords = true,
ignorecase = false,
symbols = true,
droprest = true,
strategy = 2,
width = 60,
maxlength = 450,
warning = true,
},
arg = {}, -- command line arguments
api = {}, -- additional APIs to load
format = { -- various formatting strings
menurecentprojects = "%f | %i",
apptitle = "%T - %F",
},
activateoutput = true, -- activate output/console on Run/Debug/Compile
unhidewindow = false, -- to unhide a gui window
projectautoopen = true,
autorecoverinactivity = 10, -- seconds
outlineinactivity = 0.250, -- seconds
markersinactivity = 0.500, -- seconds
symbolindexinactivity = 2, -- seconds
filehistorylength = 20,
projecthistorylength = 20,
bordersize = 2,
savebak = false,
singleinstance = false,
singleinstanceport = 0xe493,
showmemoryusage = false,
showhiddenfiles = false,
hidpi = false, -- HiDPI/Retina display support
hotexit = false,
-- file exclusion lists
excludelist = {".svn/", ".git/", ".hg/", "CVS/", "*.pyc", "*.pyo", "*.exe", "*.dll", "*.obj","*.o", "*.a", "*.lib", "*.so", "*.dylib", "*.ncb", "*.sdf", "*.suo", "*.pdb", "*.idb", ".DS_Store", "*.class", "*.psd", "*.db"},
binarylist = {"*.jpg", "*.jpeg", "*.png", "*.gif", "*.ttf", "*.tga", "*.dds", "*.ico", "*.eot", "*.pdf", "*.swf", "*.jar", "*.zip", ".gz", ".rar"},
},
specs = {
none = {
sep = "\1",
}
},
tools = {},
iofilters = {},
interpreters = {},
packages = {},
apis = {},
timers = {},
onidle = {},
proto = {}, -- prototypes for various classes
app = nil, -- application engine
interpreter = nil, -- current Lua interpreter
frame = nil, -- gui related
debugger = {}, -- debugger related info
filetree = nil, -- filetree
findReplace = nil, -- find & replace handling
settings = nil, -- user settings (window pos, last files..)
session = {
projects = {}, -- project configuration for the current session
lastupdated = nil, -- timestamp of the last modification in any of the editors
lastsaved = nil, -- timestamp of the last recovery information saved
},
-- misc
exitingProgram = false, -- are we currently exiting, ID_EXIT
infocus = nil, -- last component with a focus
editorApp = wx.wxGetApp(),
editorFilename = nil,
openDocuments = {},-- open notebook editor documents[winId] = {
-- editor = wxStyledTextCtrl,
-- index = wxNotebook page index,
-- filePath = full filepath, nil if not saved,
-- fileName = just the filename,
-- modTime = wxDateTime of disk file or nil,
-- isModified = bool is the document modified? }
ignoredFilesList = {},
font = {
eNormal = nil,
eItalic = nil,
oNormal = nil,
oItalic = nil,
fNormal = nil,
},
osname = wx.wxPlatformInfo.Get():GetOperatingSystemFamilyName(),
osarch = arch,
oshome = os.getenv("HOME") or (iswindows and os.getenv('HOMEDRIVE') and os.getenv('HOMEPATH')
and (os.getenv('HOMEDRIVE')..os.getenv('HOMEPATH'))),
wxver = string.match(wx.wxVERSION_STRING, "[%d%.]+"),
test = {}, -- local functions used for testing
Print = function(self, ...)
if DisplayOutputLn then
-- flush any pending output
while #pendingOutput > 0 do DisplayOutputLn(unpack(table.remove(pendingOutput, 1))) end
-- print without parameters can be used for flushing, so skip the printing
if select('#', ...) > 0 then DisplayOutputLn(...) end
return
end
pendingOutput[#pendingOutput + 1] = {...}
end,
}
-- add wx.wxMOD_RAW_CONTROL as it's missing in wxlua 2.8.12.3;
-- provide default for wx.wxMOD_CONTROL as it's missing in wxlua 2.8 that
-- is available through Linux package managers
if not wx.wxMOD_CONTROL then wx.wxMOD_CONTROL = 0x02 end
if not wx.wxMOD_RAW_CONTROL then
wx.wxMOD_RAW_CONTROL = ide.osname == 'Macintosh' and 0x10 or wx.wxMOD_CONTROL
end
-- ArchLinux running 2.8.12.2 doesn't have wx.wxMOD_SHIFT defined
if not wx.wxMOD_SHIFT then wx.wxMOD_SHIFT = 0x04 end
-- wxDIR_NO_FOLLOW is missing in wxlua 2.8.12 as well
if not wx.wxDIR_NO_FOLLOW then wx.wxDIR_NO_FOLLOW = 0x10 end
if not wxaui.wxAUI_TB_PLAIN_BACKGROUND then wxaui.wxAUI_TB_PLAIN_BACKGROUND = 2^8 end
if not setfenv then -- Lua 5.2
-- based on http://lua-users.org/lists/lua-l/2010-06/msg00314.html
-- this assumes f is a function
local function findenv(f)
local level = 1
repeat
local name, value = debug.getupvalue(f, level)
if name == '_ENV' then return level, value end
level = level + 1
until name == nil
return nil end
getfenv = function (f) return(select(2, findenv(f)) or _G) end
setfenv = function (f, t)
local level = findenv(f)
if level then debug.setupvalue(f, level, t) end
return f end
end
dofile "src/version.lua"
for _, file in ipairs({"proto", "ids", "style", "keymap", "toolbar"}) do
dofile("src/editor/"..file..".lua")
end
ide.config.styles = StylesGetDefault()
ide.config.stylesoutshell = StylesGetDefault()
local function setLuaPaths(mainpath, osname)
-- use LUA_DEV to setup paths for Lua for Windows modules if installed
local luadev = osname == "Windows" and os.getenv('LUA_DEV')
if luadev and not wx.wxDirExists(luadev) then luadev = nil end
local luadev_path = (luadev
and ('LUA_DEV/?.lua;LUA_DEV/?/init.lua;LUA_DEV/lua/?.lua;LUA_DEV/lua/?/init.lua')
:gsub('LUA_DEV', (luadev:gsub('[\\/]$','')))
or nil)
local luadev_cpath = (luadev
and ('LUA_DEV/?.dll;LUA_DEV/?51.dll;LUA_DEV/clibs/?.dll;LUA_DEV/clibs/?51.dll')
:gsub('LUA_DEV', (luadev:gsub('[\\/]$','')))
or nil)
if luadev then
local path, clibs = os.getenv('PATH'), luadev:gsub('[\\/]$','')..'\\clibs'
if not path:find(clibs, 1, true) then wx.wxSetEnv('PATH', path..';'..clibs) end
end
-- (luaconf.h) in Windows, any exclamation mark ('!') in the path is replaced
-- by the path of the directory of the executable file of the current process.
-- this effectively prevents any path with an exclamation mark from working.
-- if the path has an excamation mark, allow Lua to expand it as this
-- expansion happens only once.
if osname == "Windows" and mainpath:find('%!') then mainpath = "!/../" end
-- if LUA_PATH or LUA_CPATH is not specified, then add ;;
-- ;; will be replaced with the default (c)path by the Lua interpreter
wx.wxSetEnv("LUA_PATH",
(os.getenv("LUA_PATH") or ';') .. ';'
.. "./?.lua;./?/init.lua;./lua/?.lua;./lua/?/init.lua" .. ';'
.. mainpath.."lualibs/?/?.lua;"..mainpath.."lualibs/?.lua;"
.. mainpath.."lualibs/?/?/init.lua;"..mainpath.."lualibs/?/init.lua"
.. (luadev_path and (';' .. luadev_path) or ''))
ide.osclibs = -- keep the list to use for other Lua versions
osname == "Windows" and mainpath.."bin/?.dll;"..mainpath.."bin/clibs/?.dll" or
osname == "Macintosh" and mainpath.."bin/lib?.dylib;"..mainpath.."bin/clibs/?.dylib" or
osname == "Unix" and mainpath..("bin/linux/%s/lib?.so;"):format(arch)
..mainpath..("bin/linux/%s/clibs/?.so"):format(arch) or
assert(false, "Unexpected OS name")
wx.wxSetEnv("LUA_CPATH",
(os.getenv("LUA_CPATH") or ';') .. ';' .. ide.osclibs
.. (luadev_cpath and (';' .. luadev_cpath) or ''))
-- on some OSX versions, PATH is sanitized to not include even /usr/local/bin; add it
if osname == "Macintosh" then
local ok, path = wx.wxGetEnv("PATH")
if ok then wx.wxSetEnv("PATH", (#path > 0 and path..":" or "").."/usr/local/bin") end
end
end
ide.test.setLuaPaths = setLuaPaths
---------------
-- process args
local filenames = {}
local configs = {}
do
local arg = {...}
-- application name is expected as the first argument
local fullPath = arg[1] or "zbstudio"
ide.arg = arg
-- on Windows use GetExecutablePath, which is Unicode friendly,
-- whereas wxGetCwd() is not (at least in wxlua 2.8.12.2).
-- some wxlua version on windows report wx.dll instead of *.exe.
local exepath = wx.wxStandardPaths.Get():GetExecutablePath()
if ide.osname == "Windows" and exepath:find("%.exe$") then
fullPath = exepath
elseif not wx.wxIsAbsolutePath(fullPath) then
fullPath = wx.wxGetCwd().."/"..fullPath
end
ide.editorFilename = fullPath
ide.appname = fullPath:match("([%w_-%.]+)$"):gsub("%.[^%.]*$","")
assert(ide.appname, "no application path defined")
for index = 2, #arg do
if (arg[index] == "-cfg" and index+1 <= #arg) then
table.insert(configs,arg[index+1])
elseif arg[index-1] ~= "-cfg"
-- on OSX command line includes -psn... parameter, don't include these
and (ide.osname ~= 'Macintosh' or not arg[index]:find("^-psn")) then
table.insert(filenames,arg[index])
end
end
setLuaPaths(GetPathWithSep(ide.editorFilename), ide.osname)
end
----------------------
-- process application
ide.app = dofile(ide.appname.."/app.lua")
local app = assert(ide.app)
local function loadToTab(filter, folder, tab, recursive, proto)
if filter and type(filter) ~= 'function' then
filter = app.loadfilters[filter] or nil
end
for _, file in ipairs(FileSysGetRecursive(folder, recursive, "*.lua")) do
if not filter or filter(file) then
LoadLuaFileExt(tab, file, proto)
end
end
return tab
end
local function loadInterpreters(filter)
loadToTab(filter or "interpreters", "interpreters", ide.interpreters, false,
ide.proto.Interpreter)
end
-- load tools
local function loadTools(filter)
loadToTab(filter or "tools", "tools", ide.tools, false)
end
-- load packages
local function processPackages(packages)
-- check dependencies and assign file names to each package
local skip = {}
for fname, package in pairs(packages) do
if type(package.dependencies) == 'table'
and package.dependencies.osname
and not package.dependencies.osname:find(ide.osname, 1, true) then
ide:Print(("Package '%s' not loaded: requires %s platform, but you are running %s.")
:format(fname, package.dependencies.osname, ide.osname))
skip[fname] = true
end
local needsversion = tonumber(package.dependencies)
or type(package.dependencies) == 'table' and tonumber(package.dependencies[1])
or -1
local isversion = tonumber(ide.VERSION)
if isversion and needsversion > isversion then
ide:Print(("Package '%s' not loaded: requires version %s, but you are running version %s.")
:format(fname, needsversion, ide.VERSION))
skip[fname] = true
end
package.fname = fname
end
for fname, package in pairs(packages) do
if not skip[fname] then ide.packages[fname] = package end
end
end
function UpdateSpecs()
for _, spec in pairs(ide.specs) do
spec.sep = spec.sep or "\1" -- default separator doesn't match anything
spec.iscomment = {}
spec.iskeyword0 = {}
spec.isstring = {}
if (spec.lexerstyleconvert) then
if (spec.lexerstyleconvert.comment) then
for _, s in pairs(spec.lexerstyleconvert.comment) do
spec.iscomment[s] = true
end
end
if (spec.lexerstyleconvert.keywords0) then
for _, s in pairs(spec.lexerstyleconvert.keywords0) do
spec.iskeyword0[s] = true
end
end
if (spec.lexerstyleconvert.stringtxt) then
for _, s in pairs(spec.lexerstyleconvert.stringtxt) do
spec.isstring[s] = true
end
end
end
end
end
-- load specs
local function loadSpecs(filter)
loadToTab(filter or "specs", "spec", ide.specs, true)
UpdateSpecs()
end
function GetIDEString(keyword, default)
return app.stringtable[keyword] or default or keyword
end
----------------------
-- process config
-- set ide.config environment
do
ide.configs = {
system = MergeFullPath("cfg", "user.lua"),
user = ide.oshome and MergeFullPath(ide.oshome, "."..ide.appname.."/user.lua"),
}
ide.configqueue = {}
local num = 0
local package = setmetatable({}, {
__index = function(_,k) return package[k] end,
__newindex = function(_,k,v) package[k] = v end,
__call = function(_,p)
-- package can be defined inline, like "package {...}"
if type(p) == 'table' then
num = num + 1
local name = 'config'..num..'package'
ide.packages[name] = setmetatable(p, ide.proto.Plugin)
-- package can be included as "package 'file.lua'" or "package 'folder/'"
elseif type(p) == 'string' then
local config = ide.configqueue[#ide.configqueue]
local pkg
for _, packagepath in ipairs({'.', 'packages/', '../packages/'}) do
local p = config and MergeFullPath(config.."/../"..packagepath, p)
pkg = wx.wxDirExists(p) and loadToTab(nil, p, {}, false, ide.proto.Plugin)
or wx.wxFileExists(p) and LoadLuaFileExt({}, p, ide.proto.Plugin)
or wx.wxFileExists(p..".lua") and LoadLuaFileExt({}, p..".lua", ide.proto.Plugin)
if pkg then
processPackages(pkg)
break
end
end
if not pkg then ide:Print(("Can't find '%s' to load package from."):format(p)) end
else
ide:Print(("Can't load package based on parameter of type '%s'."):format(type(p)))
end
end,
})
local includes = {}
local include = function(c)
if c then
for _, config in ipairs({ide.configqueue[#ide.configqueue], ide.configs.user, ide.configs.system}) do
local p = config and MergeFullPath(config.."/../", c)
includes[p] = (includes[p] or 0) + 1
if includes[p] > 1 or LoadLuaConfig(p) or LoadLuaConfig(p..".lua") then return end
includes[p] = includes[p] - 1
end
ide:Print(("Can't find configuration file '%s' to process."):format(c))
end
end
setmetatable(ide.config, {
__index = setmetatable({
load = {interpreters = loadInterpreters, specs = loadSpecs, tools = loadTools},
package = package,
include = include,
}, {__index = _G or _ENV})
})
end
LoadLuaConfig(ide.appname.."/config.lua")
ide.editorApp:SetAppName(GetIDEString("settingsapp"))
-- check if the .ini file needs to be migrated on Windows
if ide.osname == 'Windows' and ide.wxver >= "2.9.5" then
-- Windows used to have local ini file kept in wx.wxGetHomeDir (before 2.9),
-- but since 2.9 it's in GetUserConfigDir(), so migrate it.
local ini = ide.editorApp:GetAppName() .. ".ini"
local old = wx.wxFileName(wx.wxGetHomeDir(), ini)
local new = wx.wxFileName(wx.wxStandardPaths.Get():GetUserConfigDir(), ini)
if old:FileExists() and not new:FileExists() then
FileCopy(old:GetFullPath(), new:GetFullPath())
ide:Print(("Migrated configuration file from '%s' to '%s'.")
:format(old:GetFullPath(), new:GetFullPath()))
end
end
----------------------
-- process plugins
if app.preinit then app.preinit() end
loadInterpreters()
loadSpecs()
loadTools()
do
-- process configs
LoadLuaConfig(ide.configs.system)
LoadLuaConfig(ide.configs.user)
-- process all other configs (if any)
for _, v in ipairs(configs) do LoadLuaConfig(v, true) end
configs = nil
-- check and apply default styles in case a user resets styles in the config
for _, styles in ipairs({"styles", "stylesoutshell"}) do
if not ide.config[styles] then
ide:Print(("Ignored incorrect value of '%s' setting in the configuration file")
:format(styles))
ide.config[styles] = StylesGetDefault()
end
end
local sep = GetPathSeparator()
if ide.config.language then
LoadLuaFileExt(ide.config.messages, "cfg"..sep.."i18n"..sep..ide.config.language..".lua")
end
-- always load 'en' as it's requires as a fallback for pluralization
if ide.config.language ~= 'en' then
LoadLuaFileExt(ide.config.messages, "cfg"..sep.."i18n"..sep.."en.lua")
end
end
processPackages(loadToTab(nil, "packages", {}, false, ide.proto.Plugin))
if ide.oshome then
local userpackages = MergeFullPath(ide.oshome, "."..ide.appname.."/packages")
if wx.wxDirExists(userpackages) then
processPackages(loadToTab(nil, userpackages, {}, false, ide.proto.Plugin))
end
end
---------------
-- Load App
for _, file in ipairs({
"settings", "singleinstance", "iofilters", "package", "markup",
"gui", "filetree", "output", "debugger", "outline", "commandbar",
"editor", "findreplace", "commands", "autocomplete", "shellbox", "markers",
"menu_file", "menu_edit", "menu_search",
"menu_view", "menu_project", "menu_tools", "menu_help",
"print", "inspect" }) do
dofile("src/editor/"..file..".lua")
end
-- register all the plugins
PackageEventHandle("onRegister")
-- initialization that was delayed until configs processed and packages loaded
ProjectUpdateInterpreters()
-- load rest of settings
SettingsRestoreFramePosition(ide.frame, "MainFrame")
SettingsRestoreView()
SettingsRestoreFileHistory(SetFileHistory)
SettingsRestoreEditorSettings()
SettingsRestoreProjectSession(FileTreeSetProjects)
SettingsRestoreFileSession(function(tabs, params)
if params and params.recovery
then return SetOpenTabs(params)
else return SetOpenFiles(tabs, params) end
end)
-- ---------------------------------------------------------------------------
-- Load the filenames
do
for _, filename in ipairs(filenames) do
if filename ~= "--" then
if wx.wxDirExists(filename) then
ProjectUpdateProjectDir(filename)
elseif not ActivateFile(filename) then
DisplayOutputLn(("Can't open file '%s': %s"):format(filename, wx.wxSysErrorMsg()))
end
end
end
if ide:GetEditorNotebook():GetPageCount() == 0 then NewFile() end
end
if app.postinit then app.postinit() end
-- this is a workaround for a conflict between global shortcuts and local
-- shortcuts (like F2) used in the file tree or a watch panel.
-- because of several issues on OSX (as described in details in this thread:
-- https://groups.google.com/d/msg/wx-dev/juJj_nxn-_Y/JErF1h24UFsJ),
-- the workaround installs a global event handler that manually re-routes
-- conflicting events when the current focus is on a proper object.
-- non-conflicting shortcuts are handled through key-down events.
local remap = {
[ID_ADDWATCH] = ide:GetWatch(),
[ID_EDITWATCH] = ide:GetWatch(),
[ID_DELETEWATCH] = ide:GetWatch(),
[ID_RENAMEFILE] = ide:GetProjectTree(),
[ID_DELETEFILE] = ide:GetProjectTree(),
}
local function rerouteMenuCommand(obj, id)
-- check if the conflicting shortcut is enabled:
-- (1) SetEnabled wasn't called or (2) Enabled was set to `true`.
local uievent = wx.wxUpdateUIEvent(id)
obj:ProcessEvent(uievent)
if not uievent:GetSetEnabled() or uievent:GetEnabled() then
obj:AddPendingEvent(wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, id))
end
end
local function remapkey(event)
local keycode = event:GetKeyCode()
local mod = event:GetModifiers()
for id, obj in pairs(remap) do
local focus = obj:FindFocus()
if focus and focus:GetId() == obj:GetId() then
local ae = wx.wxAcceleratorEntry(); ae:FromString(KSC(id))
if ae:GetFlags() == mod and ae:GetKeyCode() == keycode then
rerouteMenuCommand(obj, id)
return
end
end
end
event:Skip()
end
ide:GetWatch():Connect(wx.wxEVT_KEY_DOWN, remapkey)
ide:GetProjectTree():Connect(wx.wxEVT_KEY_DOWN, remapkey)
local function resolveConflict(localid, globalid)
return function(event)
local shortcut = ide.config.keymap[localid]
for id, obj in pairs(remap) do
if ide.config.keymap[id]:lower() == shortcut:lower() then
local focus = obj:FindFocus()
if focus and focus:GetId() == obj:GetId() then
obj:AddPendingEvent(wx.wxCommandEvent(wx.wxEVT_COMMAND_MENU_SELECTED, id))
return
-- also need to check for children of objects
-- to avoid re-triggering events when labels are being edited
elseif focus and focus:GetParent():GetId() == obj:GetId() then
return
end
end
end
rerouteMenuCommand(ide.frame, globalid)
end
end
local at = {}
for lid in pairs(remap) do
local shortcut = ide.config.keymap[lid]
-- find a (potential) conflict for this shortcut (if any)
for gid, ksc in pairs(ide.config.keymap) do
-- if the same shortcut is used elsewhere (not one of IDs being checked)
if shortcut:lower() == ksc:lower() and not remap[gid] then
local fakeid = NewID()
ide.frame:Connect(fakeid, wx.wxEVT_COMMAND_MENU_SELECTED,
resolveConflict(lid, gid))
local ae = wx.wxAcceleratorEntry(); ae:FromString(ksc)
table.insert(at, wx.wxAcceleratorEntry(ae:GetFlags(), ae:GetKeyCode(), fakeid))
end
end
end
if ide.osname == 'Macintosh' then
table.insert(at, wx.wxAcceleratorEntry(wx.wxACCEL_CTRL, ('M'):byte(), ID_VIEWMINIMIZE))
end
ide.frame:SetAcceleratorTable(wx.wxAcceleratorTable(at))
-- only set menu bar *after* postinit handler as it may include adding
-- app-specific menus (Help/About), which are not recognized by MacOS
-- as special items unless SetMenuBar is done after menus are populated.
ide.frame:SetMenuBar(ide.frame.menuBar)
ide:Print() -- flush pending output (if any)
PackageEventHandle("onAppLoad")
-- The status bar content is drawn incorrectly if it is shown
-- after being initially hidden.
-- Show the statusbar and hide it after showing the frame, which fixes the issue.
local statusbarfix = ide.osname == 'Windows' and not ide.frame:GetStatusBar():IsShown()
if statusbarfix then ide.frame:GetStatusBar():Show(true) end
ide.frame:Show(true)
if statusbarfix then ide.frame:GetStatusBar():Show(false) end
-- somehow having wxAuiToolbar "steals" the focus from the editor on OSX;
-- have to set the focus implicitly on the current editor (if any)
if ide.osname == 'Macintosh' then
local editor = GetEditor()
if editor then editor:SetFocus() end
end
wx.wxGetApp():MainLoop()
-- There are several reasons for this call:
-- (1) to fix a crash on OSX when closing with debugging in progress.
-- (2) to fix a crash on Linux 32/64bit during GC cleanup in wxlua
-- after an external process has been started from the IDE.
-- (3) to fix exit on Windows when started as "bin\lua src\main.lua".
os.exit()

View File

@ -0,0 +1,645 @@
-- Copyright 2011-15 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
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: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
function FileDirHasContent(dir)
local f = wx.wxFindFirstFile(dir, wx.wxFILE + wx.wxDIR)
return #f>0
end
function FileSysGetRecursive(path, recursive, spec, opts)
local content = {}
local showhidden = ide.config and ide.config.showhiddenfiles
local sep = GetPathSeparator()
-- trip 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 optskipbinary = (opts or {}).skipbinary
local optondirectory = (opts or {}).ondirectory
local function spec2list(spec, list)
-- return empty list if no spec is provided
if spec == nil or spec == "*" or spec == "*.*" then return {}, 0 end
-- accept "*.lua" and "*.txt,*.wlua" combinations
if type(spec) == 'table' then spec = table.concat(spec, ",") end
local masknum, list = 0, list or {}
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
-- and replace (escaped) ** with .* and (escaped) * with [^\//]*
table.insert(list, EscapeMagic(m)
:gsub("%%%*%%%*", ".*"):gsub("%%%*", "[^/\\]*").."$")
end
masknum = masknum + 1
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 function getDir(path)
dir:Open(path)
if not dir:IsOpened() then
if DisplayOutputLn and TR then
DisplayOutputLn(TR("Can't open '%s': %s"):format(path, wx.wxSysErrorMsg()))
end
return
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
if recursive and ismatch(fname..sep, nil, exmasks)
and (not optondirectory or optondirectory(fname) ~= false)
-- 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
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
found, file = dir:GetNext()
end
end
while #queue > 0 do getDir(table.remove(queue)) end
if optyield then 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 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 _ = 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 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 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 return false, msg or "Unknown error" end
if len < length then break end
pos = pos + len
file:Seek(pos)
end
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
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]") 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 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
function TR(msg, count)
local messages = ide.config.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 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)
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 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, 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 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 editor = GetEditor()
if isCtrlFocused(editor) then return editor end
local nb = ide:GetOutputNotebook()
for p = 0, nb:GetPageCount()-1 do
local ctrl = nb:GetPage(p)
if ctrl:GetClassInfo():GetClassName() == "wxStyledTextCtrl"
and isCtrlFocused(ctrl) then
return ctrl:DynamicCast("wxStyledTextCtrl")
end
end
return 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:GetLineDyn(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
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

View File

@ -0,0 +1 @@
ide.VERSION = [[1.30]]