Order files
This commit is contained in:
332
android/tools/zbstudio.app/Contents/ZeroBraneStudio/src/defs.lua
Normal file
332
android/tools/zbstudio.app/Contents/ZeroBraneStudio/src/defs.lua
Normal 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
|
||||
}
|
Binary file not shown.
@ -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
|
@ -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
@ -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
@ -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
|
@ -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)
|
@ -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)
|
@ -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
|
@ -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"},
|
||||
}
|
@ -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())
|
@ -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
|
@ -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)
|
@ -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)
|
@ -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 © 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 © 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 © 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
|
@ -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)
|
@ -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)
|
@ -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
|
@ -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
|
@ -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())
|
@ -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
|
@ -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
|
@ -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
|
@ -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,
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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")},
|
||||
}
|
774
android/tools/zbstudio.app/Contents/ZeroBraneStudio/src/main.lua
Normal file
774
android/tools/zbstudio.app/Contents/ZeroBraneStudio/src/main.lua
Normal 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()
|
645
android/tools/zbstudio.app/Contents/ZeroBraneStudio/src/util.lua
Normal file
645
android/tools/zbstudio.app/Contents/ZeroBraneStudio/src/util.lua
Normal 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
|
@ -0,0 +1 @@
|
||||
ide.VERSION = [[1.30]]
|
Reference in New Issue
Block a user