Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b2241085d | ||
|
|
c9cfd42d34 | ||
|
|
c25c56069d | ||
|
|
a68e8f7bd3 | ||
|
|
fc6de036e0 | ||
|
|
08d42a2501 | ||
|
|
da32878984 | ||
|
|
3fd5d88656 | ||
|
|
7d9ad5100c | ||
|
|
6bff634446 | ||
|
|
b0de487bf0 | ||
|
|
560d56835a | ||
|
|
3c6a06f537 | ||
|
|
b1f3bf0bd9 | ||
|
|
6ea3a4708d | ||
|
|
4de0eb1dc0 | ||
|
|
c2f6ed6338 | ||
|
|
fad7ff0cc3 | ||
|
|
9afa35f9b2 | ||
|
|
3a5b46f65e | ||
|
|
26d0908c89 | ||
|
|
770db579d9 | ||
|
|
8e09ed1658 | ||
|
|
25ac96d39a | ||
|
|
22fda661ec | ||
|
|
529a0e8c9e | ||
|
|
7cc061e18d | ||
|
|
5f2226bac2 | ||
|
|
37795c2480 | ||
|
|
5e934dfd69 | ||
|
|
f970ba38a0 | ||
|
|
8af6c1b5b6 | ||
|
|
0777a5fbd8 | ||
|
|
942681e246 | ||
|
|
ce8f120e48 | ||
|
|
048fd2349a | ||
|
|
2627957e5a | ||
|
|
16b0ca0fc7 | ||
|
|
e3189ba38a | ||
|
|
ad48622f68 | ||
|
|
0a9439ad22 | ||
|
|
a7577d5054 | ||
|
|
668cdcb4db | ||
|
|
4ec3e87473 | ||
|
|
90b4f38223 | ||
|
|
c1918e5c38 | ||
|
|
34baaa9447 | ||
|
|
817938593a | ||
|
|
b944f9d01d | ||
|
|
5d4b2395b5 | ||
|
|
f9ab1546ff | ||
|
|
971e7c8316 | ||
|
|
60a89b8f32 | ||
|
|
7275f0d11a | ||
|
|
35213d7d3b | ||
|
|
cde26253e3 | ||
|
|
b45c0600c7 | ||
|
|
5784dea99a | ||
|
|
e880fdde60 | ||
|
|
b7d8512b7b | ||
|
|
a7a8f32c91 | ||
|
|
9fc4ab5e9b | ||
|
|
37434534cc | ||
|
|
fc03c2843b | ||
|
|
2abbad04ac | ||
|
|
dbb392e009 | ||
|
|
65947ff924 | ||
|
|
d8324b269d | ||
|
|
ac9c2e9a84 | ||
|
|
3b2fcfd1f9 | ||
|
|
14fe7af771 | ||
|
|
7b0405bc79 | ||
|
|
fc6b0c176d | ||
|
|
5c50a2645b | ||
|
|
11cfbfa68d | ||
|
|
e6b318c643 | ||
|
|
1351db6114 | ||
|
|
b58e931409 | ||
|
|
e0eb05b122 | ||
|
|
8ecd8dfa6b | ||
|
|
c1961e5c4b | ||
|
|
3ead2966d8 | ||
|
|
0bcf590f45 | ||
|
|
e8f308ca08 | ||
|
|
ecb59ceb9e | ||
|
|
a317f986e1 | ||
|
|
623905c81c | ||
|
|
11b702ac6e | ||
|
|
f926aef2f8 | ||
|
|
3754406e85 | ||
|
|
981bccbe27 | ||
|
|
047a9e8ac5 | ||
|
|
d1f2e8450a | ||
|
|
21cfba9cb6 | ||
|
|
3f711373ed | ||
|
|
00fe05e89f | ||
|
|
3bcd54bd46 | ||
|
|
8de9e41fd6 | ||
|
|
29b6755a9b | ||
|
|
7d6be282f1 | ||
|
|
f89fd4d752 | ||
|
|
03adda1cef | ||
|
|
2992424e87 | ||
|
|
e302a97682 | ||
|
|
7b5d37d595 | ||
|
|
01ae85dc5a | ||
|
|
f650d8f64f | ||
|
|
602f8ef223 | ||
|
|
6afc999b75 | ||
|
|
2ccd214a1f | ||
|
|
8efde0ec1f | ||
|
|
1c5b14870c | ||
|
|
2c87909920 | ||
|
|
2a2a3bed96 | ||
|
|
e2f65bced5 | ||
|
|
c979d60d28 | ||
|
|
7954ff1f64 | ||
|
|
a3a5c75694 | ||
|
|
d6f3b4052b | ||
|
|
7bc64d90c7 | ||
|
|
56d262b753 | ||
|
|
a368f0acf9 | ||
|
|
d4a53733e7 | ||
|
|
1117e9df9a | ||
|
|
230de3450f | ||
|
|
0ae48ce6de | ||
|
|
6a90c5e850 | ||
|
|
f1ac72b265 | ||
|
|
79fd90c986 | ||
|
|
4dcf470c09 | ||
|
|
fcdbd456de | ||
|
|
e92127d6c8 | ||
|
|
0d8e6b0581 | ||
|
|
16d72396b1 | ||
|
|
6c7e289f71 | ||
|
|
deb99ec084 | ||
|
|
12f84d3509 |
@@ -1,8 +1,10 @@
|
||||
# Project Description
|
||||
|
||||
A simple and extensible Lua IDE and debugger. It supports multiple file
|
||||
formats, "api" for autocompletion and tooltips, and custom command-line
|
||||
tools. Its main focus is extensibility for target applications using Lua.
|
||||
|
||||
--[[ FEATURES ]]-----------------------------------------------------------
|
||||
## Features
|
||||
|
||||
* Written in Lua, so easily customizable
|
||||
* Automatically loads several 'plugin' like classes
|
||||
@@ -23,23 +25,27 @@ tools. Its main focus is extensibility for target applications using Lua.
|
||||
* Console to directly test code snippets with local and remote execution
|
||||
* Integrated debugger (with support for local and remote debugging)
|
||||
|
||||
--[[ FRONT-ENDS ]]--------------------------------------------------------
|
||||
## Frontends
|
||||
|
||||
There is currently two front-ends using the same editor engine. The original
|
||||
is "estrela", which has a focus on 3d graphics related usage of Lua, especially
|
||||
in combination with the luxinia engine or luxinia2 framework.
|
||||
The second is "zbstudio" which has a focus on remote use of Lua in robotics.
|
||||
one is `Estrela`, which has a focus on 3d graphics related usage of Lua,
|
||||
especially in combination with the luxinia engine or luxinia2 framework.
|
||||
The second front-end is `ZeroBrane Studio` (zbstudio) which has a focus
|
||||
on using Lua in education, mobile development, and robotics.
|
||||
|
||||
Both are part of the standard distribution.
|
||||
|
||||
--[[ INSTALLATION ]]-------------------------------------------------------
|
||||
## Installation
|
||||
|
||||
git clone git://github.com/pkulchenko/ZeroBraneStudio.git zbstudio
|
||||
```bash
|
||||
$ git clone git://github.com/pkulchenko/ZeroBraneStudio.git zbstudio
|
||||
or
|
||||
git clone git://estrelaeditor.git.sourceforge.net/gitroot/estrelaeditor/estrelaeditor estrelaeditor
|
||||
$ git clone git://estrelaeditor.git.sourceforge.net/gitroot/estrelaeditor/estrelaeditor estrelaeditor
|
||||
```
|
||||
|
||||
--[[ USAGE ]]--------------------------------------------------------------
|
||||
## Usage
|
||||
|
||||
```
|
||||
Open File(s):
|
||||
<exe> <filename> [<filename>...]
|
||||
any non-option will be treated as filename
|
||||
@@ -47,17 +53,18 @@ Open File(s):
|
||||
Overriding Config:
|
||||
<exe> [...] -cfg "<luacode overriding config>" [...]
|
||||
e.g.: zbstudio.exe -cfg "singleinstance=false;" somefile.lua
|
||||
```
|
||||
|
||||
--[[ AUTHOR ]]-------------------------------------------------------------
|
||||
## Author
|
||||
|
||||
Estrela Editor
|
||||
### Estrela Editor
|
||||
|
||||
Luxinia Dev: Christoph Kubisch (crazybutcher@luxinia.de)
|
||||
**Luxinia Dev:** Christoph Kubisch (crazybutcher@luxinia.de)
|
||||
|
||||
ZeroBrane Studio and MobDebug
|
||||
### ZeroBrane Studio and MobDebug
|
||||
|
||||
ZeroBrane LLC: Paul Kulchenko (paul@kulchenko.com)
|
||||
**ZeroBrane LLC:** Paul Kulchenko (paul@kulchenko.com)
|
||||
|
||||
--[[ LICENSE ]]------------------------------------------------------------
|
||||
## License
|
||||
|
||||
See LICENSE file
|
||||
See LICENSE file.
|
||||
4301
api/lua/love2d.lua
Normal file
4301
api/lua/love2d.lua
Normal file
File diff suppressed because it is too large
Load Diff
BIN
bin/clibs/mime/core.dll
Normal file
BIN
bin/clibs/mime/core.dll
Normal file
Binary file not shown.
BIN
bin/clibs/ssl.dll
Normal file
BIN
bin/clibs/ssl.dll
Normal file
Binary file not shown.
27
cfg/user-sample.lua
Normal file
27
cfg/user-sample.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
--[[-- Rename this file to `user.lua` to get loaded
|
||||
|
||||
Configuration files are loaded in the following order
|
||||
|
||||
1. <application>\config.lua
|
||||
2. cfg\user.lua
|
||||
3. -cfg commandline strings
|
||||
|
||||
-- an example of how loaded configuration can be modified from this file
|
||||
|
||||
local G = ... -- this now points to the global environment in the script
|
||||
local luaspec = G.ide.specs['lua']
|
||||
luaspec.exts[2] = "luaz"
|
||||
luaspec.keywords[1] = luaspec.keywords[1] .. ' foo'
|
||||
|
||||
-- these changes are going to be mapped to ide.config.editor...
|
||||
-- change encoding to Cyrillic
|
||||
editor.fontencoding = G.wx.wxFONTENCODING_ISO8859_5
|
||||
-- or WinCyrillic
|
||||
editor.fontencoding = G.wx.wxFONTENCODING_CP1251
|
||||
outputshell.fontencoding = G.wx.wxFONTENCODING_CP1251
|
||||
|
||||
-- specify full path to love2d executable; this is only needed
|
||||
-- if the game folder and the executable are NOT in the same folder.
|
||||
path.love2d = 'd:/lua/love/love' -- set the path of love executable
|
||||
|
||||
--]]--
|
||||
@@ -1,9 +0,0 @@
|
||||
--[[--
|
||||
|
||||
estrela loads configs in the following order
|
||||
|
||||
1. <application>\config.lua
|
||||
2. cfg\user.lua
|
||||
3. -cfg commandline strings
|
||||
|
||||
--]]--
|
||||
28
interpreters/love2d.lua
Normal file
28
interpreters/love2d.lua
Normal file
@@ -0,0 +1,28 @@
|
||||
return {
|
||||
name = "Love2d",
|
||||
description = "Love2d game engine",
|
||||
api = {"baselib", "love2d"},
|
||||
frun = function(self,wfilename,rundebug)
|
||||
if rundebug then DebuggerAttachDefault() end
|
||||
local love2d = ide.config.path.love2d
|
||||
or wx.wxFileName(self:fprojdir(wfilename)):GetPath(wx.wxPATH_GET_VOLUME)
|
||||
.. '/love'
|
||||
local cmd = ('"%s" "%s"%s'):format(string.gsub(love2d, "\\","/"),
|
||||
self:fprojdir(wfilename), rundebug and ' -debug' or '')
|
||||
-- CommandLineRun(cmd,wdir,tooutput,nohide,stringcallback,uid,endcallback)
|
||||
return CommandLineRun(cmd,self:fworkdir(wfilename),true,false,nil,nil,
|
||||
function() ide.debugger.pid = nil end)
|
||||
end,
|
||||
fprojdir = function(self,wfilename)
|
||||
return wfilename:GetPath(wx.wxPATH_GET_VOLUME)
|
||||
end,
|
||||
fworkdir = function (self,wfilename)
|
||||
return ide.config.path.projectdir
|
||||
or wfilename:GetPath(wx.wxPATH_GET_VOLUME)
|
||||
end,
|
||||
hasdebugger = true,
|
||||
fattachdebug = function(self)
|
||||
DebuggerAttachDefault()
|
||||
end,
|
||||
scratchextloop = true,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
return {
|
||||
name = "Lua",
|
||||
description = "Commandline Lua interpreter",
|
||||
api = {"wxwidgets","baselib"},
|
||||
frun = function(self,wfilename)
|
||||
local mainpath = ide.editorFilename:gsub("[^/\\]+$","")
|
||||
local filepath = wfilename:GetFullPath()
|
||||
local code = ([[
|
||||
xpcall(function() dofile '%s' end,
|
||||
function(err) print(debug.traceback(err)) end)
|
||||
]]):format(filepath:gsub("\\","/"))
|
||||
local cmd = '"'..mainpath..'/bin/lua.exe" -e "'..code..'"'
|
||||
CommandLineRun(cmd,self:fworkdir(wfilename),true,false)
|
||||
end,
|
||||
fprojdir = function(self,wfilename)
|
||||
return wfilename:GetPath(wx.wxPATH_GET_VOLUME)
|
||||
end,
|
||||
fworkdir = function (self,wfilename)
|
||||
return ide.config.path.projectdir and ide.config.path.projectdir:len()>0 and
|
||||
ide.config.path.projectdir
|
||||
end,
|
||||
}
|
||||
@@ -1,22 +1,33 @@
|
||||
local mainpath = string.gsub(ide.editorFilename:gsub("[^/\\]+$",""),"\\","/")
|
||||
local os = ide.osname
|
||||
|
||||
local clibs =
|
||||
os == "Windows" and mainpath.."bin/?.dll;"..mainpath.."bin/clibs/?.dll" or
|
||||
os == "Macintosh" and mainpath.."bin/lib?.dylib;"..mainpath.."bin/clibs/?.dylib" or
|
||||
os == "Unix" and mainpath.."bin/?.so;"..mainpath.."bin/clibs/?.so" or nil
|
||||
wx.wxSetEnv("LUA_PATH", package.path .. ';'
|
||||
.. mainpath.."lualibs/?/?.lua;"..mainpath.."lualibs/?.lua")
|
||||
if clibs then wx.wxSetEnv("LUA_CPATH", package.cpath .. ';' .. clibs) end
|
||||
|
||||
local macExe = mainpath..'bin/lua.app/Contents/MacOS/lua'
|
||||
local exe = (os == "Macintosh" and wx.wxFileExists(macExe)
|
||||
and macExe or mainpath..'bin/lua')
|
||||
|
||||
return {
|
||||
name = "Lua Debug",
|
||||
description = "Commandline Lua interpreter",
|
||||
name = "Lua with Debugger",
|
||||
description = "Lua interpreter with debugger",
|
||||
api = {"wxwidgets","baselib"},
|
||||
frun = function(self,wfilename,rundebug)
|
||||
local mainpath = string.gsub(ide.editorFilename:gsub("[^/\\]+$",""),"\\","/")
|
||||
local filepath = string.gsub(wfilename:GetFullPath(), "\\","/")
|
||||
local script
|
||||
if rundebug then
|
||||
DebuggerAttachDefault()
|
||||
script = (""..
|
||||
"package.path=package.path..';"..mainpath.."lualibs/?/?.lua;"..mainpath.."lualibs/?.lua';"..
|
||||
"package.cpath=package.cpath..';"..mainpath.."bin/clibs/?.dll';"..
|
||||
"require('mobdebug').loop('" .. wx.wxGetHostName().."',"..ide.debugger.portnumber..")")
|
||||
script = rundebug
|
||||
else
|
||||
script = ([[dofile '%s']]):format(filepath)
|
||||
end
|
||||
local code = ([[xpcall(function() io.stdout:setvbuf('no'); %s end,function(err) print(debug.traceback(err)) end)]]):format(script)
|
||||
local cmd = '"'..mainpath..'bin/lua.exe" -e "'..code..'"'
|
||||
local cmd = '"'..exe..'" -e "'..code..'"'
|
||||
-- CommandLineRun(cmd,wdir,tooutput,nohide,stringcallback,uid,endcallback)
|
||||
return CommandLineRun(cmd,self:fworkdir(wfilename),true,false,nil,nil,
|
||||
function() ide.debugger.pid = nil end)
|
||||
|
||||
@@ -52,7 +52,7 @@ return {
|
||||
local editorDir = string.gsub(ide.editorFilename:gsub("[^/\\]+$",""),"\\","/")
|
||||
script = ""..
|
||||
"package.path=package.path..';"..editorDir.."lualibs/?/?.lua';"..
|
||||
"io.stdout:setvbuf('no'); require('mobdebug').start('" .. wx.wxGetHostName().."',"..ide.debugger.portnumber..")"
|
||||
"io.stdout:setvbuf('no'); require('mobdebug').start('" .. ide.debugger.hostname.."',"..ide.debugger.portnumber..")"
|
||||
|
||||
args = args..' -es "'..script..'"'..startargs
|
||||
else
|
||||
|
||||
929
lualibs/luainspect/ast.lua
Normal file
929
lualibs/luainspect/ast.lua
Normal file
@@ -0,0 +1,929 @@
|
||||
-- luainspect.ast - Lua Abstract Syntax Tree (AST) and token list operations.
|
||||
--
|
||||
-- Two main structures are maintained. A Metalua-style AST represents the
|
||||
-- nested syntactic structure obtained from the parse.
|
||||
-- A separate linear ordered list of tokens represents the syntactic structure
|
||||
-- from the lexing, including line information (character positions only not row/columns),
|
||||
-- comments, and keywords, which is originally built from the lineinfo attributes
|
||||
-- injected by Metalua into the AST (IMPROVE: it probably would be simpler
|
||||
-- to obtain this from the lexer directly rather then inferring it from the parsing).
|
||||
-- During AST manipulations, the lineinfo maintained in the AST is ignored
|
||||
-- because it was found more difficult to maintain and not in the optimal format.
|
||||
--
|
||||
-- The contained code deals with
|
||||
-- - Building the AST from source.
|
||||
-- - Building the tokenlist from the AST lineinfo.
|
||||
-- - Querying the AST+tokenlist.
|
||||
-- - Modifying the AST+tokenlist (including incremental parsing source -> AST)
|
||||
-- - Annotating the AST with navigational info (e.g. parent links) to assist queries.
|
||||
-- - Dumping the tokenlist for debugging.
|
||||
--
|
||||
-- (c) 2010 David Manura, MIT License.
|
||||
|
||||
|
||||
--! require 'luainspect.typecheck' (context)
|
||||
|
||||
-- boilerplate/utility
|
||||
-- LUA_PATH="?.lua;/path/to/metalua/src/compiler/?.lua;/path/to/metalua/src/lib/?.lua"
|
||||
-- import modules -- order is important
|
||||
require "lexer"
|
||||
require "gg"
|
||||
require "mlp_lexer"
|
||||
require "mlp_misc"
|
||||
require "mlp_table"
|
||||
require "mlp_meta"
|
||||
require "mlp_expr"
|
||||
require "mlp_stat"
|
||||
--require "mlp_ext"
|
||||
_G.mlc = {} -- make gg happy
|
||||
-- Metalua:IMPROVE: make above imports simpler
|
||||
|
||||
local M = {}
|
||||
|
||||
--[=TESTSUITE
|
||||
-- utilities
|
||||
local ops = {}
|
||||
ops['=='] = function(a,b) return a == b end
|
||||
local function check(opname, a, b)
|
||||
local op = assert(ops[opname])
|
||||
if not op(a,b) then
|
||||
error("fail == " .. tostring(a) .. " " .. tostring(b))
|
||||
end
|
||||
end
|
||||
--]=]
|
||||
|
||||
-- CATEGORY: debug
|
||||
local function DEBUG(...)
|
||||
if LUAINSPECT_DEBUG then
|
||||
print('DEBUG:', ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Converts character position to row,column position in string src.
|
||||
-- Add values are 1-indexed.
|
||||
function M.pos_to_linecol(pos, src)
|
||||
local linenum = 1
|
||||
local lasteolpos = 0
|
||||
for eolpos in src:gmatch"()\n" do
|
||||
if eolpos > pos then break end
|
||||
linenum = linenum + 1
|
||||
lasteolpos = eolpos
|
||||
end
|
||||
local colnum = pos - lasteolpos
|
||||
return linenum, colnum
|
||||
end
|
||||
|
||||
-- Removes any sheband ("#!") line from Lua source string.
|
||||
-- CATEGORY: Lua parsing
|
||||
function M.remove_shebang(src)
|
||||
local shebang = src:match("^#![^\r\n]*")
|
||||
return shebang and (" "):rep(#shebang) .. src:sub(#shebang+1) or src
|
||||
end
|
||||
|
||||
|
||||
-- Custom version of loadstring that parses out line number info
|
||||
-- CATEGORY: Lua parsing
|
||||
function M.loadstring(src)
|
||||
local f, err = loadstring(src, "")
|
||||
if f then
|
||||
return f
|
||||
else
|
||||
err = err:gsub('^%[string ""%]:', "")
|
||||
local linenum = assert(err:match("(%d+):"))
|
||||
local colnum = 0
|
||||
local linenum2 = err:match("^%d+: '[^']+' expected %(to close '[^']+' at line (%d+)")
|
||||
return nil, err, linenum, colnum, linenum2
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- helper for ast_from_string. Raises on error.
|
||||
-- FIX? filename currently ignored in Metalua
|
||||
-- CATEGORY: Lua parsing
|
||||
local function ast_from_string_helper(src, filename)
|
||||
filename = filename or '(string)'
|
||||
local lx = mlp.lexer:newstream (src, filename)
|
||||
local ast = mlp.chunk(lx)
|
||||
return ast
|
||||
end
|
||||
|
||||
|
||||
-- Counts number of lines in text.
|
||||
-- Warning: the decision of whether to count a trailing new-line in a file
|
||||
-- or an empty file as a line is a little subjective. This function currently
|
||||
-- defines the line count as 1 plus the number of new line characters.
|
||||
-- CATEGORY: utility/string
|
||||
local function linecount(text)
|
||||
local n = 1
|
||||
for _ in text:gmatch'\n' do
|
||||
n = n + 1
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
|
||||
-- Converts Lua source string to Lua AST (via mlp/gg).
|
||||
-- CATEGORY: Lua parsing
|
||||
function M.ast_from_string(src, filename)
|
||||
local ok, ast = pcall(ast_from_string_helper, src, filename)
|
||||
if not ok then
|
||||
local err = ast
|
||||
err = err:match('[^\n]*')
|
||||
err = err:gsub("^.-:%s*line", "line")
|
||||
-- mlp.chunk prepending this is undesirable. error(msg,0) would be better in gg.lua. Reported.
|
||||
-- TODO-Metalua: remove when fixed in Metalua.
|
||||
local linenum, colnum = err:match("line (%d+), char (%d+)")
|
||||
if not linenum then
|
||||
-- Metalua libraries may return "...gg.lua:56: .../mlp_misc.lua:179: End-of-file expected"
|
||||
-- without the normal line/char numbers given things like "if x then end end". Should be
|
||||
-- fixed probably with gg.parse_error in _chunk in mlp_misc.lua.
|
||||
-- TODO-Metalua: remove when fixed in Metalua.
|
||||
linenum = linecount(src)
|
||||
colnum = 1
|
||||
end
|
||||
local linenum2 = nil
|
||||
return nil, err, linenum, colnum, linenum2
|
||||
else
|
||||
return ast
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Simple comment parser. Returns Metalua-style comment.
|
||||
-- CATEGORY: Lua lexing
|
||||
local function quick_parse_comment(src)
|
||||
local s = src:match"^%-%-([^\n]*)()\n$"
|
||||
if s then return {s, 1, #src, 'short'} end
|
||||
local _, s = src:match(lexer.lexer.patterns.long_comment .. '\r?\n?$')
|
||||
if s then return {s, 1, #src, 'long'} end
|
||||
return nil
|
||||
end
|
||||
--FIX:check new-line correctness
|
||||
--note: currently requiring \n at end of single line comment to avoid
|
||||
-- incremental compilation with `--x\nf()` and removing \n from still
|
||||
-- recognizing as comment `--x`.
|
||||
-- currently allowing \r\n at end of long comment since Metalua includes
|
||||
-- it in lineinfo of long comment (FIX:Metalua?)
|
||||
|
||||
|
||||
-- Gets length of longest prefix string in both provided strings.
|
||||
-- Returns max n such that text1:sub(1,n) == text2:sub(1,n) and n <= max(#text1,#text2)
|
||||
-- CATEGORY: string utility
|
||||
local function longest_prefix(text1, text2)
|
||||
local nmin = 0
|
||||
local nmax = math.min(#text1, #text2)
|
||||
while nmax > nmin do
|
||||
local nmid = math.ceil((nmin+nmax)/2)
|
||||
if text1:sub(1,nmid) == text2:sub(1,nmid) then
|
||||
nmin = nmid
|
||||
else
|
||||
nmax = nmid-1
|
||||
end
|
||||
end
|
||||
return nmin
|
||||
end
|
||||
|
||||
|
||||
-- Gets length of longest postfix string in both provided strings.
|
||||
-- Returns max n such that text1:sub(-n) == text2:sub(-n) and n <= max(#text1,#text2)
|
||||
-- CATEGORY: string utility
|
||||
local function longest_postfix(text1, text2)
|
||||
local nmin = 0
|
||||
local nmax = math.min(#text1, #text2)
|
||||
while nmax > nmin do
|
||||
local nmid = math.ceil((nmin+nmax)/2)
|
||||
if text1:sub(-nmid) == text2:sub(-nmid) then --[*]
|
||||
nmin = nmid
|
||||
else
|
||||
nmax = nmid-1
|
||||
end
|
||||
end
|
||||
return nmin
|
||||
end -- differs from longest_prefix only on line [*]
|
||||
|
||||
|
||||
|
||||
-- Determines AST node that must be re-evaluated upon changing code string from
|
||||
-- `src` to `bsrc`, given previous top_ast/tokenlist/src.
|
||||
-- Note: decorates top_ast as side-effect.
|
||||
-- If preserve is true, then does not expand AST match even if replacement is invalid.
|
||||
-- CATEGORY: AST/tokenlist manipulation
|
||||
function M.invalidated_code(top_ast, tokenlist, src, bsrc, preserve)
|
||||
-- Converts posiiton range in src to position range in bsrc.
|
||||
local function range_transform(src_fpos, src_lpos)
|
||||
local src_nlpos = #src - src_lpos
|
||||
local bsrc_fpos = src_fpos
|
||||
local bsrc_lpos = #bsrc - src_nlpos
|
||||
return bsrc_fpos, bsrc_lpos
|
||||
end
|
||||
|
||||
if src == bsrc then return end -- up-to-date
|
||||
|
||||
-- Find range of positions in src that differences correspond to.
|
||||
-- Note: for zero byte range, src_pos2 = src_pos1 - 1.
|
||||
local npre = longest_prefix(src, bsrc)
|
||||
local npost = math.min(#src-npre, longest_postfix(src, bsrc))
|
||||
-- note: min avoids overlap ambiguity
|
||||
local src_fpos, src_lpos = 1 + npre, #src - npost
|
||||
|
||||
-- Find smallest AST node containing src range above. May also
|
||||
-- be contained in (smaller) comment or whitespace.
|
||||
local match_ast, match_comment, iswhitespace =
|
||||
M.smallest_ast_containing_range(top_ast, tokenlist, src_fpos, src_lpos)
|
||||
DEBUG('invalidate-smallest:', match_ast and (match_ast.tag or 'notag'), match_comment, iswhitespace)
|
||||
|
||||
-- Determine which (ast, comment, or whitespace) to match, and get its pos range in src and bsrc.
|
||||
local srcm_fpos, srcm_lpos, bsrcm_fpos, bsrcm_lpos, mast, mtype
|
||||
if iswhitespace then
|
||||
mast, mtype = nil, 'whitespace'
|
||||
srcm_fpos, srcm_lpos = src_fpos, src_lpos
|
||||
elseif match_comment then
|
||||
mast, mtype = match_comment, 'comment'
|
||||
srcm_fpos, srcm_lpos = match_comment.fpos, match_comment.lpos
|
||||
else
|
||||
mast, mtype = match_ast, 'ast'
|
||||
repeat
|
||||
srcm_fpos, srcm_lpos = M.ast_pos_range(mast, tokenlist)
|
||||
if not srcm_fpos then
|
||||
if mast == top_ast then
|
||||
srcm_fpos, srcm_lpos = 1, #src
|
||||
break
|
||||
else
|
||||
M.ensure_parents_marked(top_ast)
|
||||
mast = mast.parent
|
||||
end
|
||||
end
|
||||
until srcm_fpos
|
||||
end
|
||||
bsrcm_fpos, bsrcm_lpos = range_transform(srcm_fpos, srcm_lpos)
|
||||
|
||||
-- Never expand match if preserve specified.
|
||||
if preserve then
|
||||
return srcm_fpos, srcm_lpos, bsrcm_fpos, bsrcm_lpos, mast, mtype
|
||||
end
|
||||
|
||||
-- Determine if replacement could break parent nodes.
|
||||
local isreplacesafe
|
||||
if mtype == 'whitespace' then
|
||||
if bsrc:sub(bsrcm_fpos, bsrcm_lpos):match'^%s*$' then -- replaced with whitespace
|
||||
if bsrc:sub(bsrcm_fpos-1, bsrcm_lpos+1):match'%s' then -- not eliminating whitespace
|
||||
isreplacesafe = true
|
||||
end
|
||||
end
|
||||
elseif mtype == 'comment' then
|
||||
local m2src = bsrc:sub(bsrcm_fpos, bsrcm_lpos)
|
||||
DEBUG('invalidate-comment[' .. m2src .. ']')
|
||||
if quick_parse_comment(m2src) then -- replaced with comment
|
||||
isreplacesafe = true
|
||||
end
|
||||
end
|
||||
if isreplacesafe then -- return on safe replacement
|
||||
return srcm_fpos, srcm_lpos, bsrcm_fpos, bsrcm_lpos, mast, mtype
|
||||
end
|
||||
|
||||
-- Find smallest containing statement block that will compile (or top_ast).
|
||||
while 1 do
|
||||
match_ast = M.get_containing_statementblock(match_ast, top_ast)
|
||||
if match_ast == top_ast then
|
||||
return 1,#src, 1, #bsrc, match_ast, 'statblock'
|
||||
-- entire AST invalidated
|
||||
end
|
||||
local srcm_fpos, srcm_lpos = M.ast_pos_range(match_ast, tokenlist)
|
||||
local bsrcm_fpos, bsrcm_lpos = range_transform(srcm_fpos, srcm_lpos)
|
||||
local msrc = bsrc:sub(bsrcm_fpos, bsrcm_lpos)
|
||||
DEBUG('invalidate-statblock:', match_ast and match_ast.tag, '[' .. msrc .. ']')
|
||||
if loadstring(msrc) then -- compiled
|
||||
return srcm_fpos, srcm_lpos, bsrcm_fpos, bsrcm_lpos, match_ast, 'statblock'
|
||||
end
|
||||
M.ensure_parents_marked(top_ast)
|
||||
match_ast = match_ast.parent
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Walks AST `ast` in arbitrary order, visiting each node `n`, executing `fdown(n)` (if specified)
|
||||
-- when doing down and `fup(n)` (if specified) when going if.
|
||||
-- CATEGORY: AST walk
|
||||
function M.walk(ast, fdown, fup)
|
||||
assert(type(ast) == 'table')
|
||||
if fdown then fdown(ast) end
|
||||
for _,bast in ipairs(ast) do
|
||||
if type(bast) == 'table' then
|
||||
M.walk(bast, fdown, fup)
|
||||
end
|
||||
end
|
||||
if fup then fup(ast) end
|
||||
end
|
||||
|
||||
|
||||
-- Replaces contents of table t1 with contents of table t2.
|
||||
-- Does not change metatable (if any).
|
||||
-- This function is useful for swapping one AST node with another
|
||||
-- while preserving any references to the node.
|
||||
-- CATEGORY: table utility
|
||||
function M.switchtable(t1, t2)
|
||||
for k in pairs(t1) do t1[k] = nil end
|
||||
for k in pairs(t2) do t1[k] = t2[k] end
|
||||
end
|
||||
|
||||
|
||||
-- Inserts all elements in list bt at index i in list t.
|
||||
-- CATEGORY: table utility
|
||||
local function tinsertlist(t, i, bt)
|
||||
local oldtlen, delta = #t, i - 1
|
||||
for ti = #t + 1, #t + #bt do t[ti] = false end -- preallocate (avoid holes)
|
||||
for ti = oldtlen, i, -1 do t[ti + #bt] = t[ti] end -- shift
|
||||
for bi = 1, #bt do t[bi + delta] = bt[bi] end -- fill
|
||||
end
|
||||
--[=[TESTSUITE:
|
||||
local function _tinsertlist(t, i, bt)
|
||||
for bi=#bt,1,-1 do table.insert(t, i, bt[bi]) end
|
||||
end -- equivalent but MUCH less efficient for large tables
|
||||
local function _tinsertlist(t, i, bt)
|
||||
for bi=1,#bt do table.insert(t, i+bi-1, bt[bi]) end
|
||||
end -- equivalent but MUCH less efficient for large tables
|
||||
local t = {}; tinsertlist(t, 1, {}); assert(table.concat(t)=='')
|
||||
local t = {}; tinsertlist(t, 1, {2,3}); assert(table.concat(t)=='23')
|
||||
local t = {4}; tinsertlist(t, 1, {2,3}); assert(table.concat(t)=='234')
|
||||
local t = {2}; tinsertlist(t, 2, {3,4}); assert(table.concat(t)=='234')
|
||||
local t = {4,5}; tinsertlist(t, 1, {2,3}); assert(table.concat(t)=='2345')
|
||||
local t = {2,5}; tinsertlist(t, 2, {3,4}); assert(table.concat(t)=='2345')
|
||||
local t = {2,3}; tinsertlist(t, 3, {4,5}); assert(table.concat(t)=='2345')
|
||||
print 'DONE'
|
||||
--]=]
|
||||
|
||||
|
||||
|
||||
-- Gets list of keyword positions related to node ast in source src
|
||||
-- note: ast must be visible, i.e. have lineinfo (e.g. unlike `Id "self" definition).
|
||||
-- Note: includes operators.
|
||||
-- Note: Assumes ast Metalua-style lineinfo is valid.
|
||||
-- CATEGORY: tokenlist build
|
||||
function M.get_keywords(ast, src)
|
||||
local list = {}
|
||||
if not ast.lineinfo then return list end
|
||||
-- examine space between each pair of children i and j.
|
||||
-- special cases: 0 is before first child and #ast+1 is after last child
|
||||
|
||||
-- Put children in lexical order.
|
||||
-- Some binary operations have arguments reversed from lexical order.
|
||||
-- For example, `a > b` becomes `Op{'lt', `Id 'b', `Id 'a'}
|
||||
local oast =
|
||||
(ast.tag == 'Op' and #ast == 3 and ast[2].lineinfo.first[3] > ast[3].lineinfo.first[3])
|
||||
and {ast[1], ast[3], ast[2]} or ast
|
||||
|
||||
local i = 0
|
||||
while i <= #ast do
|
||||
-- j is node following i that has lineinfo
|
||||
local j = i+1; while j < #ast+1 and not oast[j].lineinfo do j=j+1 end
|
||||
|
||||
-- Get position range [fpos,lpos] between subsequent children.
|
||||
local fpos
|
||||
if i == 0 then -- before first child
|
||||
fpos = ast.lineinfo.first[3]
|
||||
else
|
||||
local last = oast[i].lineinfo.last; local c = last.comments
|
||||
fpos = (c and #c > 0 and c[#c][3] or last[3]) + 1
|
||||
end
|
||||
local lpos
|
||||
if j == #ast+1 then -- after last child
|
||||
lpos = ast.lineinfo.last[3]
|
||||
else
|
||||
local first = oast[j].lineinfo.first; local c = first.comments
|
||||
--DEBUG('first', ast.tag, first[3], src:sub(first[3], first[3]+3))
|
||||
lpos = (c and #c > 0 and c[1][2] or first[3]) - 1
|
||||
end
|
||||
|
||||
-- Find keyword in range.
|
||||
local spos = fpos
|
||||
repeat
|
||||
local mfpos, tok, mlppos = src:match("^%s*()(%a+)()", spos)
|
||||
if not mfpos then
|
||||
mfpos, tok, mlppos = src:match("^%s*()(%p+)()", spos)
|
||||
end
|
||||
if mfpos then
|
||||
local mlpos = mlppos-1
|
||||
if mlpos > lpos then mlpos = lpos end
|
||||
--DEBUG('look', ast.tag, #ast,i,j,'*', mfpos, tok, mlppos, fpos, lpos, src:sub(fpos, fpos+5))
|
||||
if mlpos >= mfpos then
|
||||
list[#list+1] = mfpos
|
||||
list[#list+1] = mlpos
|
||||
end
|
||||
end
|
||||
spos = mlppos
|
||||
until not spos or spos > lpos
|
||||
-- note: finds single keyword. in `local function` returns only `local`
|
||||
--DEBUG(i,j ,'test[' .. src:sub(fpos, lpos) .. ']')
|
||||
|
||||
i = j -- next
|
||||
|
||||
--DESIGN:Lua: comment: string.match accepts a start position but not a stop position
|
||||
end
|
||||
return list
|
||||
end
|
||||
-- Q:Metalua: does ast.lineinfo[loc].comments imply #ast.lineinfo[loc].comments > 0 ?
|
||||
|
||||
|
||||
|
||||
-- Generates ordered list of tokens in top_ast/src.
|
||||
-- Note: currently ignores operators and parens.
|
||||
-- Note: Modifies ast.
|
||||
-- Note: Assumes ast Metalua-style lineinfo is valid.
|
||||
-- CATEGORY: AST/tokenlist query
|
||||
local isterminal = {Nil=true, Dots=true, True=true, False=true, Number=true, String=true,
|
||||
Dots=true, Id=true}
|
||||
local function compare_tokens_(atoken, btoken) return atoken.fpos < btoken.fpos end
|
||||
function M.ast_to_tokenlist(top_ast, src)
|
||||
local tokens = {} -- {nbytes=#src}
|
||||
local isseen = {}
|
||||
M.walk(top_ast, function(ast)
|
||||
if isterminal[ast.tag] then -- Extract terminal
|
||||
local token = ast
|
||||
if ast.lineinfo then
|
||||
token.fpos, token.lpos, token.ast = ast.lineinfo.first[3], ast.lineinfo.last[3], ast
|
||||
table.insert(tokens, token)
|
||||
end
|
||||
else -- Extract non-terminal
|
||||
local keywordposlist = M.get_keywords(ast, src)
|
||||
for i=1,#keywordposlist,2 do
|
||||
local fpos, lpos = keywordposlist[i], keywordposlist[i+1]
|
||||
local toksrc = src:sub(fpos, lpos)
|
||||
local token = {tag='Keyword', fpos=fpos, lpos=lpos, ast=ast, toksrc}
|
||||
table.insert(tokens, token)
|
||||
end
|
||||
end
|
||||
-- Extract comments
|
||||
for i=1,2 do
|
||||
local comments = ast.lineinfo and ast.lineinfo[i==1 and 'first' or 'last'].comments
|
||||
if comments then for _, comment in ipairs(comments) do
|
||||
if not isseen[comment] then
|
||||
comment.tag = 'Comment'
|
||||
local token = comment
|
||||
token.fpos, token.lpos, token.ast = comment[2], comment[3], comment
|
||||
table.insert(tokens, token)
|
||||
isseen[comment] = true
|
||||
end
|
||||
end end
|
||||
end
|
||||
end, nil)
|
||||
table.sort(tokens, compare_tokens_)
|
||||
return tokens
|
||||
end
|
||||
|
||||
|
||||
-- Gets tokenlist range [fidx,lidx] covered by ast. Returns nil,nil if not found.
|
||||
--FIX:PERFORMANCE:this is slow on large files.
|
||||
-- CATEGORY: AST/tokenlist query
|
||||
function M.ast_idx_range_in_tokenlist(tokenlist, ast)
|
||||
-- Get list of primary nodes under ast.
|
||||
local isold = {}; M.walk(ast, function(ast) isold[ast] = true end)
|
||||
-- Get range.
|
||||
local fidx, lidx
|
||||
for idx=1,#tokenlist do
|
||||
local token = tokenlist[idx]
|
||||
if isold[token.ast] then
|
||||
lidx = idx
|
||||
if not fidx then fidx = idx end
|
||||
end
|
||||
end
|
||||
return fidx, lidx
|
||||
end
|
||||
|
||||
|
||||
-- Gets index range in tokenlist overlapped by character position range [fpos, lpos].
|
||||
-- For example, `do ff() end` with range ` ff() ` would match tokens `ff()`.
|
||||
-- Tokens partly inside range are counted, so range `f()` would match tokens `ff()`.
|
||||
-- If lidx = fidx - 1, then position range is whitespace between tokens lidx (on left)
|
||||
-- and fidx (on right), and this may include token pseudoindices 0 (start of file) and
|
||||
-- #tokenlist+1 (end of file).
|
||||
-- Note: lpos == fpos - 1 indicates zero-width range between chars lpos and fpos.
|
||||
-- CATEGORY: tokenlist query
|
||||
function M.tokenlist_idx_range_over_pos_range(tokenlist, fpos, lpos)
|
||||
-- Find first/last indices of tokens overlapped (even partly) by position range.
|
||||
local fidx, lidx
|
||||
for idx=1,#tokenlist do
|
||||
local token = tokenlist[idx]
|
||||
--if (token.fpos >= fpos and token.fpos <= lpos) or (token.lpos >= fpos and token.lpos <= lpos) then -- token overlaps range
|
||||
if fpos <= token.lpos and lpos >= token.fpos then -- range overlaps token (even partially)
|
||||
if not fidx then fidx = idx end
|
||||
lidx = idx
|
||||
end
|
||||
end
|
||||
if not fidx then -- on fail, check between tokens
|
||||
for idx=1,#tokenlist+1 do -- between idx-1 and idx
|
||||
local tokfpos, toklpos = tokenlist[idx-1] and tokenlist[idx-1].lpos, tokenlist[idx] and tokenlist[idx].fpos
|
||||
if (not tokfpos or fpos > tokfpos) and (not toklpos or lpos < toklpos) then -- range between tokens
|
||||
return idx, idx-1
|
||||
end
|
||||
end
|
||||
end
|
||||
return fidx, lidx
|
||||
end
|
||||
--[=[TESTSUITE
|
||||
local function test(...)
|
||||
return table.concat({M.tokenlist_idx_range_over_pos_range(...)}, ',')
|
||||
end
|
||||
check('==', test({}, 2, 2), "1,0") -- no tokens
|
||||
check('==', test({{tag='Id', fpos=1, lpos=1}}, 2, 2), "2,1") -- right of one token
|
||||
check('==', test({{tag='Id', fpos=3, lpos=3}}, 2, 2), "1,0") -- left of one token
|
||||
check('==', test({{tag='Id', fpos=3, lpos=4}}, 2, 3), "1,1") -- left partial overlap one token
|
||||
check('==', test({{tag='Id', fpos=3, lpos=4}}, 4, 5), "1,1") -- right partial overlap one token
|
||||
check('==', test({{tag='Id', fpos=3, lpos=6}}, 4, 5), "1,1") -- partial inner overlap one token
|
||||
check('==', test({{tag='Id', fpos=3, lpos=6}}, 3, 6), "1,1") -- exact overlap one token
|
||||
check('==', test({{tag='Id', fpos=4, lpos=5}}, 3, 6), "1,1") -- extra overlap one token
|
||||
check('==', test({{tag='Id', fpos=2, lpos=3}, {tag='Id', fpos=5, lpos=6}}, 4, 4), "2,1") -- between tokens, " " exact
|
||||
check('==', test({{tag='Id', fpos=2, lpos=3}, {tag='Id', fpos=5, lpos=6}}, 4, 3), "2,1") -- between tokens, "" on left
|
||||
check('==', test({{tag='Id', fpos=2, lpos=3}, {tag='Id', fpos=5, lpos=6}}, 5, 4), "2,1") -- between tokens, "" on right
|
||||
check('==', test({{tag='Id', fpos=2, lpos=3}, {tag='Id', fpos=4, lpos=5}}, 4, 3), "2,1") -- between tokens, "" exact
|
||||
--]=]
|
||||
|
||||
-- Removes tokens in tokenlist covered by ast.
|
||||
-- CATEGORY: tokenlist manipulation
|
||||
local function remove_ast_in_tokenlist(tokenlist, ast)
|
||||
local fidx, lidx = M.ast_idx_range_in_tokenlist(tokenlist, ast)
|
||||
if fidx then -- note: fidx implies lidx
|
||||
for idx=lidx,fidx,-1 do table.remove(tokenlist, idx) end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Inserts tokens from btokenlist into tokenlist. Preserves sort.
|
||||
-- CATEGORY: tokenlist manipulation
|
||||
local function insert_tokenlist(tokenlist, btokenlist)
|
||||
local ftoken = btokenlist[1]
|
||||
if ftoken then
|
||||
-- Get index in tokenlist in which to insert tokens in btokenlist.
|
||||
local fidx
|
||||
for idx=1,#tokenlist do
|
||||
if tokenlist[idx].fpos > ftoken.fpos then fidx = idx; break end
|
||||
end
|
||||
fidx = fidx or #tokenlist + 1 -- else append
|
||||
|
||||
-- Insert tokens.
|
||||
tinsertlist(tokenlist, fidx, btokenlist)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Get character position range covered by ast in tokenlist. Returns nil,nil on not found.
|
||||
-- CATEGORY: AST/tokenlist query
|
||||
function M.ast_pos_range(ast, tokenlist) -- IMPROVE:style: ast_idx_range_in_tokenlist has params reversed
|
||||
local fidx, lidx = M.ast_idx_range_in_tokenlist(tokenlist, ast)
|
||||
if fidx then
|
||||
return tokenlist[fidx].fpos, tokenlist[lidx].lpos
|
||||
else
|
||||
return nil, nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Gets string representation of AST node. nil if none.
|
||||
-- IMPROVE: what if node is empty block?
|
||||
-- CATEGORY: AST/tokenlist query
|
||||
function M.ast_to_text(ast, tokenlist, src) -- IMPROVE:style: ast_idx_range_in_tokenlist has params reversed
|
||||
local fpos, lpos = M.ast_pos_range(ast, tokenlist)
|
||||
if fpos then
|
||||
return src:sub(fpos, lpos)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- Gets smallest AST node in top_ast/tokenlist/src
|
||||
-- completely containing position range [pos1, pos2].
|
||||
-- careful: "function" is not part of the `Function node.
|
||||
-- If range is inside comment, returns comment also.
|
||||
-- If range is inside whitespace, then returns true in third return value.
|
||||
-- CATEGORY: AST/tokenlist query
|
||||
function M.smallest_ast_containing_range(top_ast, tokenlist, pos1, pos2)
|
||||
local f0idx, l0idx = M.tokenlist_idx_range_over_pos_range(tokenlist, pos1, pos2)
|
||||
|
||||
-- Find enclosing AST.
|
||||
M.ensure_parents_marked(top_ast)
|
||||
local fidx, lidx = f0idx, l0idx
|
||||
while tokenlist[fidx] and not tokenlist[fidx].ast.parent do fidx = fidx - 1 end
|
||||
while tokenlist[lidx] and not tokenlist[lidx].ast.parent do lidx = lidx + 1 end
|
||||
-- DEBUG(fidx, lidx, f0idx, l0idx, #tokenlist, pos1, pos2, tokenlist[fidx], tokenlist[lidx])
|
||||
local ast = not (tokenlist[fidx] and tokenlist[lidx]) and top_ast or
|
||||
M.common_ast_parent(tokenlist[fidx].ast, tokenlist[lidx].ast, top_ast)
|
||||
-- DEBUG('m2', tokenlist[fidx], tokenlist[lidx], top_ast, ast, ast and ast.tag)
|
||||
if l0idx == f0idx - 1 then -- whitespace
|
||||
return ast, nil, true
|
||||
elseif l0idx == f0idx and tokenlist[l0idx].tag == 'Comment' then
|
||||
return ast, tokenlist[l0idx], nil
|
||||
else
|
||||
return ast, nil, nil
|
||||
end
|
||||
end
|
||||
--IMPROVE: handle string edits and maybe others
|
||||
|
||||
|
||||
-- Gets smallest statement block containing position pos or
|
||||
-- nearest statement block before pos, whichever is smaller, given ast/tokenlist.
|
||||
function M.current_statementblock(ast, tokenlist, pos)
|
||||
local fidx,lidx = M.tokenlist_idx_range_over_pos_range(tokenlist, pos, pos)
|
||||
if fidx > lidx then fidx = lidx end -- use nearest backward
|
||||
|
||||
-- Find closest AST node backward
|
||||
while fidx >= 1 and tokenlist[fidx].tag == 'Comment' do fidx=fidx-1 end
|
||||
|
||||
if fidx < 1 then return ast, false end
|
||||
local mast = tokenlist[fidx].ast
|
||||
if not mast then return ast, false end
|
||||
mast = M.get_containing_statementblock(mast, ast)
|
||||
local isafter = false
|
||||
if mast.tag2 ~= 'Block' then
|
||||
local mfidx,mlidx = M.ast_idx_range_in_tokenlist(tokenlist, mast)
|
||||
if pos > mlidx then
|
||||
isafter = true
|
||||
end
|
||||
end
|
||||
|
||||
return mast, isafter
|
||||
end
|
||||
|
||||
-- Gets index of bast in ast (nil if not found).
|
||||
-- CATEGORY: AST query
|
||||
function M.ast_idx(ast, bast)
|
||||
for idx=1,#ast do
|
||||
if ast[idx] == bast then return idx end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
-- Gets parent of ast and index of ast in parent.
|
||||
-- Root node top_ast must also be provided. Returns nil, nil if ast is root.
|
||||
-- Note: may call mark_parents.
|
||||
-- CATEGORY: AST query
|
||||
function M.ast_parent_idx(top_ast, ast)
|
||||
if ast == top_ast then return nil, nil end
|
||||
M.ensure_parents_marked(top_ast); assert(ast.parent)
|
||||
local idx = M.ast_idx(ast.parent, ast)
|
||||
return ast.parent, idx
|
||||
end
|
||||
|
||||
|
||||
-- Gets common parent of aast and bast. Always returns value.
|
||||
-- Must provide root top_ast too.
|
||||
-- CATEGORY: AST query
|
||||
function M.common_ast_parent(aast, bast, top_ast)
|
||||
M.ensure_parents_marked(top_ast)
|
||||
local isparent = {}
|
||||
local tast = bast; repeat isparent[tast] = true; tast = tast.parent until not tast
|
||||
local uast = aast; repeat if isparent[uast] then return uast end; uast = uast.parent until not uast
|
||||
assert(false)
|
||||
end
|
||||
|
||||
|
||||
-- Replaces old_ast with new_ast/new_tokenlist in top_ast/tokenlist.
|
||||
-- Note: assumes new_ast is a block. assumes old_ast is a statement or block.
|
||||
-- CATEGORY: AST/tokenlist
|
||||
function M.replace_statements(top_ast, tokenlist, old_ast, new_ast, new_tokenlist)
|
||||
remove_ast_in_tokenlist(tokenlist, old_ast)
|
||||
insert_tokenlist(tokenlist, new_tokenlist)
|
||||
if old_ast == top_ast then -- special case: no parent
|
||||
M.switchtable(old_ast, new_ast) -- note: safe since block is not in tokenlist.
|
||||
else
|
||||
local parent_ast, idx = M.ast_parent_idx(top_ast, old_ast)
|
||||
table.remove(parent_ast, idx)
|
||||
tinsertlist(parent_ast, idx, new_ast)
|
||||
end
|
||||
|
||||
-- fixup annotations
|
||||
for _,bast in ipairs(new_ast) do
|
||||
if top_ast.tag2 then M.mark_tag2(bast, bast.tag == 'Do' and 'StatBlock' or 'Block') end
|
||||
if old_ast.parent then M.mark_parents(bast, old_ast.parent) end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Adjusts lineinfo in tokenlist.
|
||||
-- All char positions starting at pos1 are shifted by delta number of chars.
|
||||
-- CATEGORY: tokenlist
|
||||
function M.adjust_lineinfo(tokenlist, pos1, delta)
|
||||
for _,token in ipairs(tokenlist) do
|
||||
if token.fpos >= pos1 then
|
||||
token.fpos = token.fpos + delta
|
||||
end
|
||||
if token.lpos >= pos1 then
|
||||
token.lpos = token.lpos + delta
|
||||
end
|
||||
end
|
||||
--tokenlist.nbytes = tokenlist.nbytes + delta
|
||||
end
|
||||
|
||||
|
||||
-- For each node n in ast, sets n.parent to parent node of n.
|
||||
-- Assumes ast.parent will be parent_ast (may be nil)
|
||||
-- CATEGORY: AST query
|
||||
function M.mark_parents(ast, parent_ast)
|
||||
ast.parent = parent_ast
|
||||
for _,ast2 in ipairs(ast) do
|
||||
if type(ast2) == 'table' then
|
||||
M.mark_parents(ast2, ast)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Calls mark_parents(ast) if ast not marked.
|
||||
-- CATEGORY: AST query
|
||||
function M.ensure_parents_marked(ast)
|
||||
if ast[1] and not ast[1].parent then M.mark_parents(ast) end
|
||||
end
|
||||
|
||||
|
||||
-- For each node n in ast, sets n.tag2 to context string:
|
||||
-- 'Block' - node is block
|
||||
-- 'Stat' - node is statement
|
||||
-- 'StatBlock' - node is statement and block (i.e. `Do)
|
||||
-- 'Exp' - node is expression
|
||||
-- 'Explist' - node is expression list (or identifier list)
|
||||
-- 'Pair' - node is key-value pair in table constructor
|
||||
-- note: ast.tag2 will be set to context.
|
||||
-- CATEGORY: AST query
|
||||
local iscertainstat = {Do=true, Set=true, While=true, Repeat=true, If=true,
|
||||
Fornum=true, Forin=true, Local=true, Localrec=true, Return=true, Break=true}
|
||||
function M.mark_tag2(ast, context)
|
||||
context = context or 'Block'
|
||||
ast.tag2 = context
|
||||
for i,bast in ipairs(ast) do
|
||||
if type(bast) == 'table' then
|
||||
local nextcontext
|
||||
if bast.tag == 'Do' then
|
||||
nextcontext = 'StatBlock'
|
||||
elseif iscertainstat[bast.tag] then
|
||||
nextcontext = 'Stat'
|
||||
elseif bast.tag == 'Call' or bast.tag == 'Invoke' then
|
||||
nextcontext = context == 'Block' and 'Stat' or 'Exp'
|
||||
--DESIGN:Metalua: these calls actually contain expression lists,
|
||||
-- but the expression list is not represented as a complete node
|
||||
-- by Metalua (as blocks are in `Do statements)
|
||||
elseif bast.tag == 'Pair' then
|
||||
nextcontext = 'Pair'
|
||||
elseif not bast.tag then
|
||||
if ast.tag == 'Set' or ast.tag == 'Local' or ast.tag == 'Localrec'
|
||||
or ast.tag == 'Forin' and i <= 2
|
||||
or ast.tag == 'Function' and i == 1
|
||||
then
|
||||
nextcontext = 'Explist'
|
||||
else
|
||||
nextcontext = 'Block'
|
||||
end
|
||||
else
|
||||
nextcontext = 'Exp'
|
||||
end
|
||||
M.mark_tag2(bast, nextcontext)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Gets smallest statement or block containing or being `ast`.
|
||||
-- The AST root node `top_ast` must also be provided.
|
||||
-- Note: may decorate AST as side-effect (mark_tag2/mark_parents).
|
||||
-- top_ast is assumed a block, so this is always successful.
|
||||
-- CATEGORY: AST query
|
||||
function M.get_containing_statementblock(ast, top_ast)
|
||||
if not top_ast.tag2 then M.mark_tag2(top_ast) end
|
||||
if ast.tag2 == 'Stat' or ast.tag2 == 'StatBlock' or ast.tag2 == 'Block' then
|
||||
return ast
|
||||
else
|
||||
M.ensure_parents_marked(top_ast)
|
||||
return M.get_containing_statementblock(ast.parent, top_ast)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Finds smallest statement, block, or comment AST in ast/tokenlist containing position
|
||||
-- range [fpos, lpos]. If allowexpand is true (default nil) and located AST
|
||||
-- coincides with position range, then next containing statement is used
|
||||
-- instead (this allows multiple calls to further expand the statement selection).
|
||||
-- CATEGORY: AST query
|
||||
function M.select_statementblockcomment(ast, tokenlist, fpos, lpos, allowexpand)
|
||||
--IMPROVE: rename ast to top_ast
|
||||
local match_ast, comment_ast = M.smallest_ast_containing_range(ast, tokenlist, fpos, lpos)
|
||||
local select_ast = comment_ast or M.get_containing_statementblock(match_ast, ast)
|
||||
local nfpos, nlpos = M.ast_pos_range(select_ast, tokenlist)
|
||||
--DEBUG('s', nfpos, nlpos, fpos, lpos, match_ast.tag, select_ast.tag)
|
||||
if allowexpand and fpos == nfpos and lpos == nlpos then
|
||||
if comment_ast then
|
||||
-- Select enclosing statement.
|
||||
select_ast = match_ast
|
||||
nfpos, nlpos = M.ast_pos_range(select_ast, tokenlist)
|
||||
else
|
||||
-- note: multiple times may be needed to expand selection. For example, in
|
||||
-- `for x=1,2 do f() end` both the statement `f()` and block `f()` have
|
||||
-- the same position range.
|
||||
M.ensure_parents_marked(ast)
|
||||
while select_ast.parent and fpos == nfpos and lpos == nlpos do
|
||||
select_ast = M.get_containing_statementblock(select_ast.parent, ast)
|
||||
nfpos, nlpos = M.ast_pos_range(select_ast, tokenlist)
|
||||
end
|
||||
end
|
||||
end
|
||||
return nfpos, nlpos
|
||||
end
|
||||
|
||||
|
||||
-- Converts tokenlist to string representation for debugging.
|
||||
-- CATEGORY: tokenlist debug
|
||||
function M.dump_tokenlist(tokenlist)
|
||||
local ts = {}
|
||||
for i,token in ipairs(tokenlist) do
|
||||
ts[#ts+1] = 'tok.' .. i .. ': [' .. token.fpos .. ',' .. token.lpos .. '] '
|
||||
.. tostring(token[1]) .. ' ' .. tostring(token.ast.tag)
|
||||
end
|
||||
return table.concat(ts, '\n') -- .. 'nbytes=' .. tokenlist.nbytes .. '\n'
|
||||
end
|
||||
|
||||
|
||||
--FIX:Q: does this handle Unicode ok?
|
||||
|
||||
--FIX?:Metalua: fails on string with escape sequence '\/'. The Reference Manual
|
||||
-- doesn't say this sequence is valid though.
|
||||
|
||||
--FIX:Metalua: In `local --[[x]] function --[[y]] f() end`,
|
||||
-- 'x' comment omitted from AST.
|
||||
|
||||
--FIX:Metalua: `do --[[x]] end` doesn't generate comments in AST.
|
||||
-- `if x then --[[x]] end` and `while 1 do --[[x]] end` generates
|
||||
-- comments in first/last of block
|
||||
|
||||
--FIX:Metalua: `--[[x]] f() --[[y]]` returns lineinfo around `f()`.
|
||||
-- `--[[x]] --[[y]]` returns lineinfo around everything.
|
||||
|
||||
--FIX:Metalua: `while 1 do --[[x]] --[[y]] end` returns first > last
|
||||
-- lineinfo for contained block
|
||||
|
||||
--FIX:Metalua: search for "PATCHED:LuaInspect" in the metalualib folder.
|
||||
|
||||
--FIX?:Metalua: loadstring parses "--x" but metalua omits the comment in the AST
|
||||
|
||||
--FIX?:Metalua: `local x` is generating `Local{{`Id{x}}, {}}`, which
|
||||
-- has no lineinfo on {}. This is contrary to the Metalua
|
||||
-- spec: `Local{ {ident+} {expr+}? }.
|
||||
-- Other things like `self` also generate no lineinfo.
|
||||
-- The ast2.lineinfo above avoids this.
|
||||
|
||||
--FIX:Metalua: Metalua shouldn't overwrite ipairs/pairs. Note: Metalua version
|
||||
-- doesn't set errorlevel correctly.
|
||||
|
||||
--Q:Metalua: Why does `return --[[y]] z --[[x]]` have
|
||||
-- lineinfo.first.comments, lineinfo.last.comments,
|
||||
-- plus lineinfo.comments (which is the same as lineinfo.first.comments) ?
|
||||
|
||||
--CAUTION:Metalua: `do f() end` returns lineinfo around `do f() end`, while
|
||||
-- `while 1 do f() end` returns lineinfo around `f()` for inner block.
|
||||
|
||||
--CAUTION:Metalua: The lineinfo on Metalua comments is inconsistent with other
|
||||
-- nodes
|
||||
|
||||
--CAUTION:Metalua: lineinfo of table in `f{}` is [3,2], of `f{ x,y }` it's [4,6].
|
||||
-- This is inconsistent with `x={}` which is [3,4] and `f""` which is [1,2]
|
||||
-- for the string.
|
||||
|
||||
--CAUTION:Metalua: only the `function()` form of `Function includes `function`
|
||||
-- in lineinfo. 'function' is part of `Localrec and `Set in syntactic sugar form.
|
||||
|
||||
|
||||
--[=[TESTSUITE
|
||||
-- test longest_prefix/longest_postfix
|
||||
local function pr(text1, text2)
|
||||
local lastv
|
||||
local function same(v)
|
||||
assert(not lastv or v == lastv); lastv = v; return v
|
||||
end
|
||||
local function test1(text1, text2) -- test prefix/postfix
|
||||
same(longest_prefix(text1, text2))
|
||||
same(longest_postfix(text1:reverse(), text2:reverse()))
|
||||
end
|
||||
local function test2(text1, text2) -- test swap
|
||||
test1(text1, text2)
|
||||
test1(text2, text1)
|
||||
end
|
||||
for _,extra in ipairs{"", "x", "xy", "xyz"} do -- test extra chars
|
||||
test2(text1, text2..extra)
|
||||
test2(text2, text1..extra)
|
||||
end
|
||||
return lastv
|
||||
end
|
||||
check('==', pr("",""), 0)
|
||||
check('==', pr("a",""), 0)
|
||||
check('==', pr("a","a"), 1)
|
||||
check('==', pr("ab",""), 0)
|
||||
check('==', pr("ab","a"), 1)
|
||||
check('==', pr("ab","ab"), 2)
|
||||
check('==', pr("abcdefg","abcdefgh"), 7)
|
||||
--]=]
|
||||
|
||||
--[=[TESTSUITE
|
||||
print 'DONE'
|
||||
--]=]
|
||||
|
||||
|
||||
return M
|
||||
390
lualibs/luainspect/compat_env.lua
Normal file
390
lualibs/luainspect/compat_env.lua
Normal file
@@ -0,0 +1,390 @@
|
||||
--[[
|
||||
|
||||
compat_env v$(_VERSION) - Lua 5.1/5.2 environment compatibility functions
|
||||
|
||||
SYNOPSIS
|
||||
|
||||
-- Get load/loadfile compatibility functions only if using 5.1.
|
||||
local CL = pcall(load, '') and _G or require 'compat_env'
|
||||
local load = CL.load
|
||||
local loadfile = CL.loadfile
|
||||
|
||||
-- The following now works in both Lua 5.1 and 5.2:
|
||||
assert(load('return 2*pi', nil, 't', {pi=math.pi}))()
|
||||
assert(loadfile('ex.lua', 't', {print=print}))()
|
||||
|
||||
-- Get getfenv/setfenv compatibility functions only if using 5.2.
|
||||
local getfenv = _G.getfenv or require 'compat_env'.getfenv
|
||||
local setfenv = _G.setfenv or require 'compat_env'.setfenv
|
||||
local function f() return x end
|
||||
setfenv(f, {x=2})
|
||||
print(x, getfenv(f).x) --> 2, 2
|
||||
|
||||
DESCRIPTION
|
||||
|
||||
This module provides Lua 5.1/5.2 environment related compatibility functions.
|
||||
This includes implementations of Lua 5.2 style `load` and `loadfile`
|
||||
for use in Lua 5.1. It also includes Lua 5.1 style `getfenv` and `setfenv`
|
||||
for use in Lua 5.2.
|
||||
|
||||
API
|
||||
|
||||
local CL = require 'compat_env'
|
||||
|
||||
CL.load (ld [, source [, mode [, env] ] ]) --> f [, err]
|
||||
|
||||
This behaves the same as the Lua 5.2 `load` in both
|
||||
Lua 5.1 and 5.2.
|
||||
http://www.lua.org/manual/5.2/manual.html#pdf-load
|
||||
|
||||
CL.loadfile ([filename [, mode [, env] ] ]) --> f [, err]
|
||||
|
||||
This behaves the same as the Lua 5.2 `loadfile` in both
|
||||
Lua 5.1 and 5.2.
|
||||
http://www.lua.org/manual/5.2/manual.html#pdf-loadfile
|
||||
|
||||
CL.getfenv ([f]) --> t
|
||||
|
||||
This is identical to the Lua 5.1 `getfenv` in Lua 5.1.
|
||||
This behaves similar to the Lua 5.1 `getfenv` in Lua 5.2.
|
||||
When a global environment is to be returned, or when `f` is a
|
||||
C function, this returns `_G` since Lua 5.2 doesn't have
|
||||
(thread) global and C function environments. This will also
|
||||
return `_G` if the Lua function `f` lacks an `_ENV`
|
||||
upvalue, but it will raise an error if uncertain due to lack of
|
||||
debug info. It is not normally considered good design to use
|
||||
this function; when possible, use `load` or `loadfile` instead.
|
||||
http://www.lua.org/manual/5.1/manual.html#pdf-getfenv
|
||||
|
||||
CL.setfenv (f, t)
|
||||
|
||||
This is identical to the Lua 5.1 `setfenv` in Lua 5.1.
|
||||
This behaves similar to the Lua 5.1 `setfenv` in Lua 5.2.
|
||||
This will do nothing if `f` is a Lua function that
|
||||
lacks an `_ENV` upvalue, but it will raise an error if uncertain
|
||||
due to lack of debug info. See also Design Notes below.
|
||||
It is not normally considered good design to use
|
||||
this function; when possible, use `load` or `loadfile` instead.
|
||||
http://www.lua.org/manual/5.1/manual.html#pdf-setfenv
|
||||
|
||||
DESIGN NOTES
|
||||
|
||||
This module intends to provide robust and fairly complete reimplementations
|
||||
of the environment related Lua 5.1 and Lua 5.2 functions.
|
||||
No effort is made, however, to simulate rare or difficult to simulate features,
|
||||
such as thread environments, although this is liable to change in the future.
|
||||
Such 5.1 capabilities are discouraged and ideally
|
||||
removed from 5.1 code, thereby allowing your code to work in both 5.1 and 5.2.
|
||||
|
||||
In Lua 5.2, a `setfenv(f, {})`, where `f` lacks any upvalues, will be silently
|
||||
ignored since there is no `_ENV` in this function to write to, and the
|
||||
environment will have no effect inside the function anyway. However,
|
||||
this does mean that `getfenv(setfenv(f, t))` does not necessarily equal `t`,
|
||||
which is incompatible with 5.1 code (a possible workaround would be [1]).
|
||||
If `setfenv(f, {})` has an upvalue but no debug info, then this will raise
|
||||
an error to prevent inadvertently executing potentially untrusted code in the
|
||||
global environment.
|
||||
|
||||
It is not normally considered good design to use `setfenv` and `getfenv`
|
||||
(one reason they were removed in 5.2). When possible, consider replacing
|
||||
these with `load` or `loadfile`, which are more restrictive and have native
|
||||
implementations in 5.2.
|
||||
|
||||
This module might be merged into a more general Lua 5.1/5.2 compatibility
|
||||
library (e.g. a full reimplementation of Lua 5.2 `_G`). However,
|
||||
`load/loadfile/getfenv/setfenv` perhaps are among the more cumbersome
|
||||
functions not to have.
|
||||
|
||||
INSTALLATION
|
||||
|
||||
Download compat_env.lua:
|
||||
|
||||
wget https://raw.github.com/gist/1654007/compat_env.lua
|
||||
|
||||
Copy compat_env.lua into your LUA_PATH.
|
||||
|
||||
Alternately, unpack, test, and install into LuaRocks:
|
||||
|
||||
wget https://raw.github.com/gist/1422205/sourceunpack.lua
|
||||
lua sourceunpack.lua compat_env.lua
|
||||
(cd out && luarocks make)
|
||||
|
||||
Related work
|
||||
|
||||
http://lua-users.org/wiki/LuaVersionCompatibility
|
||||
https://github.com/stevedonovan/Penlight/blob/master/lua/pl/utils.lua
|
||||
- penlight implementations of getfenv/setfenv
|
||||
http://lua-users.org/lists/lua-l/2010-06/msg00313.html
|
||||
- initial getfenv/setfenv implementation
|
||||
|
||||
References
|
||||
|
||||
[1] http://lua-users.org/lists/lua-l/2010-06/msg00315.html
|
||||
|
||||
Copyright
|
||||
|
||||
(c) 2012 David Manura. Licensed under the same terms as Lua 5.1/5.2 (MIT license).
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--]]---------------------------------------------------------------------
|
||||
|
||||
local M = {_TYPE='module', _NAME='compat_env', _VERSION='0.2.20120124'}
|
||||
|
||||
local function check_chunk_type(s, mode)
|
||||
local nmode = mode or 'bt'
|
||||
local is_binary = s and #s > 0 and s:byte(1) == 27
|
||||
if is_binary and not nmode:match'b' then
|
||||
return nil, ("attempt to load a binary chunk (mode is '%s')"):format(mode)
|
||||
elseif not is_binary and not nmode:match't' then
|
||||
return nil, ("attempt to load a text chunk (mode is '%s')"):format(mode)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local IS_52_LOAD = pcall(load, '')
|
||||
if IS_52_LOAD then
|
||||
M.load = _G.load
|
||||
M.loadfile = _G.loadfile
|
||||
else
|
||||
-- 5.2 style `load` implemented in 5.1
|
||||
function M.load(ld, source, mode, env)
|
||||
local f
|
||||
if type(ld) == 'string' then
|
||||
local s = ld
|
||||
local ok, err = check_chunk_type(s, mode); if not ok then return ok, err end
|
||||
local err; f, err = loadstring(s, source); if not f then return f, err end
|
||||
elseif type(ld) == 'function' then
|
||||
local ld2 = ld
|
||||
if (mode or 'bt') ~= 'bt' then
|
||||
local first = ld()
|
||||
local ok, err = check_chunk_type(first, mode); if not ok then return ok, err end
|
||||
ld2 = function()
|
||||
if first then
|
||||
local chunk=first; first=nil; return chunk
|
||||
else return ld() end
|
||||
end
|
||||
end
|
||||
local err; f, err = load(ld2, source); if not f then return f, err end
|
||||
else
|
||||
error(("bad argument #1 to 'load' (function expected, got %s)"):format(type(ld)), 2)
|
||||
end
|
||||
if env then setfenv(f, env) end
|
||||
return f
|
||||
end
|
||||
|
||||
-- 5.2 style `loadfile` implemented in 5.1
|
||||
function M.loadfile(filename, mode, env)
|
||||
if (mode or 'bt') ~= 'bt' then
|
||||
local ioerr
|
||||
local fh, err = io.open(filename, 'rb'); if not fh then return fh, err end
|
||||
local function ld() local chunk; chunk,ioerr = fh:read(4096); return chunk end
|
||||
local f, err = M.load(ld, filename and '@'..filename, mode, env)
|
||||
fh:close()
|
||||
if not f then return f, err end
|
||||
if ioerr then return nil, ioerr end
|
||||
return f
|
||||
else
|
||||
local f, err = loadfile(filename); if not f then return f, err end
|
||||
if env then setfenv(f, env) end
|
||||
return f
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if _G.setfenv then -- Lua 5.1
|
||||
M.setfenv = _G.setfenv
|
||||
M.getfenv = _G.getfenv
|
||||
else -- >= Lua 5.2
|
||||
-- helper function for `getfenv`/`setfenv`
|
||||
local function envlookup(f)
|
||||
local name, val
|
||||
local up = 0
|
||||
local unknown
|
||||
repeat
|
||||
up=up+1; name, val = debug.getupvalue(f, up)
|
||||
if name == '' then unknown = true end
|
||||
until name == '_ENV' or name == nil
|
||||
if name ~= '_ENV' then
|
||||
up = nil
|
||||
if unknown then error("upvalues not readable in Lua 5.2 when debug info missing", 3) end
|
||||
end
|
||||
return (name == '_ENV') and up, val, unknown
|
||||
end
|
||||
|
||||
-- helper function for `getfenv`/`setfenv`
|
||||
local function envhelper(f, name)
|
||||
if type(f) == 'number' then
|
||||
if f < 0 then
|
||||
error(("bad argument #1 to '%s' (level must be non-negative)"):format(name), 3)
|
||||
elseif f < 1 then
|
||||
error("thread environments unsupported in Lua 5.2", 3) --[*]
|
||||
end
|
||||
f = debug.getinfo(f+2, 'f').func
|
||||
elseif type(f) ~= 'function' then
|
||||
error(("bad argument #1 to '%s' (number expected, got %s)"):format(type(name, f)), 2)
|
||||
end
|
||||
return f
|
||||
end
|
||||
-- [*] might simulate with table keyed by coroutine.running()
|
||||
|
||||
-- 5.1 style `setfenv` implemented in 5.2
|
||||
function M.setfenv(f, t)
|
||||
local f = envhelper(f, 'setfenv')
|
||||
local up, val, unknown = envlookup(f)
|
||||
if up then
|
||||
debug.upvaluejoin(f, up, function() return up end, 1) -- unique upvalue [*]
|
||||
debug.setupvalue(f, up, t)
|
||||
else
|
||||
local what = debug.getinfo(f, 'S').what
|
||||
if what ~= 'Lua' and what ~= 'main' then -- not Lua func
|
||||
error("'setfenv' cannot change environment of given object", 2)
|
||||
end -- else ignore no _ENV upvalue (warning: incompatible with 5.1)
|
||||
end
|
||||
end
|
||||
-- [*] http://lua-users.org/lists/lua-l/2010-06/msg00313.html
|
||||
|
||||
-- 5.1 style `getfenv` implemented in 5.2
|
||||
function M.getfenv(f)
|
||||
if f == 0 or f == nil then return _G end -- simulated behavior
|
||||
local f = envhelper(f, 'setfenv')
|
||||
local up, val = envlookup(f)
|
||||
if not up then return _G end -- simulated behavior [**]
|
||||
return val
|
||||
end
|
||||
-- [**] possible reasons: no _ENV upvalue, C function
|
||||
end
|
||||
|
||||
|
||||
return M
|
||||
|
||||
--[[ FILE rockspec.in
|
||||
|
||||
package = 'compat_env'
|
||||
version = '$(_VERSION)-1'
|
||||
source = {
|
||||
url = 'https://raw.github.com/gist/1654007/$(GITID)/compat_env.lua',
|
||||
--url = 'https://raw.github.com/gist/1654007/compat_env.lua', -- latest raw
|
||||
--url = 'https://gist.github.com/gists/1654007/download',
|
||||
md5 = '$(MD5)'
|
||||
}
|
||||
description = {
|
||||
summary = 'Lua 5.1/5.2 environment compatibility functions',
|
||||
detailed = [=[
|
||||
Provides Lua 5.1/5.2 environment related compatibility functions.
|
||||
This includes implementations of Lua 5.2 style `load` and `loadfile`
|
||||
for use in Lua 5.1. It also includes Lua 5.1 style `getfenv` and `setfenv`
|
||||
for use in Lua 5.2.
|
||||
]=],
|
||||
license = 'MIT/X11',
|
||||
homepage = 'https://gist.github.com/1654007',
|
||||
maintainer = 'David Manura'
|
||||
}
|
||||
dependencies = {} -- Lua 5.1 or 5.2
|
||||
build = {
|
||||
type = 'builtin',
|
||||
modules = {
|
||||
['compat_env'] = 'compat_env.lua'
|
||||
}
|
||||
}
|
||||
|
||||
--]]---------------------------------------------------------------------
|
||||
|
||||
--[[ FILE test.lua
|
||||
|
||||
-- test.lua - test suite for compat_env module.
|
||||
|
||||
local CL = require 'compat_env'
|
||||
local load = CL.load
|
||||
local loadfile = CL.loadfile
|
||||
local setfenv = CL.setfenv
|
||||
local getfenv = CL.getfenv
|
||||
|
||||
local function checkeq(a, b, e)
|
||||
if a ~= b then error(
|
||||
'not equal ['..tostring(a)..'] ['..tostring(b)..'] ['..tostring(e)..']')
|
||||
end
|
||||
end
|
||||
local function checkerr(pat, ok, err)
|
||||
assert(not ok, 'checkerr')
|
||||
assert(type(err) == 'string' and err:match(pat), err)
|
||||
end
|
||||
|
||||
-- test `load`
|
||||
checkeq(load('return 2')(), 2)
|
||||
checkerr('expected near', load'return 2 2')
|
||||
checkerr('text chunk', load('return 2', nil, 'b'))
|
||||
checkerr('text chunk', load('', nil, 'b'))
|
||||
checkerr('binary chunk', load('\027', nil, 't'))
|
||||
checkeq(load('return 2*x',nil,'bt',{x=5})(), 10)
|
||||
checkeq(debug.getinfo(load('')).source, '')
|
||||
checkeq(debug.getinfo(load('', 'foo')).source, 'foo')
|
||||
|
||||
-- test `loadfile`
|
||||
local fh = assert(io.open('tmp.lua', 'wb'))
|
||||
fh:write('return (...) or x')
|
||||
fh:close()
|
||||
checkeq(loadfile('tmp.lua')(2), 2)
|
||||
checkeq(loadfile('tmp.lua', 't')(2), 2)
|
||||
checkerr('text chunk', loadfile('tmp.lua', 'b'))
|
||||
checkeq(loadfile('tmp.lua', nil, {x=3})(), 3)
|
||||
checkeq(debug.getinfo(loadfile('tmp.lua')).source, '@tmp.lua')
|
||||
checkeq(debug.getinfo(loadfile('tmp.lua', 't', {})).source, '@tmp.lua')
|
||||
os.remove'tmp.lua'
|
||||
|
||||
-- test `setfenv`/`getfenv`
|
||||
x = 5
|
||||
local a,b=true; local function f(c) if a then return x,b,c end end
|
||||
setfenv(f, {x=3})
|
||||
checkeq(f(), 3)
|
||||
checkeq(getfenv(f).x, 3)
|
||||
checkerr('cannot change', pcall(setfenv, string.len, {})) -- C function
|
||||
checkeq(getfenv(string.len), _G) -- C function
|
||||
local function g()
|
||||
setfenv(1, {x=4})
|
||||
checkeq(getfenv(1).x, 4)
|
||||
return x
|
||||
end
|
||||
checkeq(g(), 4) -- numeric level
|
||||
if _G._VERSION ~= 'Lua 5.1' then
|
||||
checkerr('unsupported', pcall(setfenv, 0, {}))
|
||||
end
|
||||
checkeq(getfenv(0), _G)
|
||||
checkeq(getfenv(), _G) -- no arg
|
||||
checkeq(x, 5) -- main unaltered
|
||||
setfenv(function()end, {}) -- no upvalues, ignore
|
||||
checkeq(getfenv(function()end), _G) -- no upvaluse
|
||||
if _G._VERSION ~= 'Lua 5.1' then
|
||||
checkeq(getfenv(setfenv(function()end, {})), _G) -- warning: incompatible with 5.1
|
||||
end
|
||||
x = nil
|
||||
|
||||
print 'OK'
|
||||
|
||||
--]]---------------------------------------------------------------------
|
||||
|
||||
--[[ FILE CHANGES.txt
|
||||
0.2.20120124
|
||||
Renamed module to compat_env (from compat_load)
|
||||
Add getfenv/setfenv functions
|
||||
|
||||
0.1.20120121
|
||||
Initial public release
|
||||
--]]
|
||||
|
||||
90
lualibs/luainspect/dump.lua
Normal file
90
lualibs/luainspect/dump.lua
Normal file
@@ -0,0 +1,90 @@
|
||||
-- Recursive object dumper, for debugging.
|
||||
-- (c) 2010 David Manura, MIT License.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- My own object dumper.
|
||||
-- Intended for debugging, not serialization, with compact formatting.
|
||||
-- Robust against recursion.
|
||||
-- Renders Metalua table tag fields specially {tag=X, ...} --> "`X{...}".
|
||||
-- On first call, only pass parameter o.
|
||||
-- CATEGORY: AST debug
|
||||
local ignore_keys_ = {lineinfo=true}
|
||||
local norecurse_keys_ = {parent=true, ast=true}
|
||||
local function dumpstring_key_(k, isseen, newindent)
|
||||
local ks = type(k) == 'string' and k:match'^[%a_][%w_]*$' and k or
|
||||
'[' .. M.dumpstring(k, isseen, newindent) .. ']'
|
||||
return ks
|
||||
end
|
||||
local function sort_keys_(a, b)
|
||||
if type(a) == 'number' and type(b) == 'number' then
|
||||
return a < b
|
||||
elseif type(a) == 'number' then
|
||||
return false
|
||||
elseif type(b) == 'number' then
|
||||
return true
|
||||
elseif type(a) == 'string' and type(b) == 'string' then
|
||||
return a < b
|
||||
else
|
||||
return tostring(a) < tostring(b) -- arbitrary
|
||||
end
|
||||
end
|
||||
function M.dumpstring(o, isseen, indent, key)
|
||||
isseen = isseen or {}
|
||||
indent = indent or ''
|
||||
|
||||
if type(o) == 'table' then
|
||||
if isseen[o] or norecurse_keys_[key] then
|
||||
return (type(o.tag) == 'string' and '`' .. o.tag .. ':' or '') .. tostring(o)
|
||||
else isseen[o] = true end -- avoid recursion
|
||||
|
||||
local used = {}
|
||||
|
||||
local tag = o.tag
|
||||
local s = '{'
|
||||
if type(o.tag) == 'string' then
|
||||
s = '`' .. tag .. s; used['tag'] = true
|
||||
end
|
||||
local newindent = indent .. ' '
|
||||
|
||||
local ks = {}; for k in pairs(o) do ks[#ks+1] = k end
|
||||
table.sort(ks, sort_keys_)
|
||||
--for i,k in ipairs(ks) do print ('keys', k) end
|
||||
|
||||
local forcenummultiline
|
||||
for k in pairs(o) do
|
||||
if type(k) == 'number' and type(o[k]) == 'table' then forcenummultiline = true end
|
||||
end
|
||||
|
||||
-- inline elements
|
||||
for _,k in ipairs(ks) do
|
||||
if used[k] then -- skip
|
||||
elseif ignore_keys_[k] then used[k] = true
|
||||
elseif (type(k) ~= 'number' or not forcenummultiline) and
|
||||
type(k) ~= 'table' and (type(o[k]) ~= 'table' or norecurse_keys_[k])
|
||||
then
|
||||
s = s .. dumpstring_key_(k, isseen, newindent) .. '=' .. M.dumpstring(o[k], isseen, newindent, k) .. ', '
|
||||
used[k] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- elements on separate lines
|
||||
local done
|
||||
for _,k in ipairs(ks) do
|
||||
if not used[k] then
|
||||
if not done then s = s .. '\n'; done = true end
|
||||
s = s .. newindent .. dumpstring_key_(k, isseen) .. '=' .. M.dumpstring(o[k], isseen, newindent, k) .. ',\n'
|
||||
end
|
||||
end
|
||||
s = s:gsub(',(%s*)$', '%1')
|
||||
s = s .. (done and indent or '') .. '}'
|
||||
return s
|
||||
elseif type(o) == 'string' then
|
||||
return string.format('%q', o)
|
||||
else
|
||||
return tostring(o)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
222
lualibs/luainspect/globals.lua
Normal file
222
lualibs/luainspect/globals.lua
Normal file
@@ -0,0 +1,222 @@
|
||||
-- LuaInspect.globals - identifier scope analysis
|
||||
-- Locates locals, globals, and their definitions.
|
||||
--
|
||||
-- (c) D.Manura, 2008-2010, MIT license.
|
||||
|
||||
-- based on http://lua-users.org/wiki/DetectingUndefinedVariables
|
||||
|
||||
local M = {}
|
||||
|
||||
--! require 'luainspect.typecheck' (context)
|
||||
|
||||
local LA = require "luainspect.ast"
|
||||
|
||||
local function definelocal(scope, name, ast)
|
||||
if scope[name] then
|
||||
scope[name].localmasked = true
|
||||
ast.localmasking = scope[name]
|
||||
end
|
||||
scope[name] = ast
|
||||
if name == '_' then ast.isignore = true end
|
||||
end
|
||||
|
||||
-- Resolves scoping and usages of variable in AST.
|
||||
-- Data Notes:
|
||||
-- ast.localdefinition refers to lexically scoped definition of `Id node `ast`.
|
||||
-- If ast.localdefinition == ast then ast is a "lexical definition".
|
||||
-- If ast.localdefinition == nil, then variable is global.
|
||||
-- ast.functionlevel is the number of functions the AST is contained in.
|
||||
-- ast.functionlevel is defined iff ast is a lexical definition.
|
||||
-- ast.isparam is true iff ast is a lexical definition and a function parameter.
|
||||
-- ast.isset is true iff ast is a lexical definition and exists an assignment on it.
|
||||
-- ast.isused is true iff ast is a lexical definition and has been referred to.
|
||||
-- ast.isignore is true if local variable should be ignored (e.g. typically "_")
|
||||
-- ast.localmasking - for a lexical definition, this is set to the lexical definition
|
||||
-- this is masking (i.e. same name). nil if not masking.
|
||||
-- ast.localmasked - true iff lexical definition masked by another lexical definition.
|
||||
-- ast.isfield is true iff `String node ast is used for field access on object,
|
||||
-- e.g. x.y or x['y'].z
|
||||
-- ast.previous - For `Index{o,s} or `Invoke{o,s,...}, s.previous == o
|
||||
local function traverse(ast, scope, globals, level, functionlevel)
|
||||
scope = scope or {}
|
||||
|
||||
local blockrecurse
|
||||
ast.level = level
|
||||
|
||||
-- operations on walking down the AST
|
||||
if ast.tag == 'Local' then
|
||||
blockrecurse = 1
|
||||
-- note: apply new scope after processing values
|
||||
elseif ast.tag == 'Localrec' then
|
||||
local namelist_ast, valuelist_ast = ast[1], ast[2]
|
||||
for _,value_ast in ipairs(namelist_ast) do
|
||||
assert(value_ast.tag == 'Id')
|
||||
local name = value_ast[1]
|
||||
local parentscope = getmetatable(scope).__index
|
||||
definelocal(parentscope, name, value_ast)
|
||||
value_ast.localdefinition = value_ast
|
||||
value_ast.functionlevel = functionlevel
|
||||
value_ast.level = level+1
|
||||
end
|
||||
blockrecurse = 1
|
||||
elseif ast.tag == 'Id' then
|
||||
local name = ast[1]
|
||||
if scope[name] then
|
||||
ast.localdefinition = scope[name]
|
||||
ast.functionlevel = functionlevel
|
||||
scope[name].isused = true
|
||||
else -- global, do nothing
|
||||
end
|
||||
elseif ast.tag == 'Function' then
|
||||
local paramlist_ast, body_ast = ast[1], ast[2]
|
||||
functionlevel = functionlevel + 1
|
||||
for _,param_ast in ipairs(paramlist_ast) do
|
||||
local name = param_ast[1]
|
||||
assert(param_ast.tag == 'Id' or param_ast.tag == 'Dots')
|
||||
if param_ast.tag == 'Id' then
|
||||
definelocal(scope, name, param_ast)
|
||||
param_ast.localdefinition = param_ast
|
||||
param_ast.functionlevel = functionlevel
|
||||
param_ast.isparam = true
|
||||
end
|
||||
param_ast.level = level+1
|
||||
end
|
||||
blockrecurse = 1
|
||||
elseif ast.tag == 'Set' then
|
||||
local reflist_ast, valuelist_ast = ast[1], ast[2]
|
||||
for _,ref_ast in ipairs(reflist_ast) do
|
||||
if ref_ast.tag == 'Id' then
|
||||
local name = ref_ast[1]
|
||||
if scope[name] then
|
||||
scope[name].isset = true
|
||||
else
|
||||
if not globals[name] then
|
||||
globals[name] = {set=ref_ast}
|
||||
end
|
||||
end
|
||||
end
|
||||
ref_ast.level = level+1
|
||||
end
|
||||
--ENHANCE? We could differentiate assignments to x (which indicates that
|
||||
-- x is not const) and assignments to a member of x (which indicates that
|
||||
-- x is not a pointer to const) and assignments to any nested member of x
|
||||
-- (which indicates that x it not a transitive const).
|
||||
elseif ast.tag == 'Fornum' then
|
||||
blockrecurse = 1
|
||||
elseif ast.tag == 'Forin' then
|
||||
blockrecurse = 1
|
||||
end
|
||||
|
||||
-- recurse (depth-first search down the AST)
|
||||
if ast.tag == 'Repeat' then
|
||||
local block_ast, cond_ast = ast[1], ast[2]
|
||||
local scope = scope
|
||||
for _,stat_ast in ipairs(block_ast) do
|
||||
scope = setmetatable({}, {__index = scope})
|
||||
traverse(stat_ast, scope, globals, level+1, functionlevel)
|
||||
end
|
||||
scope = setmetatable({}, {__index = scope})
|
||||
traverse(cond_ast, scope, globals, level+1, functionlevel)
|
||||
elseif ast.tag == 'Fornum' then
|
||||
local name_ast, block_ast = ast[1], ast[#ast]
|
||||
-- eval value list in current scope
|
||||
for i=2, #ast-1 do traverse(ast[i], scope, globals, level+1, functionlevel) end
|
||||
-- eval body in next scope
|
||||
local name = name_ast[1]
|
||||
definelocal(scope, name, name_ast)
|
||||
name_ast.localdefinition = name_ast
|
||||
name_ast.functionlevel = functionlevel
|
||||
traverse(block_ast, scope, globals, level+1, functionlevel)
|
||||
elseif ast.tag == 'Forin' then
|
||||
local namelist_ast, vallist_ast, block_ast = ast[1], ast[2], ast[3]
|
||||
-- eval value list in current scope
|
||||
traverse(vallist_ast, scope, globals, level+1, functionlevel)
|
||||
-- eval body in next scope
|
||||
for _,name_ast in ipairs(namelist_ast) do
|
||||
local name = name_ast[1]
|
||||
definelocal(scope, name, name_ast)
|
||||
name_ast.localdefinition = name_ast
|
||||
name_ast.functionlevel = functionlevel
|
||||
name_ast.level = level+1
|
||||
end
|
||||
traverse(block_ast, scope, globals, level+1, functionlevel)
|
||||
else -- normal
|
||||
for i,v in ipairs(ast) do
|
||||
if i ~= blockrecurse and type(v) == 'table' then
|
||||
local scope = setmetatable({}, {__index = scope})
|
||||
traverse(v, scope, globals, level+1, functionlevel)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- operations on walking up the AST
|
||||
if ast.tag == 'Local' then
|
||||
-- Unlike Localrec, variables come into scope after evaluating values.
|
||||
local namelist_ast, valuelist_ast = ast[1], ast[2]
|
||||
for _,name_ast in ipairs(namelist_ast) do
|
||||
assert(name_ast.tag == 'Id')
|
||||
local name = name_ast[1]
|
||||
local parentscope = getmetatable(scope).__index
|
||||
definelocal(parentscope, name, name_ast)
|
||||
name_ast.localdefinition = name_ast
|
||||
name_ast.functionlevel = functionlevel
|
||||
name_ast.level = level+1
|
||||
end
|
||||
elseif ast.tag == 'Index' then
|
||||
if ast[2].tag == 'String' then
|
||||
ast[2].isfield = true
|
||||
ast[2].previous = ast[1]
|
||||
end
|
||||
elseif ast.tag == 'Invoke' then
|
||||
assert(ast[2].tag == 'String')
|
||||
ast[2].isfield = true
|
||||
ast[2].previous = ast[1]
|
||||
end
|
||||
end
|
||||
|
||||
function M.globals(ast)
|
||||
-- Default list of defined variables.
|
||||
local scope = setmetatable({}, {})
|
||||
local globals = {}
|
||||
traverse(ast, scope, globals, 1, 1) -- Start check.
|
||||
|
||||
return globals
|
||||
end
|
||||
|
||||
|
||||
-- Gets locals in scope of statement of block ast. If isafter is true and ast is statement,
|
||||
-- uses scope just after statement ast.
|
||||
-- Assumes 'parent' attributes on ast are marked.
|
||||
-- Returns table mapping name -> AST local definition.
|
||||
function M.variables_in_scope(ast, isafter)
|
||||
local scope = {}
|
||||
local cast = ast
|
||||
while cast.parent do
|
||||
local midx = LA.ast_idx(cast.parent, cast)
|
||||
for idx=1,midx do
|
||||
local bast = cast.parent[idx]
|
||||
if bast.tag == 'Localrec' or bast.tag == 'Local' and (idx < midx or isafter) then
|
||||
local names_ast = bast[1]
|
||||
for bidx=1,#names_ast do
|
||||
local name_ast = names_ast[bidx]
|
||||
local name = name_ast[1]
|
||||
scope[name] = name_ast
|
||||
end
|
||||
elseif cast ~= ast and (bast.tag == 'For' or bast.tag == 'Forin' or bast.tag == 'Function') then
|
||||
local names_ast = bast[1]
|
||||
for bidx=1,#names_ast do
|
||||
local name_ast = names_ast[bidx]
|
||||
if name_ast.tag == 'Id' then --Q: or maybe `Dots should be included
|
||||
local name = name_ast[1]
|
||||
scope[name] = name_ast
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
cast = cast.parent
|
||||
end
|
||||
return scope
|
||||
end
|
||||
|
||||
|
||||
return M
|
||||
1431
lualibs/luainspect/init.lua
Normal file
1431
lualibs/luainspect/init.lua
Normal file
File diff suppressed because it is too large
Load Diff
433
lualibs/luainspect/signatures.lua
Normal file
433
lualibs/luainspect/signatures.lua
Normal file
@@ -0,0 +1,433 @@
|
||||
local M = {}
|
||||
|
||||
local T = require "luainspect.types"
|
||||
|
||||
-- signatures of known globals
|
||||
M.global_signatures = {
|
||||
assert = "assert (v [, message])",
|
||||
collectgarbage = "collectgarbage (opt [, arg])",
|
||||
dofile = "dofile (filename)",
|
||||
error = "error (message [, level])",
|
||||
_G = "(table)",
|
||||
getfenv = "getfenv ([f])",
|
||||
getmetatable = "getmetatable (object)",
|
||||
ipairs = "ipairs (t)",
|
||||
load = "load (func [, chunkname])",
|
||||
loadfile = "loadfile ([filename])",
|
||||
loadstring = "loadstring (string [, chunkname])",
|
||||
next = "next (table [, index])",
|
||||
pairs = "pairs (t)",
|
||||
pcall = "pcall (f, arg1, ...)",
|
||||
print = "print (...)",
|
||||
rawequal = "rawequal (v1, v2)",
|
||||
rawget = "rawget (table, index)",
|
||||
rawset = "rawset (table, index, value)",
|
||||
select = "select (index, ...)",
|
||||
setfenv = "setfenv (f, table)",
|
||||
setmetatable = "setmetatable (table, metatable)",
|
||||
tonumber = "tonumber (e [, base])",
|
||||
tostring = "tostring (e)",
|
||||
type = "type (v)",
|
||||
unpack = "unpack (list [, i [, j]])",
|
||||
_VERSION = "(string)",
|
||||
xpcall = "xpcall (f, err)",
|
||||
module = "module (name [, ...])",
|
||||
require = "require (modname)",
|
||||
coroutine = "(table) coroutine manipulation library",
|
||||
debug = "(table) debug facilities library",
|
||||
io = "(table) I/O library",
|
||||
math = "(table) math functions libary",
|
||||
os = "(table) OS facilities library",
|
||||
package = "(table) package library",
|
||||
string = "(table) string manipulation library",
|
||||
table = "(table) table manipulation library",
|
||||
["coroutine.create"] = "coroutine.create (f)",
|
||||
["coroutine.resume"] = "coroutine.resume (co [, val1, ...])",
|
||||
["coroutine.running"] = "coroutine.running ()",
|
||||
["coroutine.status"] = "coroutine.status (co)",
|
||||
["coroutine.wrap"] = "coroutine.wrap (f)",
|
||||
["coroutine.yield"] = "coroutine.yield (...)",
|
||||
["debug.debug"] = "debug.debug ()",
|
||||
["debug.getfenv"] = "debug.getfenv (o)",
|
||||
["debug.gethook"] = "debug.gethook ([thread])",
|
||||
["debug.getinfo"] = "debug.getinfo ([thread,] function [, what])",
|
||||
["debug.getlocal"] = "debug.getlocal ([thread,] level, local)",
|
||||
["debug.getmetatable"] = "debug.getmetatable (object)",
|
||||
["debug.getregistry"] = "debug.getregistry ()",
|
||||
["debug.getupvalue"] = "debug.getupvalue (func, up)",
|
||||
["debug.setfenv"] = "debug.setfenv (object, table)",
|
||||
["debug.sethook"] = "debug.sethook ([thread,] hook, mask [, count])",
|
||||
["debug.setlocal"] = "debug.setlocal ([thread,] level, local, value)",
|
||||
["debug.setmetatable"] = "debug.setmetatable (object, table)",
|
||||
["debug.setupvalue"] = "debug.setupvalue (func, up, value)",
|
||||
["debug.traceback"] = "debug.traceback ([thread,] [message] [, level])",
|
||||
["io.close"] = "io.close ([file])",
|
||||
["io.flush"] = "io.flush ()",
|
||||
["io.input"] = "io.input ([file])",
|
||||
["io.lines"] = "io.lines ([filename])",
|
||||
["io.open"] = "io.open (filename [, mode])",
|
||||
["io.output"] = "io.output ([file])",
|
||||
["io.popen"] = "io.popen (prog [, mode])",
|
||||
["io.read"] = "io.read (...)",
|
||||
["io.tmpfile"] = "io.tmpfile ()",
|
||||
["io.type"] = "io.type (obj)",
|
||||
["io.write"] = "io.write (...)",
|
||||
["math.abs"] = "math.abs (x)",
|
||||
["math.acos"] = "math.acos (x)",
|
||||
["math.asin"] = "math.asin (x)",
|
||||
["math.atan"] = "math.atan (x)",
|
||||
["math.atan2"] = "math.atan2 (y, x)",
|
||||
["math.ceil"] = "math.ceil (x)",
|
||||
["math.cos"] = "math.cos (x)",
|
||||
["math.cosh"] = "math.cosh (x)",
|
||||
["math.deg"] = "math.deg (x)",
|
||||
["math.exp"] = "math.exp (x)",
|
||||
["math.floor"] = "math.floor (x)",
|
||||
["math.fmod"] = "math.fmod (x, y)",
|
||||
["math.frexp"] = "math.frexp (x)",
|
||||
["math.huge"] = "math.huge",
|
||||
["math.ldexp"] = "math.ldexp (m, e)",
|
||||
["math.log"] = "math.log (x)",
|
||||
["math.log10"] = "math.log10 (x)",
|
||||
["math.max"] = "math.max (x, ...)",
|
||||
["math.min"] = "math.min (x, ...)",
|
||||
["math.modf"] = "math.modf (x)",
|
||||
["math.pi"] = "math.pi",
|
||||
["math.pow"] = "math.pow (x, y)",
|
||||
["math.rad"] = "math.rad (x)",
|
||||
["math.random"] = "math.random ([m [, n]])",
|
||||
["math.randomseed"] = "math.randomseed (x)",
|
||||
["math.sin"] = "math.sin (x)",
|
||||
["math.sinh"] = "math.sinh (x)",
|
||||
["math.sqrt"] = "math.sqrt (x)",
|
||||
["math.tan"] = "math.tan (x)",
|
||||
["math.tanh"] = "math.tanh (x)",
|
||||
["os.clock"] = "os.clock ()",
|
||||
["os.date"] = "os.date ([format [, time]])",
|
||||
["os.difftime"] = "os.difftime (t2, t1)",
|
||||
["os.execute"] = "os.execute ([command])",
|
||||
["os.exit"] = "os.exit ([code])",
|
||||
["os.getenv"] = "os.getenv (varname)",
|
||||
["os.remove"] = "os.remove (filename)",
|
||||
["os.rename"] = "os.rename (oldname, newname)",
|
||||
["os.setlocale"] = "os.setlocale (locale [, category])",
|
||||
["os.time"] = "os.time ([table])",
|
||||
["os.tmpname"] = "os.tmpname ()",
|
||||
["package.cpath"] = "package.cpath",
|
||||
["package.loaded"] = "package.loaded",
|
||||
["package.loaders"] = "package.loaders",
|
||||
["package.loadlib"] = "package.loadlib (libname, funcname)",
|
||||
["package.path"] = "package.path",
|
||||
["package.preload"] = "package.preload",
|
||||
["package.seeall"] = "package.seeall (module)",
|
||||
["string.byte"] = "string.byte (s [, i [, j]])",
|
||||
["string.char"] = "string.char (...)",
|
||||
["string.dump"] = "string.dump (function)",
|
||||
["string.find"] = "string.find (s, pattern [, init [, plain]])",
|
||||
["string.format"] = "string.format (formatstring, ...)",
|
||||
["string.gmatch"] = "string.gmatch (s, pattern)",
|
||||
["string.gsub"] = "string.gsub (s, pattern, repl [, n])",
|
||||
["string.len"] = "string.len (s)",
|
||||
["string.lower"] = "string.lower (s)",
|
||||
["string.match"] = "string.match (s, pattern [, init])",
|
||||
["string.rep"] = "string.rep (s, n)",
|
||||
["string.reverse"] = "string.reverse (s)",
|
||||
["string.sub"] = "string.sub (s, i [, j])",
|
||||
["string.upper"] = "string.upper (s)",
|
||||
["table.concat"] = "table.concat (table [, sep [, i [, j]]])",
|
||||
["table.insert"] = "table.insert (table, [pos,] value)",
|
||||
["table.maxn"] = "table.maxn (table)",
|
||||
["table.remove"] = "table.remove (table [, pos])",
|
||||
["table.sort"] = "table.sort (table [, comp])",
|
||||
}
|
||||
|
||||
-- utility function. Converts e.g. name 'math.sqrt' to its value.
|
||||
local function resolve_global_helper_(name)
|
||||
local o = _G
|
||||
for fieldname in name:gmatch'[^%.]+' do o = o[fieldname] end
|
||||
return o
|
||||
end
|
||||
local function resolve_global(name)
|
||||
local a, b = pcall(resolve_global_helper_, name)
|
||||
if a then return b else return nil, b end
|
||||
end
|
||||
|
||||
-- Same as global_signatures but maps value (not name) to signature.
|
||||
M.value_signatures = {}
|
||||
local isobject = {['function']=true, ['table']=true, ['userdata']=true, ['coroutine']=true}
|
||||
for name,sig in pairs(M.global_signatures) do
|
||||
local val, err = resolve_global(name)
|
||||
if isobject[type(val)] then
|
||||
M.value_signatures[val] = sig
|
||||
end
|
||||
end
|
||||
|
||||
-- min,max argument counts.
|
||||
M.argument_counts = {
|
||||
[assert] = {1,2},
|
||||
[collectgarbage] = {1,2},
|
||||
[dofile] = {1},
|
||||
[error] = {1,2},
|
||||
[getfenv or false] = {0,1},
|
||||
[getmetatable] = {1,1},
|
||||
[ipairs] = {1,1},
|
||||
[load] = {1,2},
|
||||
[loadfile] = {0,1},
|
||||
[loadstring] = {1,2},
|
||||
[next] = {1,2},
|
||||
[pairs] = {1,1},
|
||||
[pcall] = {1,math.huge},
|
||||
[print] = {0,math.huge},
|
||||
[rawequal] = {2,2},
|
||||
[rawget] = {2,2},
|
||||
[rawset] = {3,3},
|
||||
[select] = {1, math.huge},
|
||||
[setfenv or false] = {2,2},
|
||||
[setmetatable] = {2,2},
|
||||
[tonumber] = {1,2},
|
||||
[tostring] = {1},
|
||||
[type] = {1},
|
||||
[unpack] = {1,3},
|
||||
[xpcall] = {2,2},
|
||||
[module] = {1,math.huge},
|
||||
[require] = {1,1},
|
||||
[coroutine.create] = {1,1},
|
||||
[coroutine.resume] = {1, math.huge},
|
||||
[coroutine.running] = {0,0},
|
||||
[coroutine.status] = {1,1},
|
||||
[coroutine.wrap] = {1,1},
|
||||
[coroutine.yield] = {0,math.huge},
|
||||
[debug.debug] = {0,0},
|
||||
[debug.getfenv or false] = {1,1},
|
||||
[debug.gethook] = {0,1},
|
||||
[debug.getinfo] = {1,3},
|
||||
[debug.getlocal] = {2,3},
|
||||
[debug.getmetatable] = {1,1},
|
||||
[debug.getregistry] = {0,0},
|
||||
[debug.getupvalue] = {2,2},
|
||||
[debug.setfenv or false] = {2,2},
|
||||
[debug.sethook] = {2,4},
|
||||
[debug.setlocal] = {3,4},
|
||||
[debug.setmetatable] = {2,2},
|
||||
[debug.setupvalue] = {3,3},
|
||||
[debug.traceback] = {0,3},
|
||||
[io.close] = {0,1},
|
||||
[io.flush] = {0,0},
|
||||
[io.input] = {0,1},
|
||||
[io.lines] = {0,1},
|
||||
[io.open] = {1,2},
|
||||
[io.output] = {0,1},
|
||||
[io.popen] = {1,2},
|
||||
[io.read] = {0,math.huge},
|
||||
[io.tmpfile] = {0},
|
||||
[io.type] = {1},
|
||||
[io.write] = {0,math.huge},
|
||||
[math.abs] = {1},
|
||||
[math.acos] = {1},
|
||||
[math.asin] = {1},
|
||||
[math.atan] = {1},
|
||||
[math.atan2] = {2,2},
|
||||
[math.ceil] = {1,1},
|
||||
[math.cos] = {1,1},
|
||||
[math.cosh] = {1,1},
|
||||
[math.deg] = {1,1},
|
||||
[math.exp] = {1,1},
|
||||
[math.floor] = {1,1},
|
||||
[math.fmod] = {2,2},
|
||||
[math.frexp] = {1,1},
|
||||
[math.ldexp] = {2,2},
|
||||
[math.log] = {1,1},
|
||||
[math.log10] = {1,1},
|
||||
[math.max] = {1,math.huge},
|
||||
[math.min] = {1,math.huge},
|
||||
[math.modf] = {1,1},
|
||||
[math.pow] = {2,2},
|
||||
[math.rad] = {1,1},
|
||||
[math.random] = {0,2},
|
||||
[math.randomseed] = {1,1},
|
||||
[math.sin] = {1,1},
|
||||
[math.sinh] = {1,1},
|
||||
[math.sqrt] = {1,1},
|
||||
[math.tan] = {1,1},
|
||||
[math.tanh] = {1,1},
|
||||
[os.clock] = {0,0},
|
||||
[os.date] = {0,2},
|
||||
[os.difftime] = {2,2},
|
||||
[os.execute] = {0,1},
|
||||
[os.exit] = {0,1},
|
||||
[os.getenv] = {1,1},
|
||||
[os.remove] = {1,1},
|
||||
[os.rename] = {2,2},
|
||||
[os.setlocale] = {1,2},
|
||||
[os.time] = {0,1},
|
||||
[os.tmpname] = {0,0},
|
||||
[package.loadlib] = {2,2},
|
||||
[package.seeall] = {1,1},
|
||||
[string.byte] = {1,3},
|
||||
[string.char] = {0,math.huge},
|
||||
[string.dump] = {1,1},
|
||||
[string.find] = {2,4},
|
||||
[string.format] = {1,math.huge},
|
||||
[string.gmatch] = {2,2},
|
||||
[string.gsub] = {3,4},
|
||||
[string.len] = {1,1},
|
||||
[string.lower] = {1,1},
|
||||
[string.match] = {2,3},
|
||||
[string.rep] = {2,2},
|
||||
[string.reverse] = {1,1},
|
||||
[string.sub] = {2,3},
|
||||
[string.upper] = {1,1},
|
||||
[table.concat] = {1,4},
|
||||
[table.insert] = {2,3},
|
||||
[table.maxn] = {1,1},
|
||||
[table.remove] = {1,2},
|
||||
[table.sort] = {1,2},
|
||||
[false] = nil -- trick (relies on potentially undefined behavior)
|
||||
}
|
||||
|
||||
|
||||
-- functions with zero or nearly zero side-effects, and with deterministic results, that may be evaluated by the analyzer.
|
||||
M.safe_function = {
|
||||
[require] = true,
|
||||
[rawequal] = true,
|
||||
[rawget] = true,
|
||||
[require] = true, -- sort of
|
||||
[select] = true,
|
||||
[tonumber] = true,
|
||||
[tostring] = true,
|
||||
[type] = true,
|
||||
[unpack] = true,
|
||||
[coroutine.create] = true,
|
||||
-- [coroutine.resume]
|
||||
[coroutine.running] = true,
|
||||
[coroutine.status] = true,
|
||||
[coroutine.wrap] = true,
|
||||
--[coroutine.yield]
|
||||
-- [debug.debug]
|
||||
--[debug.getfenv] = true,
|
||||
[debug.gethook] = true,
|
||||
[debug.getinfo] = true,
|
||||
[debug.getlocal] = true,
|
||||
[debug.getmetatable] = true,
|
||||
[debug.getregistry] = true,
|
||||
[debug.getupvalue] = true,
|
||||
-- [debug.setfenv]
|
||||
-- [debug.sethook]
|
||||
-- [debug.setlocal]
|
||||
-- [debug.setmetatable]
|
||||
-- [debug.setupvalue]
|
||||
-- [debug.traceback] = true,
|
||||
[io.type] = true,
|
||||
-- skip all other io.*
|
||||
[math.abs] = true,
|
||||
[math.acos] = true,
|
||||
[math.asin] = true,
|
||||
[math.atan] = true,
|
||||
[math.atan2] = true,
|
||||
[math.ceil] = true,
|
||||
[math.cos] = true,
|
||||
[math.cosh] = true,
|
||||
[math.deg] = true,
|
||||
[math.exp] = true,
|
||||
[math.floor] = true,
|
||||
[math.fmod] = true,
|
||||
[math.frexp] = true,
|
||||
[math.ldexp] = true,
|
||||
[math.log] = true,
|
||||
[math.log10] = true,
|
||||
[math.max] = true,
|
||||
[math.min] = true,
|
||||
[math.modf] = true,
|
||||
[math.pow] = true,
|
||||
[math.rad] = true,
|
||||
--[math.random]
|
||||
--[math.randomseed]
|
||||
[math.sin] = true,
|
||||
[math.sinh] = true,
|
||||
[math.sqrt] = true,
|
||||
[math.tan] = true,
|
||||
[math.tanh] = true,
|
||||
[os.clock] = true, -- safe but non-deterministic
|
||||
[os.date] = true,-- safe but non-deterministic
|
||||
[os.difftime] = true,
|
||||
--[os.execute]
|
||||
--[os.exit]
|
||||
[os.getenv] = true, -- though depends on environment
|
||||
--[os.remove]
|
||||
--[os.rename]
|
||||
--[os.setlocale]
|
||||
[os.time] = true, -- safe but non-deterministic
|
||||
--[os.tmpname]
|
||||
[string.byte] = true,
|
||||
[string.char] = true,
|
||||
[string.dump] = true,
|
||||
[string.find] = true,
|
||||
[string.format] = true,
|
||||
[string.gmatch] = true,
|
||||
[string.gsub] = true,
|
||||
[string.len] = true,
|
||||
[string.lower] = true,
|
||||
[string.match] = true,
|
||||
[string.rep] = true,
|
||||
[string.reverse] = true,
|
||||
[string.sub] = true,
|
||||
[string.upper] = true,
|
||||
[table.maxn] = true,
|
||||
}
|
||||
|
||||
M.mock_functions = {}
|
||||
|
||||
-- TODO:IMPROVE
|
||||
local function mockfunction(func, ...)
|
||||
local inputs = {n=0}
|
||||
local outputs = {n=0}
|
||||
local isoutputs
|
||||
for i=1,select('#', ...) do
|
||||
local v = select(i, ...)
|
||||
if type(v) == 'table' then v = v[1] end
|
||||
if v == 'N' or v == 'I' then v = T.number end
|
||||
if v == '->' then
|
||||
isoutputs = true
|
||||
elseif isoutputs then
|
||||
outputs[#outputs+1] = v; outputs.n = outputs.n + 1
|
||||
else
|
||||
inputs[#inputs+1] = v; inputs.n = inputs.n + 1
|
||||
end
|
||||
end
|
||||
M.mock_functions[func] = {inputs=inputs, outputs=outputs}
|
||||
end
|
||||
|
||||
|
||||
mockfunction(math.abs, 'N', '->', {'N',0,math.huge})
|
||||
mockfunction(math.acos, {'N',-1,1}, '->', {'N',0,math.pi/2})
|
||||
mockfunction(math.asin, {'N',-1,1}, '->', {'N',-math.pi/2,math.pi/2})
|
||||
mockfunction(math.atan, {'N',-math.huge,math.huge}, '->',
|
||||
{'N',-math.pi/2,math.pi/2})
|
||||
--FIX atan2
|
||||
mockfunction(math.ceil, 'N','->','I')
|
||||
mockfunction(math.cos, 'N','->',{'N',-1,1})
|
||||
mockfunction(math.cosh, 'N','->',{'N',1,math.huge})
|
||||
mockfunction(math.deg, 'N','->','N')
|
||||
mockfunction(math.exp, 'N','->',{'N',0,math.huge})
|
||||
mockfunction(math.floor, 'N','->','I')
|
||||
mockfunction(math.fmod, 'N','N','->','N')
|
||||
mockfunction(math.frexp, 'N','->',{'N',-1,1},'->','I')
|
||||
mockfunction(math.ldexp, {'N','I'},'->','N')
|
||||
mockfunction(math.log, {'N',0,math.huge},'->','N')
|
||||
mockfunction(math.log10, {'N',0,math.huge},'->','N')
|
||||
-- function max(...) print 'NOT IMPL'end
|
||||
-- function min(...) print 'NOT IMPL'end
|
||||
mockfunction(math.modf, 'N','->','I',{'N',-1,1})
|
||||
|
||||
mockfunction(math.pow, 'N','N','->','N') -- improve?
|
||||
mockfunction(math.rad, 'N','->','N')
|
||||
-- random = function() print 'NOT IMPL' end
|
||||
mockfunction(math.randomseed, 'N')
|
||||
mockfunction(math.sin, 'N','->',{'N',-1,1})
|
||||
mockfunction(math.sinh, 'N','->','N')
|
||||
mockfunction(math.sqrt, {'N',0,math.huge},'->',{'N',0,math.huge})
|
||||
mockfunction(math.tan, 'N','->','N') -- improve?
|
||||
mockfunction(math.tanh, 'N','->',{'N',-1,1})
|
||||
|
||||
|
||||
return M
|
||||
40
lualibs/luainspect/typecheck.lua
Normal file
40
lualibs/luainspect/typecheck.lua
Normal file
@@ -0,0 +1,40 @@
|
||||
-- luainspect.typecheck - Type definitions used to check LuaInspect itself.
|
||||
--
|
||||
-- (c) 2010 David Manura, MIT License.
|
||||
|
||||
local T = require "luainspect.types"
|
||||
|
||||
local ast_mt = {__tostring = function(s) return 'AST' end}
|
||||
|
||||
return function(context)
|
||||
-- AST type.
|
||||
local ast = T.table {
|
||||
tag = T.string,
|
||||
lineinfo=T.table{first=T.table{comments=T.table{T.table{T.string,T.number,T.number}},T.number,T.number,T.number,T.string},
|
||||
ast=T.table{comments=T.table{T.table{T.string,T.number,T.number}},T.number,T.number,T.number,T.string}},
|
||||
isfield=T.boolean, tag2=T.string,
|
||||
value=T.universal, valueself=T.number, valuelist=T.table{n=T.number, isvaluepegged=T.boolean},
|
||||
resolvedname=T.string, definedglobal=T.boolean, id=T.number, isparam=T.boolean, isset=T.boolean, isused=T.boolean,
|
||||
isignore=T.boolean,
|
||||
functionlevel=T.number, localmasked=T.boolean, note=T.string, nocollect=T.table{}, isdead=T.boolean}
|
||||
-- FIX: some of these are "boolean or nil" actually
|
||||
ast.localdefinition=ast; ast.localmasking = ast
|
||||
ast.previous = ast; ast.parent = ast
|
||||
ast.seevalue = ast; ast.seenote=ast
|
||||
setmetatable(ast, ast_mt)
|
||||
|
||||
ast[1] = ast; ast[2] = ast
|
||||
context.apply_value('ast$', ast)
|
||||
|
||||
-- Token type.
|
||||
context.apply_value('token$', T.table{
|
||||
tag=T.string, fpos=T.number, lpos=T.number, keywordid=T.number, ast=ast, [1]=T.string
|
||||
})
|
||||
|
||||
-- Lua source code string type.
|
||||
context.apply_value('src$', '')
|
||||
|
||||
-- SciTE syler object type.
|
||||
local nf = function()end
|
||||
context.apply_value('^styler$', T.table{SetState=nf, More=nf, Current=nf, Forward=nf, StartStyling=nf, EndStyling=nf, language=T.string})
|
||||
end
|
||||
130
lualibs/luainspect/types.lua
Normal file
130
lualibs/luainspect/types.lua
Normal file
@@ -0,0 +1,130 @@
|
||||
local T = {} -- types
|
||||
|
||||
-- istype[o] iff o represents a type (i.e. set of values)
|
||||
T.istype = {}
|
||||
|
||||
-- iserror[o] iff o represents an error type (created via T.error).
|
||||
T.iserror = {}
|
||||
|
||||
-- istabletype[o] iff o represents a table type (created by T.table).
|
||||
T.istabletype = {}
|
||||
|
||||
-- Number type
|
||||
T.number = {}
|
||||
setmetatable(T.number, T.number)
|
||||
function T.number.__tostring(self)
|
||||
return 'number'
|
||||
end
|
||||
T.istype[T.number] = true
|
||||
|
||||
-- String type
|
||||
T.string = {}
|
||||
setmetatable(T.string, T.string)
|
||||
function T.string.__tostring(self)
|
||||
return 'string'
|
||||
end
|
||||
T.istype[T.string] = true
|
||||
|
||||
-- Boolean type
|
||||
T.boolean = {}
|
||||
setmetatable(T.boolean, T.boolean)
|
||||
function T.boolean.__tostring(self)
|
||||
return 'boolean'
|
||||
end
|
||||
T.istype[T.boolean] = true
|
||||
|
||||
-- Table type
|
||||
function T.table(t)
|
||||
T.istype[t] = true
|
||||
T.istabletype[t] = true
|
||||
return t
|
||||
end
|
||||
|
||||
-- Universal type. This is a superset of all other types.
|
||||
T.universal = {}
|
||||
setmetatable(T.universal, T.universal)
|
||||
function T.universal.__tostring(self)
|
||||
return 'unknown'
|
||||
end
|
||||
T.istype[T.universal] = true
|
||||
|
||||
-- nil type. Represents `nil` but can be stored in tables.
|
||||
T['nil'] = {}
|
||||
setmetatable(T['nil'], T['nil'])
|
||||
T['nil'].__tostring = function(self)
|
||||
return 'nil'
|
||||
end
|
||||
T.istype[T['nil']] = true
|
||||
|
||||
-- None type. Represents a non-existent value, in a similar way
|
||||
-- that `none` is used differently from `nil` in the Lua C API.
|
||||
T.none = {}
|
||||
setmetatable(T.none, T.none)
|
||||
function T.none.__tostring(self)
|
||||
return 'none'
|
||||
end
|
||||
T.istype[T.none] = true
|
||||
|
||||
-- Error type
|
||||
local CError = {}; CError.__index = CError
|
||||
function CError.__tostring(self) return "error:" .. tostring(self.value) end
|
||||
function T.error(val)
|
||||
local self = setmetatable({value=val}, CError)
|
||||
T.istype[self] = true
|
||||
T.iserror[self] = true
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
-- Gets a type that is a superset of the two given types.
|
||||
function T.superset_types(a, b)
|
||||
if T.iserror[a] then return a end
|
||||
if T.iserror[b] then return b end
|
||||
if rawequal(a, b) then -- note: including nil == nil
|
||||
return a
|
||||
elseif type(a) == 'string' or a == T.string then
|
||||
if type(b) == 'string' or b == T.string then
|
||||
return T.string
|
||||
else
|
||||
return T.universal
|
||||
end
|
||||
elseif type(a) == 'number' or a == T.number then
|
||||
if type(b) == 'number' or b == T.number then
|
||||
return T.number
|
||||
else
|
||||
return T.universal
|
||||
end
|
||||
elseif type(a) == 'boolean' or a == T.boolean then
|
||||
if type(b) == 'boolean' or b == T.boolean then
|
||||
return T.boolean
|
||||
else
|
||||
return T.universal
|
||||
end
|
||||
else
|
||||
return T.universal -- IMPROVE
|
||||
end
|
||||
end
|
||||
--[[TESTS:
|
||||
assert(T.superset_types(2, 2) == 2)
|
||||
assert(T.superset_types(2, 3) == T.number)
|
||||
assert(T.superset_types(2, T.number) == T.number)
|
||||
assert(T.superset_types(T.number, T.string) == T.universal)
|
||||
print 'DONE'
|
||||
--]]
|
||||
|
||||
-- Determines whether type `o` certainly evaluates to true (true),
|
||||
-- certainly evaluates to false (false) or could evaluate to either
|
||||
-- true of false ('?').
|
||||
function T.boolean_cast(o)
|
||||
if T.iserror[o] then -- special case
|
||||
return '?'
|
||||
elseif o == nil or o == false or o == T['nil'] then -- all subsets of {nil, false}
|
||||
return false
|
||||
elseif o == T.universal or o == T.boolean then -- all supersets of boolean
|
||||
return '?'
|
||||
else -- all subsets of universal - {nil, false}
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return T
|
||||
@@ -28,8 +28,9 @@
|
||||
|
||||
module ("lexer", package.seeall)
|
||||
|
||||
require 'metalua.runtime'
|
||||
|
||||
-- don't load metalua.runtime as it loads metalua.base, which pollutes
|
||||
-- global namespace and overwrites pairs/ipairs -- PK 6/4/2012
|
||||
require 'metalua.table2'
|
||||
|
||||
lexer = { alpha={ }, sym={ } }
|
||||
lexer.__index=lexer
|
||||
|
||||
@@ -27,6 +27,6 @@ local keywords = {
|
||||
"...", "..", "==", ">=", "<=", "~=",
|
||||
"+{", "-{" }
|
||||
|
||||
for w in values(keywords) do mlp_lexer:add(w) end
|
||||
for _,w in pairs(keywords) do mlp_lexer:add(w) end -- PK 6/4/2012
|
||||
|
||||
_M.lexer = mlp_lexer
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
--
|
||||
-- MobDebug 0.44
|
||||
-- Copyright Paul Kulchenko 2011-2012
|
||||
-- Based on RemDebug 1.0 (http://www.keplerproject.org/remdebug)
|
||||
-- MobDebug 0.473
|
||||
-- Copyright 2011-12 Paul Kulchenko
|
||||
-- Based on RemDebug 1.0 Copyright Kepler Project 2005
|
||||
--
|
||||
|
||||
local mobdebug = {
|
||||
_NAME = "mobdebug",
|
||||
_VERSION = 0.473,
|
||||
_COPYRIGHT = "Paul Kulchenko",
|
||||
_DESCRIPTION = "Mobile Remote Debugger for the Lua programming language",
|
||||
_VERSION = "0.44"
|
||||
port = 8171
|
||||
}
|
||||
|
||||
local coroutine = coroutine
|
||||
@@ -23,11 +24,18 @@ local setmetatable = setmetatable
|
||||
local string = string
|
||||
local tonumber = tonumber
|
||||
local mosync = mosync
|
||||
local jit = jit
|
||||
|
||||
-- this is a socket class that implements maConnect interface
|
||||
local function socketMobileLua()
|
||||
local self = {}
|
||||
self.select = function() return {} end
|
||||
self.select = function(readfrom) -- writeto and timeout parameters are ignored
|
||||
local canread = {}
|
||||
for _,s in ipairs(readfrom) do
|
||||
if s:receive(0) then canread[s] = true end
|
||||
end
|
||||
return canread
|
||||
end
|
||||
self.connect = coroutine.wrap(function(host, port)
|
||||
while true do
|
||||
local connection = mosync.maConnect("socket://" .. host .. ":" .. port)
|
||||
@@ -38,13 +46,14 @@ local function socketMobileLua()
|
||||
mosync.maWait(0)
|
||||
mosync.maGetEvent(event)
|
||||
local eventType = mosync.SysEventGetType(event)
|
||||
if (mosync.EVENT_TYPE_CLOSE == eventType) then mosync.maExit(0) end
|
||||
if (mosync.EVENT_TYPE_CONN == eventType and
|
||||
mosync.SysEventGetConnHandle(event) == connection and
|
||||
mosync.SysEventGetConnOpType(event) == mosync.CONNOP_CONNECT) then
|
||||
-- result > 0 ? success : error
|
||||
if not (mosync.SysEventGetConnResult(event) > 0) then connection = nil end
|
||||
break
|
||||
elseif mosync.EventMonitor and mosync.EventMonitor.HandleEvent then
|
||||
mosync.EventMonitor:HandleEvent(event)
|
||||
end
|
||||
end
|
||||
mosync.SysFree(event)
|
||||
@@ -73,42 +82,47 @@ local function socketMobileLua()
|
||||
end
|
||||
return s
|
||||
end
|
||||
self.send = coroutine.wrap(function(self, msg)
|
||||
self.send = coroutine.wrap(function(self, msg)
|
||||
while true do
|
||||
local numberOfBytes = stringToBuffer(msg, outBuffer)
|
||||
mosync.maConnWrite(connection, outBuffer, numberOfBytes)
|
||||
local result = 0
|
||||
while true do
|
||||
mosync.maWait(0)
|
||||
mosync.maGetEvent(event)
|
||||
local eventType = mosync.SysEventGetType(event)
|
||||
if (mosync.EVENT_TYPE_CLOSE == eventType) then mosync.maExit(0) end
|
||||
if (mosync.EVENT_TYPE_CONN == eventType and
|
||||
mosync.SysEventGetConnHandle(event) == connection and
|
||||
mosync.SysEventGetConnOpType(event) == mosync.CONNOP_WRITE) then
|
||||
break
|
||||
elseif mosync.EventMonitor and mosync.EventMonitor.HandleEvent then
|
||||
mosync.EventMonitor:HandleEvent(event)
|
||||
end
|
||||
end
|
||||
self, msg = coroutine.yield()
|
||||
end
|
||||
end)
|
||||
self.receive = coroutine.wrap(function(self, len)
|
||||
self.receive = coroutine.wrap(function(self, len)
|
||||
while true do
|
||||
local line = recvBuffer
|
||||
while (len and string.len(line) < len) -- either we need len bytes
|
||||
or (not len and not line:find("\n")) do -- or one line (if no len specified)
|
||||
or (not len and not line:find("\n")) -- or one line (if no len specified)
|
||||
or (len == 0) do -- only check for new data (select-like)
|
||||
mosync.maConnRead(connection, inBuffer, 1000)
|
||||
while true do
|
||||
mosync.maWait(0)
|
||||
if len ~= 0 then mosync.maWait(0) end
|
||||
mosync.maGetEvent(event)
|
||||
local eventType = mosync.SysEventGetType(event)
|
||||
if (mosync.EVENT_TYPE_CLOSE == eventType) then mosync.maExit(0) end
|
||||
if (mosync.EVENT_TYPE_CONN == eventType and
|
||||
mosync.SysEventGetConnHandle(event) == connection and
|
||||
mosync.SysEventGetConnOpType(event) == mosync.CONNOP_READ) then
|
||||
local result = mosync.SysEventGetConnResult(event);
|
||||
local result = mosync.SysEventGetConnResult(event)
|
||||
if result > 0 then line = line .. bufferToString(inBuffer, result) end
|
||||
break; -- got the event we wanted; now check if we have all we need
|
||||
if len == 0 then self, len = coroutine.yield("") end
|
||||
break -- got the event we wanted; now check if we have all we need
|
||||
elseif len == 0 then
|
||||
self, len = coroutine.yield(nil)
|
||||
elseif mosync.EventMonitor and mosync.EventMonitor.HandleEvent then
|
||||
mosync.EventMonitor:HandleEvent(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -140,32 +154,169 @@ local function socketMobileLua()
|
||||
return self
|
||||
end
|
||||
|
||||
local socket = mosync and socketMobileLua() or (require "socket")
|
||||
-- overwrite RunEventLoop in MobileLua as it conflicts with the event
|
||||
-- loop that needs to run to process debugger events (socket read/write).
|
||||
-- event loop functionality is implemented by calling HandleEvent
|
||||
-- while waiting for debugger events.
|
||||
if mosync and mosync.EventMonitor then
|
||||
mosync.EventMonitor.RunEventLoop = function(self) end
|
||||
end
|
||||
|
||||
--
|
||||
-- RemDebug 1.0 Beta
|
||||
-- Copyright Kepler Project 2005 (http://www.keplerproject.org/remdebug)
|
||||
--
|
||||
local socket = mosync and socketMobileLua() or (require "socket")
|
||||
|
||||
local debug = require "debug"
|
||||
local coro_debugger
|
||||
local events = { BREAK = 1, WATCH = 2 }
|
||||
local events = { BREAK = 1, WATCH = 2, RESTART = 3, STACK = 4 }
|
||||
local breakpoints = {}
|
||||
local watches = {}
|
||||
local lastsource
|
||||
local lastfile
|
||||
local watchescnt = 0
|
||||
local abort -- default value is nil; this is used in start/loop distinction
|
||||
local check_break = false
|
||||
local seen_hook = false
|
||||
local skip
|
||||
local skipcount = 0
|
||||
local step_into = false
|
||||
local step_over = false
|
||||
local step_level = 0
|
||||
local stack_level = 0
|
||||
local server
|
||||
local deferror = "execution aborted at default debugee"
|
||||
local debugee = function ()
|
||||
local a = 1
|
||||
a = a + 1
|
||||
return "ok"
|
||||
for _ = 1, 10 do a = a + 1 end
|
||||
error(deferror)
|
||||
end
|
||||
|
||||
local serpent = (function() ---- include Serpent module for serialization
|
||||
local n, v = "serpent", 0.15 -- (C) 2012 Paul Kulchenko; MIT License
|
||||
local c, d = "Paul Kulchenko", "Serializer and pretty printer of Lua data types"
|
||||
local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
|
||||
local badtype = {thread = true, userdata = true}
|
||||
local keyword, globals, G = {}, {}, (_G or _ENV)
|
||||
for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
|
||||
'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
|
||||
'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end
|
||||
for k,v in pairs(G) do globals[v] = k end -- build func to name mapping
|
||||
for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do
|
||||
for k,v in pairs(G[g]) do globals[v] = g..'.'..k end end
|
||||
|
||||
local function s(t, opts)
|
||||
local name, indent, fatal = opts.name, opts.indent, opts.fatal
|
||||
local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge
|
||||
local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
|
||||
local comm = opts.comment and (tonumber(opts.comment) or math.huge)
|
||||
local seen, sref, syms, symn = {}, {}, {}, 0
|
||||
local function gensym(val) return tostring(val):gsub("[^%w]",""):gsub("(%d%w+)",
|
||||
function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return syms[s] end) end
|
||||
local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or s)
|
||||
or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
|
||||
or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end
|
||||
local function comment(s,l) return comm and (l or 0) < comm and ' --[['..tostring(s)..']]' or '' end
|
||||
local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal
|
||||
and safestr(tostring(s))..comment('err',l) or error("Can't serialize "..tostring(s)) end
|
||||
local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']
|
||||
local n = name == nil and '' or name
|
||||
local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]
|
||||
local safe = plain and n or '['..safestr(n)..']'
|
||||
return (path or '')..(plain and path and '.' or '')..safe, safe end
|
||||
local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(o, n)
|
||||
local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}
|
||||
local function padnum(d) return ("%0"..maxn.."d"):format(d) end
|
||||
table.sort(o, function(a,b)
|
||||
return (o[a] and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
|
||||
< (o[b] and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
|
||||
local function val2str(t, name, indent, path, plainindex, level)
|
||||
local ttype, level = type(t), (level or 0)
|
||||
local spath, sname = safename(path, name)
|
||||
local tag = plainindex and
|
||||
((type(name) == "number") and '' or name..space..'='..space) or
|
||||
(name ~= nil and sname..space..'='..space or '')
|
||||
if seen[t] then
|
||||
table.insert(sref, spath..space..'='..space..seen[t])
|
||||
return tag..'nil'..comment('ref', level)
|
||||
elseif badtype[ttype] then return tag..globerr(t, level)
|
||||
elseif ttype == 'function' then
|
||||
seen[t] = spath
|
||||
local ok, res = pcall(string.dump, t)
|
||||
local func = ok and ((opts.nocode and "function() end" or
|
||||
"loadstring("..safestr(res)..",'@serialized')")..comment(t, level))
|
||||
return tag..(func or globerr(t, level))
|
||||
elseif ttype == "table" then
|
||||
if level >= maxl then return tag..'{}'..comment('max', level) end
|
||||
seen[t] = spath
|
||||
if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
|
||||
local maxn, o, out = #t, {}, {}
|
||||
for key = 1, maxn do table.insert(o, key) end
|
||||
for key in pairs(t) do if not o[key] then table.insert(o, key) end end
|
||||
if opts.sortkeys then alphanumsort(o, opts.sortkeys) end
|
||||
for n, key in ipairs(o) do
|
||||
local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse
|
||||
if opts.ignore and opts.ignore[value] -- skip ignored values; do nothing
|
||||
or sparse and value == nil then -- skipping nils; do nothing
|
||||
elseif ktype == 'table' or ktype == 'function' then
|
||||
if not seen[key] and not globals[key] then
|
||||
table.insert(sref, 'local '..val2str(key,gensym(key),indent)) end
|
||||
table.insert(sref, seen[t]..'['..(seen[key] or globals[key] or gensym(key))
|
||||
..']'..space..'='..space..(seen[value] or val2str(value,nil,indent)))
|
||||
else
|
||||
if badtype[ktype] then plainindex, key = true, '['..globerr(key, level+1)..']' end
|
||||
table.insert(out,val2str(value,key,indent,spath,plainindex,level+1))
|
||||
end
|
||||
end
|
||||
local prefix = string.rep(indent or '', level)
|
||||
local head = indent and '{\n'..prefix..indent or '{'
|
||||
local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
|
||||
local tail = indent and "\n"..prefix..'}' or '}'
|
||||
return (custom and custom(tag,head,body,tail) or tag..head..body..tail)..comment(t, level)
|
||||
else return tag..safestr(t) end -- handle all other types
|
||||
end
|
||||
local sepr = indent and "\n" or ";"..space
|
||||
local body = val2str(t, name, indent) -- this call also populates sref
|
||||
local tail = #sref>0 and table.concat(sref, sepr)..sepr or ''
|
||||
return not name and body or "do local "..body..sepr..tail.."return "..name..sepr.."end"
|
||||
end
|
||||
|
||||
local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end
|
||||
return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,
|
||||
dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end,
|
||||
line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end,
|
||||
block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end }
|
||||
end)() ---- end of Serpent module
|
||||
|
||||
local function stack(start)
|
||||
local function vars(f)
|
||||
local func = debug.getinfo(f, "f").func
|
||||
local i = 1
|
||||
local locals = {}
|
||||
while true do
|
||||
local name, value = debug.getlocal(f, i)
|
||||
if not name then break end
|
||||
if string.sub(name, 1, 1) ~= '(' then locals[name] = {value, tostring(value)} end
|
||||
i = i + 1
|
||||
end
|
||||
i = 1
|
||||
local ups = {}
|
||||
while func and true do -- check for func as it may be nil for tail calls
|
||||
local name, value = debug.getupvalue(func, i)
|
||||
if not name then break end
|
||||
ups[name] = {value, tostring(value)}
|
||||
i = i + 1
|
||||
end
|
||||
return locals, ups
|
||||
end
|
||||
|
||||
local stack = {}
|
||||
for i = (start or 0), 100 do
|
||||
local source = debug.getinfo(i, "Snl")
|
||||
if not source then break end
|
||||
table.insert(stack, {
|
||||
{source.name, source.source, source.linedefined,
|
||||
source.currentline, source.what, source.namewhat, source.short_src},
|
||||
vars(i+1)})
|
||||
if source.what == 'main' then break end
|
||||
end
|
||||
return stack
|
||||
end
|
||||
|
||||
local function set_breakpoint(file, line)
|
||||
@@ -195,7 +346,7 @@ local function restore_vars(vars)
|
||||
while true do
|
||||
local name = debug.getlocal(3, i)
|
||||
if not name then break end
|
||||
debug.setlocal(3, i, vars[name])
|
||||
if string.sub(name, 1, 1) ~= '(' then debug.setlocal(3, i, vars[name]) end
|
||||
written_vars[name] = true
|
||||
i = i + 1
|
||||
end
|
||||
@@ -204,7 +355,7 @@ local function restore_vars(vars)
|
||||
local name = debug.getupvalue(func, i)
|
||||
if not name then break end
|
||||
if not written_vars[name] then
|
||||
debug.setupvalue(func, i, vars[name])
|
||||
if string.sub(name, 1, 1) ~= '(' then debug.setupvalue(func, i, vars[name]) end
|
||||
written_vars[name] = true
|
||||
end
|
||||
i = i + 1
|
||||
@@ -218,27 +369,87 @@ local function capture_vars()
|
||||
while true do
|
||||
local name, value = debug.getupvalue(func, i)
|
||||
if not name then break end
|
||||
vars[name] = value
|
||||
if string.sub(name, 1, 1) ~= '(' then vars[name] = value end
|
||||
i = i + 1
|
||||
end
|
||||
i = 1
|
||||
while true do
|
||||
local name, value = debug.getlocal(3, i)
|
||||
if not name then break end
|
||||
vars[name] = value
|
||||
if string.sub(name, 1, 1) ~= '(' then vars[name] = value end
|
||||
i = i + 1
|
||||
end
|
||||
setmetatable(vars, { __index = getfenv(func), __newindex = getfenv(func) })
|
||||
return vars
|
||||
end
|
||||
|
||||
local function stack_depth(start_depth)
|
||||
for i = start_depth, 0, -1 do
|
||||
if debug.getinfo(i, "l") then return i+1 end
|
||||
end
|
||||
return start_depth
|
||||
end
|
||||
|
||||
local function is_safe(stack_level, conservative)
|
||||
-- the stack grows up: 0 is getinfo, 1 is is_safe, 2 is debug_hook, 3 is user function
|
||||
if stack_level == 3 then return true end
|
||||
local main = debug.getinfo(3, "S").source
|
||||
|
||||
for i = 3, stack_level do
|
||||
-- return if it is not safe to abort
|
||||
local info = debug.getinfo(i, "S")
|
||||
if not info then return true end
|
||||
if conservative and info.source ~= main or info.what == "C" then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function in_debugger()
|
||||
local this = debug.getinfo(1, "S").source
|
||||
-- only need to check few frames as mobdebug frames should be close
|
||||
for i = 3, 7 do
|
||||
local info = debug.getinfo(i, "S")
|
||||
if not info then return false end
|
||||
if info.source == this then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function debug_hook(event, line)
|
||||
if abort then error("aborted") end -- abort execution for RE/LOAD
|
||||
if abort and is_safe(stack_level) then error(abort) end
|
||||
|
||||
-- LuaJIT needs special treatment. Because debug_hook is set for
|
||||
-- *all* coroutines, and not just the one being debugged as in regular Lua
|
||||
-- (http://lua-users.org/lists/lua-l/2011-06/msg00513.html),
|
||||
-- need to avoid debugging mobdebug's own code as LuaJIT doesn't
|
||||
-- always correctly generate call/return hook events (there are more
|
||||
-- calls than returns, which breaks stack depth calculation and
|
||||
-- 'step' and 'step over' commands stop working; possibly because
|
||||
-- 'tail return' events are not generated by LuaJIT).
|
||||
-- the next line checks if the debugger is run under LuaJIT and if
|
||||
-- one of debugger methods is present in the stack, it simply returns.
|
||||
if jit and in_debugger() then return end
|
||||
|
||||
if event == "call" then
|
||||
stack_level = stack_level + 1
|
||||
elseif event == "return" or event == "tail return" then
|
||||
stack_level = stack_level - 1
|
||||
elseif event == "line" then
|
||||
|
||||
-- check if we need to skip some callbacks (to save time)
|
||||
if skip then
|
||||
skipcount = skipcount + 1
|
||||
if skipcount < skip or not is_safe(stack_level) then return end
|
||||
skipcount = 0
|
||||
end
|
||||
|
||||
-- this is needed to check if the stack got shorter.
|
||||
-- this may happen when "pcall(load, '')" is called
|
||||
-- or when "error()" is called in a function.
|
||||
-- in either case there are more "call" than "return" events reported.
|
||||
-- this validation is done for every "line" event, but should be
|
||||
-- "cheap" as it only checks for the stack to get shorter
|
||||
stack_level = stack_depth(stack_level)
|
||||
local caller = debug.getinfo(2, "S")
|
||||
|
||||
-- grab the filename and fix it if needed
|
||||
@@ -265,7 +476,6 @@ local function debug_hook(event, line)
|
||||
local status, res = pcall(value)
|
||||
if status and res then
|
||||
coroutine.resume(coro_debugger, events.WATCH, vars, file, line, index)
|
||||
restore_vars(vars)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -273,37 +483,73 @@ local function debug_hook(event, line)
|
||||
if step_into
|
||||
or (step_over and stack_level <= step_level)
|
||||
or has_breakpoint(file, line)
|
||||
or (check_break and (socket.select({server}, {}, 0))[server]) then
|
||||
or (socket.select({server}, {}, 0))[server] then
|
||||
vars = vars or capture_vars()
|
||||
check_break = true -- this is only needed to avoid breaking too early when debugging is starting
|
||||
seen_hook = true
|
||||
step_into = false
|
||||
step_over = false
|
||||
coroutine.resume(coro_debugger, events.BREAK, vars, file, line)
|
||||
local status, res = coroutine.resume(coro_debugger, events.BREAK, vars, file, line)
|
||||
|
||||
-- handle 'stack' command that provides stack() information to the debugger
|
||||
if status and res == 'stack' then
|
||||
while status and res == 'stack' do
|
||||
-- resume with the stack trace and variables
|
||||
if vars then restore_vars(vars) end -- restore vars so they are reflected in stack values
|
||||
status, res = coroutine.resume(coro_debugger, events.STACK, stack(3), file, line)
|
||||
end
|
||||
end
|
||||
|
||||
-- need to recheck once more as resume after 'stack' command may
|
||||
-- return something else (for example, 'exit'), which needs to be handled
|
||||
if status and res and res ~= 'stack' then
|
||||
if abort == nil and res == "exit" then os.exit(1) end
|
||||
abort = res
|
||||
-- only abort if safe; if not, there is another (earlier) check inside
|
||||
-- debug_hook, which will abort execution at the first safe opportunity
|
||||
if is_safe(stack_level) then error(abort) end
|
||||
end
|
||||
end
|
||||
if vars then restore_vars(vars) end
|
||||
end
|
||||
end
|
||||
|
||||
local function stringify_results(status, ...)
|
||||
if not status then return status, ... end -- on error report as it
|
||||
|
||||
local t = {...}
|
||||
for i,v in pairs(t) do -- stringify each of the returned values
|
||||
t[i] = serpent.line(v, {nocode = true, comment = 1})
|
||||
end
|
||||
-- stringify table with all returned values
|
||||
-- this is done to allow each returned value to be used (serialized or not)
|
||||
-- intependently and to preserve "original" comments
|
||||
return status, serpent.dump(t, {sparse = false})
|
||||
end
|
||||
|
||||
local function debugger_loop(sfile, sline)
|
||||
local command
|
||||
local app
|
||||
local eval_env = {}
|
||||
local function emptyWatch () return false end
|
||||
local loaded = {}
|
||||
for k in pairs(package.loaded) do loaded[k] = true end
|
||||
|
||||
while true do
|
||||
local line, err
|
||||
if server.settimeout then server:settimeout(0.010) end
|
||||
if wx and server.settimeout then server:settimeout(0.1) end
|
||||
while true do
|
||||
line, err = server:receive()
|
||||
if not line and err == "timeout" then
|
||||
-- yield for wx GUI applications if possible to avoid "busyness"
|
||||
if wx and wx.wxGetApp then
|
||||
local app = wx.wxGetApp()
|
||||
app = app or (wx and wx.wxGetApp and wx.wxGetApp())
|
||||
if app then
|
||||
local win = app:GetTopWindow()
|
||||
if win then
|
||||
-- process messages in a regular way
|
||||
-- and exit as soon as the event loop is idle
|
||||
win:Connect(wx.wxEVT_IDLE, function(event)
|
||||
app:ExitMainLoop()
|
||||
win:Connect(wx.wxEVT_IDLE, function()
|
||||
win:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_IDLE)
|
||||
app:ExitMainLoop()
|
||||
end)
|
||||
app:MainLoop()
|
||||
end
|
||||
@@ -323,7 +569,7 @@ local function debugger_loop(sfile, sline)
|
||||
server:send("400 Bad Request\n")
|
||||
end
|
||||
elseif command == "DELB" then
|
||||
_, _, _, file, line = string.find(line, "^([A-Z]+)%s+([%w%p%s]+)%s+(%d+)%s*$")
|
||||
local _, _, _, file, line = string.find(line, "^([A-Z]+)%s+([%w%p%s]+)%s+(%d+)%s*$")
|
||||
if file and line then
|
||||
remove_breakpoint(file, tonumber(line))
|
||||
server:send("200 OK\n")
|
||||
@@ -337,9 +583,8 @@ local function debugger_loop(sfile, sline)
|
||||
local status
|
||||
if func then
|
||||
setfenv(func, eval_env)
|
||||
status, res = pcall(func)
|
||||
status, res = stringify_results(pcall(func))
|
||||
end
|
||||
res = tostring(res)
|
||||
if status then
|
||||
server:send("200 OK " .. string.len(res) .. "\n")
|
||||
server:send(res)
|
||||
@@ -355,33 +600,37 @@ local function debugger_loop(sfile, sline)
|
||||
size = tonumber(size)
|
||||
|
||||
if abort == nil then -- no LOAD/RELOAD allowed inside start()
|
||||
if size > 0 then local _ = server:receive(size) end
|
||||
if size > 0 then server:receive(size) end
|
||||
if sfile and sline then
|
||||
server:send("201 Started " .. sfile .. " " .. sline .. "\n")
|
||||
else
|
||||
server:send("200 OK 0\n")
|
||||
end
|
||||
else
|
||||
if size == 0 then -- RELOAD the current script being debugged
|
||||
server:send("200 OK 0\n")
|
||||
abort = true
|
||||
coroutine.yield() -- this should not return as the hook will abort
|
||||
-- reset environment to allow required modules to load again
|
||||
-- remove those packages that weren't loaded when debugger started
|
||||
for k in pairs(package.loaded) do
|
||||
if not loaded[k] then package.loaded[k] = nil end
|
||||
end
|
||||
|
||||
local chunk = server:receive(size)
|
||||
if chunk then -- LOAD a new script for debugging
|
||||
local func, res = loadstring(chunk, name)
|
||||
if func then
|
||||
server:send("200 OK 0\n")
|
||||
debugee = func
|
||||
abort = true
|
||||
coroutine.yield() -- this should not return as the hook will abort
|
||||
else
|
||||
server:send("401 Error in Expression " .. string.len(res) .. "\n")
|
||||
server:send(res)
|
||||
end
|
||||
if size == 0 then -- RELOAD the current script being debugged
|
||||
server:send("200 OK 0\n")
|
||||
coroutine.yield("load")
|
||||
else
|
||||
server:send("400 Bad Request\n")
|
||||
local chunk = server:receive(size)
|
||||
if chunk then -- LOAD a new script for debugging
|
||||
local func, res = loadstring(chunk, name)
|
||||
if func then
|
||||
server:send("200 OK 0\n")
|
||||
debugee = func
|
||||
coroutine.yield("load")
|
||||
else
|
||||
server:send("401 Error in Expression " .. string.len(res) .. "\n")
|
||||
server:send(res)
|
||||
end
|
||||
else
|
||||
server:send("400 Bad Request\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif command == "SETW" then
|
||||
@@ -418,6 +667,8 @@ local function debugger_loop(sfile, sline)
|
||||
server:send("202 Paused " .. file .. " " .. line .. "\n")
|
||||
elseif ev == events.WATCH then
|
||||
server:send("203 Paused " .. file .. " " .. line .. " " .. idx_watch .. "\n")
|
||||
elseif ev == events.RESTART then
|
||||
-- nothing to do
|
||||
else
|
||||
server:send("401 Error in Execution " .. string.len(file) .. "\n")
|
||||
server:send(file)
|
||||
@@ -432,6 +683,8 @@ local function debugger_loop(sfile, sline)
|
||||
server:send("202 Paused " .. file .. " " .. line .. "\n")
|
||||
elseif ev == events.WATCH then
|
||||
server:send("203 Paused " .. file .. " " .. line .. " " .. idx_watch .. "\n")
|
||||
elseif ev == events.RESTART then
|
||||
-- nothing to do
|
||||
else
|
||||
server:send("401 Error in Execution " .. string.len(file) .. "\n")
|
||||
server:send(file)
|
||||
@@ -451,13 +704,34 @@ local function debugger_loop(sfile, sline)
|
||||
server:send("202 Paused " .. file .. " " .. line .. "\n")
|
||||
elseif ev == events.WATCH then
|
||||
server:send("203 Paused " .. file .. " " .. line .. " " .. idx_watch .. "\n")
|
||||
elseif ev == events.RESTART then
|
||||
-- nothing to do
|
||||
else
|
||||
server:send("401 Error in Execution " .. string.len(file) .. "\n")
|
||||
server:send(file)
|
||||
end
|
||||
elseif command == "SUSPEND" then
|
||||
-- do nothing; it already fulfilled its role
|
||||
elseif command == "STACK" then
|
||||
-- first check if we can execute the stack command
|
||||
-- as it requires yielding back to debug_hook it cannot be executed
|
||||
-- if we have not seen the hook yet as happens after start().
|
||||
-- in this case we simply return an empty result
|
||||
if not seen_hook then
|
||||
server:send("200 OK " .. serpent.dump({}) .. "\n")
|
||||
else
|
||||
-- yield back to debug hook to get stack information
|
||||
local ev, vars = coroutine.yield("stack")
|
||||
if ev == events.STACK then
|
||||
server:send("200 OK " ..
|
||||
serpent.dump(vars, {nocode = true, sparse = false}) .. "\n")
|
||||
else
|
||||
server:send("401 Error in Expression 0\n")
|
||||
end
|
||||
end
|
||||
elseif command == "EXIT" then
|
||||
server:send("200 OK\n")
|
||||
os.exit()
|
||||
coroutine.yield("exit")
|
||||
else
|
||||
server:send("400 Bad Request\n")
|
||||
end
|
||||
@@ -468,8 +742,18 @@ local function connect(controller_host, controller_port)
|
||||
return socket.connect(controller_host, controller_port)
|
||||
end
|
||||
|
||||
-- Tries to start the debug session by connecting with a controller
|
||||
local function isrunning()
|
||||
return coro_debugger and coroutine.status(coro_debugger) == 'suspended'
|
||||
end
|
||||
|
||||
-- Starts a debug session by connecting to a controller
|
||||
local function start(controller_host, controller_port)
|
||||
-- only one debugging session can be run (as there is only one debug hook)
|
||||
if isrunning() then return end
|
||||
|
||||
controller_host = controller_host or "localhost"
|
||||
controller_port = controller_port or mobdebug.port
|
||||
|
||||
server = socket.connect(controller_host, controller_port)
|
||||
if server then
|
||||
local info = debug.getinfo(2, "Sl")
|
||||
@@ -477,6 +761,12 @@ local function start(controller_host, controller_port)
|
||||
if string.find(file, "@") == 1 then file = string.sub(file, 2) end
|
||||
if string.find(file, "%.[/\\]") == 1 then file = string.sub(file, 3) end
|
||||
|
||||
-- correct stack depth which already has some calls on it
|
||||
-- so it doesn't go into negative when those calls return
|
||||
-- as this breaks subsequence checks in stack_depth().
|
||||
-- start from 16th frame, which is sufficiently large for this check.
|
||||
stack_level = stack_depth(16)
|
||||
|
||||
debug.sethook(debug_hook, "lcr")
|
||||
coro_debugger = coroutine.create(debugger_loop)
|
||||
return coroutine.resume(coro_debugger, file, info.currentline)
|
||||
@@ -485,32 +775,50 @@ local function start(controller_host, controller_port)
|
||||
end
|
||||
end
|
||||
|
||||
local function loop(controller_host, controller_port)
|
||||
local function controller(controller_host, controller_port)
|
||||
-- only one debugging session can be run (as there is only one debug hook)
|
||||
if isrunning() then return end
|
||||
|
||||
controller_host = controller_host or "localhost"
|
||||
controller_port = controller_port or mobdebug.port
|
||||
|
||||
local exitonerror = not skip -- exit if not running a scratchpad
|
||||
server = socket.connect(controller_host, controller_port)
|
||||
if server then
|
||||
local function report(trace, err)
|
||||
local msg = err .. "\n" .. trace
|
||||
server:send("401 Error in Execution " .. string.len(msg) .. "\n")
|
||||
server:send(msg)
|
||||
server:close()
|
||||
return err
|
||||
end
|
||||
|
||||
coro_debugger = coroutine.create(debugger_loop)
|
||||
|
||||
while true do
|
||||
step_into = true
|
||||
abort = false
|
||||
coro_debugger = coroutine.create(debugger_loop)
|
||||
if skip then skipcount = skip end -- to force suspend right away
|
||||
|
||||
local coro_debugee = coroutine.create(debugee)
|
||||
debug.sethook(coro_debugee, debug_hook, "lcr")
|
||||
local status, error = coroutine.resume(coro_debugee)
|
||||
local status, err = coroutine.resume(coro_debugee)
|
||||
|
||||
-- was there an error or is the script done?
|
||||
if not abort then -- this is an expected error; ignore it
|
||||
if not status then -- this is something to be reported
|
||||
return false,report(debug.traceback(coro_debugee), error)
|
||||
-- 'abort' state is allowed here; ignore it
|
||||
if abort then
|
||||
if tostring(abort) == 'exit' then break end
|
||||
else
|
||||
if status then -- normal execution is done
|
||||
break
|
||||
elseif err and not tostring(err):find(deferror) then
|
||||
-- report the error back
|
||||
-- err is not necessarily a string, so convert to string to report
|
||||
report(debug.traceback(coro_debugee), tostring(err))
|
||||
if exitonerror then break end
|
||||
-- resume once more to clear the response the debugger wants to send
|
||||
local status, err = coroutine.resume(coro_debugger, events.RESTART)
|
||||
if status and err == "exit" then break end
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
server:close()
|
||||
@@ -521,6 +829,16 @@ local function loop(controller_host, controller_port)
|
||||
return true
|
||||
end
|
||||
|
||||
local function scratchpad(controller_host, controller_port, frequency)
|
||||
skip = frequency or 100
|
||||
return controller(controller_host, controller_port)
|
||||
end
|
||||
|
||||
local function loop(controller_host, controller_port)
|
||||
skip = nil -- just in case if loop() is called after scratchpad()
|
||||
return controller(controller_host, controller_port)
|
||||
end
|
||||
|
||||
local basedir = ""
|
||||
|
||||
-- Handles server debugging commands
|
||||
@@ -530,7 +848,7 @@ local function handle(params, client)
|
||||
if command == "run" or command == "step" or command == "out"
|
||||
or command == "over" or command == "exit" then
|
||||
client:send(string.upper(command) .. "\n")
|
||||
client:receive()
|
||||
client:receive() -- this should consume the first '200 OK' response
|
||||
local breakpoint = client:receive()
|
||||
if not breakpoint then
|
||||
print("Program finished")
|
||||
@@ -538,7 +856,9 @@ local function handle(params, client)
|
||||
return -- use return here for those cases where os.exit() is not wanted
|
||||
end
|
||||
local _, _, status = string.find(breakpoint, "^(%d+)")
|
||||
if status == "202" then
|
||||
if status == "200" then
|
||||
-- don't need to do anything
|
||||
elseif status == "202" then
|
||||
_, _, file, line = string.find(breakpoint, "^202 Paused%s+([%w%p%s]+)%s+(%d+)%s*$")
|
||||
if file and line then
|
||||
print("Paused at file " .. file .. " line " .. line)
|
||||
@@ -553,13 +873,14 @@ local function handle(params, client)
|
||||
if size then
|
||||
local msg = client:receive(tonumber(size))
|
||||
print("Error in remote application: " .. msg)
|
||||
os.exit()
|
||||
os.exit(1)
|
||||
return nil, nil, msg -- use return here for those cases where os.exit() is not wanted
|
||||
end
|
||||
else
|
||||
print("Unknown error")
|
||||
os.exit()
|
||||
return nil, nil, "Unknown error" -- use return here for those cases where os.exit() is not wanted
|
||||
os.exit(1)
|
||||
-- use return here for those cases where os.exit() is not wanted
|
||||
return nil, nil, "Debugger error: unexpected response '" .. breakpoint .. "'"
|
||||
end
|
||||
elseif command == "setb" then
|
||||
_, _, _, file, line = string.find(params, "^([a-z]+)%s+([%w%p%s]+)%s+(%d+)%s*$")
|
||||
@@ -633,27 +954,36 @@ local function handle(params, client)
|
||||
for index, exp in pairs(watches) do
|
||||
client:send("DELW " .. index .. "\n")
|
||||
if client:receive() == "200 OK" then
|
||||
watches[index] = nil
|
||||
watches[index] = nil
|
||||
else
|
||||
print("Error: watch expression at index " .. index .. " [" .. exp .. "] not removed")
|
||||
end
|
||||
end
|
||||
elseif command == "eval" or command == "exec"
|
||||
or command == "load" or command == "reload" then
|
||||
or command == "load" or command == "loadstring"
|
||||
or command == "reload" then
|
||||
local _, _, exp = string.find(params, "^[a-z]+%s+(.+)$")
|
||||
if exp or (command == "reload") then
|
||||
if command == "eval" then
|
||||
exp = string.gsub(exp, "\n", " ") -- convert new lines
|
||||
client:send("EXEC return " .. exp .. "\n")
|
||||
elseif command == "exec" then
|
||||
exp = string.gsub(exp, "\n", " ") -- convert new lines
|
||||
if command == "eval" or command == "exec" then
|
||||
exp = (exp:gsub("%-%-%[(=*)%[.-%]%1%]", "") -- remove comments
|
||||
:gsub("%-%-.-\n", " ") -- remove line comments
|
||||
:gsub("\n", " ")) -- convert new lines
|
||||
if command == "eval" then exp = "return " .. exp end
|
||||
client:send("EXEC " .. exp .. "\n")
|
||||
elseif command == "reload" then
|
||||
client:send("LOAD 0 -\n")
|
||||
elseif command == "loadstring" then
|
||||
local _, _, _, file, lines = string.find(exp, "^([\"'])(.-)%1%s+(.+)")
|
||||
if not file then
|
||||
_, _, file, lines = string.find(exp, "^(%S+)%s+(.+)")
|
||||
end
|
||||
client:send("LOAD " .. string.len(lines) .. " " .. file .. "\n")
|
||||
client:send(lines)
|
||||
else
|
||||
local file = io.open(exp, "r")
|
||||
if not file then print("Cannot open file " .. exp); return end
|
||||
local lines = file:read("*all")
|
||||
-- read the file and remove the shebang line as it causes a compilation error
|
||||
local lines = file:read("*all"):gsub("^#!.-\n", "\n")
|
||||
file:close()
|
||||
|
||||
local file = string.gsub(exp, "\\", "/") -- convert slash
|
||||
@@ -662,16 +992,36 @@ local function handle(params, client)
|
||||
client:send(lines)
|
||||
end
|
||||
local params = client:receive()
|
||||
if not params then
|
||||
return nil, nil, "Debugger error: missing response after EXEC/LOAD"
|
||||
end
|
||||
local _, _, status, len = string.find(params, "^(%d+)[%w%p%s]+%s+(%d+)%s*$")
|
||||
if status == "200" then
|
||||
len = tonumber(len)
|
||||
if len > 0 then
|
||||
local res = client:receive(len)
|
||||
print(res)
|
||||
return res
|
||||
local status, res
|
||||
local str = client:receive(len)
|
||||
-- handle serialized table with results
|
||||
local func, err = loadstring(str)
|
||||
if func then
|
||||
status, res = pcall(func)
|
||||
if not status then err = res
|
||||
elseif type(res) ~= "table" then
|
||||
err = "received "..type(res).." instead of expected 'table'"
|
||||
end
|
||||
end
|
||||
if err then
|
||||
print("Error in processing results: " .. err)
|
||||
return nil, nil, "Error in processing results: " .. err
|
||||
end
|
||||
print((table.unpack or unpack)(res))
|
||||
return res[1], res
|
||||
end
|
||||
elseif status == "201" then
|
||||
_, _, file, line = string.find(params, "^201 Started%s+([%w%p%s]+)%s+(%d+)%s*$")
|
||||
elseif status == "202" or params == "200 OK" then
|
||||
-- do nothing; this only happens when RE/LOAD command gets the response
|
||||
-- that was for the original command that was aborted
|
||||
elseif status == "401" then
|
||||
len = tonumber(len)
|
||||
local res = client:receive(len)
|
||||
@@ -679,7 +1029,7 @@ local function handle(params, client)
|
||||
return nil, nil, res
|
||||
else
|
||||
print("Unknown error")
|
||||
return nil, nil, "Unknown error"
|
||||
return nil, nil, "Debugger error: unexpected response after EXEC/LOAD '" .. params .. "'"
|
||||
end
|
||||
else
|
||||
print("Invalid command")
|
||||
@@ -687,7 +1037,7 @@ local function handle(params, client)
|
||||
elseif command == "listb" then
|
||||
for k, v in pairs(breakpoints) do
|
||||
local b = k .. ": " -- get filename
|
||||
for k, v in pairs(v) do
|
||||
for k in pairs(v) do
|
||||
b = b .. k .. " " -- get line numbers
|
||||
end
|
||||
print(b)
|
||||
@@ -696,6 +1046,36 @@ local function handle(params, client)
|
||||
for i, v in pairs(watches) do
|
||||
print("Watch exp. " .. i .. ": " .. v)
|
||||
end
|
||||
elseif command == "suspend" then
|
||||
client:send("SUSPEND\n")
|
||||
elseif command == "stack" then
|
||||
client:send("STACK\n")
|
||||
local resp = client:receive()
|
||||
local _, _, status, res = string.find(resp, "^(%d+)%s+%w+%s+(.+)%s*$")
|
||||
if status == "200" then
|
||||
local func, err = loadstring(res)
|
||||
if func == nil then
|
||||
print("Error in stack information: " .. err)
|
||||
return nil, nil, err
|
||||
end
|
||||
local stack = func()
|
||||
for _,frame in ipairs(stack) do
|
||||
-- remove basedir from short_src or source
|
||||
local src = string.gsub(frame[1][2], "\\", "/") -- convert slash
|
||||
if string.find(src, "@") == 1 then src = string.sub(src, 2) end
|
||||
if string.find(src, "%./") == 1 then src = string.sub(src, 3) end
|
||||
frame[1][2] = string.gsub(src, basedir, '') -- remove basedir
|
||||
print(serpent.line(frame[1], {comment = false}))
|
||||
end
|
||||
return stack
|
||||
elseif status == "401" then
|
||||
local res = "Error in stack result"
|
||||
print(res)
|
||||
return nil, nil, res
|
||||
else
|
||||
print("Unknown error")
|
||||
return nil, nil, "Debugger error: unexpected response after STACK"
|
||||
end
|
||||
elseif command == "basedir" then
|
||||
local _, _, dir = string.find(params, "^[a-z]+%s+(.+)$")
|
||||
if dir then
|
||||
@@ -713,16 +1093,17 @@ local function handle(params, client)
|
||||
print("setw <exp> -- adds a new watch expression")
|
||||
print("delw <index> -- removes the watch expression at index")
|
||||
print("delallw -- removes all watch expressions")
|
||||
print("run -- run until next breakpoint")
|
||||
print("step -- run until next line, stepping into function calls")
|
||||
print("over -- run until next line, stepping over function calls")
|
||||
print("out -- run until line after returning from current function")
|
||||
print("run -- runs until next breakpoint")
|
||||
print("step -- runs until next line, stepping into function calls")
|
||||
print("over -- runs until next line, stepping over function calls")
|
||||
print("out -- runs until line after returning from current function")
|
||||
print("listb -- lists breakpoints")
|
||||
print("listw -- lists watch expressions")
|
||||
print("eval <exp> -- evaluates expression on the current context and returns its value")
|
||||
print("exec <stmt> -- executes statement on the current context")
|
||||
print("load <file> -- loads a local file for debugging")
|
||||
print("reload -- restarts the current debugging session")
|
||||
print("stack -- reports stack trace")
|
||||
print("basedir [<path>] -- sets the base path of the remote application, or shows the current one")
|
||||
print("exit -- exits debugger")
|
||||
else
|
||||
@@ -737,6 +1118,8 @@ end
|
||||
|
||||
-- Starts debugging server
|
||||
local function listen(host, port)
|
||||
host = host or "*"
|
||||
port = port or mobdebug.port
|
||||
|
||||
local socket = require "socket"
|
||||
|
||||
@@ -772,9 +1155,12 @@ end
|
||||
-- make public functions available
|
||||
mobdebug.listen = listen
|
||||
mobdebug.loop = loop
|
||||
mobdebug.scratchpad = scratchpad
|
||||
mobdebug.handle = handle
|
||||
mobdebug.connect = connect
|
||||
mobdebug.start = start
|
||||
mobdebug.line = serpent.line
|
||||
mobdebug.dump = serpent.dump
|
||||
|
||||
-- this is needed to make "require 'modebug'" to work when mobdebug
|
||||
-- module is loaded manually
|
||||
|
||||
93
lualibs/ssl.lua
Normal file
93
lualibs/ssl.lua
Normal file
@@ -0,0 +1,93 @@
|
||||
------------------------------------------------------------------------------
|
||||
-- LuaSec 0.4.1
|
||||
-- Copyright (C) 2006-2011 Bruno Silvestre
|
||||
--
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
module("ssl", package.seeall)
|
||||
|
||||
require("ssl.core")
|
||||
require("ssl.context")
|
||||
|
||||
|
||||
_VERSION = "0.4.1"
|
||||
_COPYRIGHT = "LuaSec 0.4.1 - Copyright (C) 2006-2011 Bruno Silvestre\n" ..
|
||||
"LuaSocket 2.0.2 - Copyright (C) 2004-2007 Diego Nehab"
|
||||
|
||||
-- Export functions
|
||||
rawconnection = core.rawconnection
|
||||
rawcontext = context.rawcontext
|
||||
|
||||
--
|
||||
--
|
||||
--
|
||||
local function optexec(func, param, ctx)
|
||||
if param then
|
||||
if type(param) == "table" then
|
||||
return func(ctx, unpack(param))
|
||||
else
|
||||
return func(ctx, param)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--
|
||||
--
|
||||
--
|
||||
function newcontext(cfg)
|
||||
local succ, msg, ctx
|
||||
-- Create the context
|
||||
ctx, msg = context.create(cfg.protocol)
|
||||
if not ctx then return nil, msg end
|
||||
-- Mode
|
||||
succ, msg = context.setmode(ctx, cfg.mode)
|
||||
if not succ then return nil, msg end
|
||||
-- Load the key
|
||||
if cfg.key then
|
||||
succ, msg = context.loadkey(ctx, cfg.key, cfg.password)
|
||||
if not succ then return nil, msg end
|
||||
end
|
||||
-- Load the certificate
|
||||
if cfg.certificate then
|
||||
succ, msg = context.loadcert(ctx, cfg.certificate)
|
||||
if not succ then return nil, msg end
|
||||
end
|
||||
-- Load the CA certificates
|
||||
if cfg.cafile or cfg.capath then
|
||||
succ, msg = context.locations(ctx, cfg.cafile, cfg.capath)
|
||||
if not succ then return nil, msg end
|
||||
end
|
||||
-- Set the verification options
|
||||
succ, msg = optexec(context.setverify, cfg.verify, ctx)
|
||||
if not succ then return nil, msg end
|
||||
-- Set SSL options
|
||||
succ, msg = optexec(context.setoptions, cfg.options, ctx)
|
||||
if not succ then return nil, msg end
|
||||
-- Set the depth for certificate verification
|
||||
if cfg.depth then
|
||||
succ, msg = context.setdepth(ctx, cfg.depth)
|
||||
if not succ then return nil, msg end
|
||||
end
|
||||
return ctx
|
||||
end
|
||||
|
||||
--
|
||||
--
|
||||
--
|
||||
function wrap(sock, cfg)
|
||||
local ctx, msg
|
||||
if type(cfg) == "table" then
|
||||
ctx, msg = newcontext(cfg)
|
||||
if not ctx then return nil, msg end
|
||||
else
|
||||
ctx = cfg
|
||||
end
|
||||
local s, msg = core.create(ctx)
|
||||
if s then
|
||||
core.setfd(s, sock:getfd())
|
||||
sock:setfd(core.invalidfd)
|
||||
return s
|
||||
end
|
||||
return nil, msg
|
||||
end
|
||||
138
lualibs/ssl/https.lua
Normal file
138
lualibs/ssl/https.lua
Normal file
@@ -0,0 +1,138 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaSec 0.4.1
|
||||
-- Copyright (C) 2009-2011 PUC-Rio
|
||||
--
|
||||
-- Author: Pablo Musa
|
||||
-- Author: Tomas Guisasola
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
local socket = require("socket")
|
||||
local ssl = require("ssl")
|
||||
local ltn12 = require("ltn12")
|
||||
local http = require("socket.http")
|
||||
local url = require("socket.url")
|
||||
|
||||
local table = require("table")
|
||||
local string = require("string")
|
||||
|
||||
local try = socket.try
|
||||
local type = type
|
||||
local pairs = pairs
|
||||
local getmetatable = getmetatable
|
||||
|
||||
module("ssl.https")
|
||||
|
||||
_VERSION = "0.4.1"
|
||||
_COPYRIGHT = "LuaSec 0.4.1 - Copyright (C) 2009-2011 PUC-Rio"
|
||||
|
||||
-- Default settings
|
||||
PORT = 443
|
||||
|
||||
local cfg = {
|
||||
protocol = "tlsv1",
|
||||
options = "all",
|
||||
verify = "none",
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------
|
||||
-- Auxiliar Functions
|
||||
--------------------------------------------------------------------
|
||||
|
||||
-- Insert default HTTPS port.
|
||||
local function default_https_port(u)
|
||||
return url.build(url.parse(u, {port = PORT}))
|
||||
end
|
||||
|
||||
-- Convert an URL to a table according to Luasocket needs.
|
||||
local function urlstring_totable(url, body, result_table)
|
||||
url = {
|
||||
url = default_https_port(url),
|
||||
method = body and "POST" or "GET",
|
||||
sink = ltn12.sink.table(result_table)
|
||||
}
|
||||
if body then
|
||||
url.source = ltn12.source.string(body)
|
||||
url.headers = {
|
||||
["content-length"] = #body,
|
||||
["content-type"] = "application/x-www-form-urlencoded",
|
||||
}
|
||||
end
|
||||
return url
|
||||
end
|
||||
|
||||
-- Forward calls to the real connection object.
|
||||
local function reg(conn)
|
||||
local mt = getmetatable(conn.sock).__index
|
||||
for name, method in pairs(mt) do
|
||||
if type(method) == "function" then
|
||||
conn[name] = function (self, ...)
|
||||
return method(self.sock, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Return a function which performs the SSL/TLS connection.
|
||||
local function tcp(params)
|
||||
params = params or {}
|
||||
-- Default settings
|
||||
for k, v in pairs(cfg) do
|
||||
params[k] = params[k] or v
|
||||
end
|
||||
-- Force client mode
|
||||
params.mode = "client"
|
||||
-- 'create' function for LuaSocket
|
||||
return function ()
|
||||
local conn = {}
|
||||
conn.sock = try(socket.tcp())
|
||||
local st = getmetatable(conn.sock).__index.settimeout
|
||||
function conn:settimeout(...)
|
||||
return st(self.sock, ...)
|
||||
end
|
||||
-- Replace TCP's connection function
|
||||
function conn:connect(host, port)
|
||||
try(self.sock:connect(host, port))
|
||||
self.sock = try(ssl.wrap(self.sock, params))
|
||||
try(self.sock:dohandshake())
|
||||
reg(self, getmetatable(self.sock))
|
||||
return 1
|
||||
end
|
||||
return conn
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------
|
||||
-- Main Function
|
||||
--------------------------------------------------------------------
|
||||
|
||||
-- Make a HTTP request over secure connection. This function receives
|
||||
-- the same parameters of LuaSocket's HTTP module (except 'proxy' and
|
||||
-- 'redirect') plus LuaSec parameters.
|
||||
--
|
||||
-- @param url mandatory (string or table)
|
||||
-- @param body optional (string)
|
||||
-- @return (string if url == string or 1), code, headers, status
|
||||
--
|
||||
function request(url, body)
|
||||
local result_table = {}
|
||||
local stringrequest = type(url) == "string"
|
||||
if stringrequest then
|
||||
url = urlstring_totable(url, body, result_table)
|
||||
else
|
||||
url.url = default_https_port(url.url)
|
||||
end
|
||||
if http.PROXY or url.proxy then
|
||||
return nil, "proxy not supported"
|
||||
elseif url.redirect then
|
||||
return nil, "redirect not supported"
|
||||
elseif url.create then
|
||||
return nil, "create function not permitted"
|
||||
end
|
||||
-- New 'create' function to establish a secure connection
|
||||
url.create = tcp(url)
|
||||
local res, code, headers, status = http.request(url)
|
||||
if res and stringrequest then
|
||||
return table.concat(result_table), code, headers, status
|
||||
end
|
||||
return res, code, headers, status
|
||||
end
|
||||
315
lualibs/testwell.lua
Normal file
315
lualibs/testwell.lua
Normal file
@@ -0,0 +1,315 @@
|
||||
--
|
||||
-- Copyright (C) 2012 Paul Kulchenko
|
||||
-- A simple testing library
|
||||
-- Based on lua-TestMore : <http://fperrad.github.com/lua-TestMore/>
|
||||
-- Copyright (c) 2009-2011 Francois Perrad
|
||||
-- This library is licensed under the terms of the MIT/X11 license,
|
||||
-- like Lua itself.
|
||||
--
|
||||
|
||||
local pairs = pairs
|
||||
local tostring = tostring
|
||||
local type = type
|
||||
local _G = _G or _ENV
|
||||
|
||||
-----------------------------------------------------------
|
||||
|
||||
local tb = {
|
||||
curr_test = 0,
|
||||
good_test = 0,
|
||||
skip_test = 0,
|
||||
}
|
||||
|
||||
function tb:print(...)
|
||||
print(...)
|
||||
end
|
||||
|
||||
function tb:note(...)
|
||||
self:print(...)
|
||||
end
|
||||
|
||||
function tb:diag(...)
|
||||
local arg = {...}
|
||||
for k, v in pairs(arg) do
|
||||
arg[k] = tostring(v)
|
||||
end
|
||||
local msg = table.concat(arg)
|
||||
msg = msg:gsub("\n", "\n# ")
|
||||
msg = msg:gsub("\n# \n", "\n#\n")
|
||||
msg = msg:gsub("\n# $", '')
|
||||
self:print("# " .. msg)
|
||||
end
|
||||
|
||||
function tb:ok(test, name, more)
|
||||
self.curr_test = self.curr_test + 1
|
||||
self.good_test = self.good_test + (test and 1 or 0)
|
||||
self.skip_test = self.skip_test + (test == nil and 1 or 0)
|
||||
name = tostring(name or '')
|
||||
local out = ''
|
||||
if not test then
|
||||
out = "not "
|
||||
end
|
||||
out = out .. "ok " .. self.curr_test
|
||||
if name ~= '' then
|
||||
out = out .. " - " .. name
|
||||
end
|
||||
self:print(out)
|
||||
if test == false then
|
||||
self:diag(" Failed test " .. (name and ("'" .. name .. "'") or ''))
|
||||
if debug then
|
||||
local info = debug.getinfo(3)
|
||||
local file = info.short_src
|
||||
local line = info.currentline
|
||||
self:diag(" in " .. file .. " at line " .. line .. ".")
|
||||
end
|
||||
self:diag(more)
|
||||
end
|
||||
end
|
||||
|
||||
function tb:done_testing(reset)
|
||||
local c, g, s = self.curr_test, self.good_test, self.skip_test
|
||||
if reset then
|
||||
self.curr_test = 0
|
||||
self.good_test = 0
|
||||
self.skip_test = 0
|
||||
end
|
||||
return c, g, s
|
||||
end
|
||||
|
||||
-----------------------------------------------------------
|
||||
|
||||
local serpent = (function() ---- include Serpent module for serialization
|
||||
local n, v = "serpent", 0.15 -- (C) 2012 Paul Kulchenko; MIT License
|
||||
local c, d = "Paul Kulchenko", "Serializer and pretty printer of Lua data types"
|
||||
local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
|
||||
local badtype = {thread = true, userdata = true}
|
||||
local keyword, globals, G = {}, {}, (_G or _ENV)
|
||||
for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
|
||||
'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
|
||||
'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end
|
||||
for k,v in pairs(G) do globals[v] = k end -- build func to name mapping
|
||||
for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do
|
||||
for k,v in pairs(G[g]) do globals[v] = g..'.'..k end end
|
||||
|
||||
local function s(t, opts)
|
||||
local name, indent, fatal = opts.name, opts.indent, opts.fatal
|
||||
local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge
|
||||
local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
|
||||
local comm = opts.comment and (tonumber(opts.comment) or math.huge)
|
||||
local seen, sref, syms, symn = {}, {}, {}, 0
|
||||
local function gensym(val) return tostring(val):gsub("[^%w]",""):gsub("(%d%w+)",
|
||||
function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return syms[s] end) end
|
||||
local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or s)
|
||||
or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
|
||||
or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end
|
||||
local function comment(s,l) return comm and (l or 0) < comm and ' --[['..tostring(s)..']]' or '' end
|
||||
local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal
|
||||
and safestr(tostring(s))..comment('err',l) or error("Can't serialize "..tostring(s)) end
|
||||
local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']
|
||||
local n = name == nil and '' or name
|
||||
local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]
|
||||
local safe = plain and n or '['..safestr(n)..']'
|
||||
return (path or '')..(plain and path and '.' or '')..safe, safe end
|
||||
local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(o, n)
|
||||
local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}
|
||||
local function padnum(d) return ("%0"..maxn.."d"):format(d) end
|
||||
table.sort(o, function(a,b)
|
||||
return (o[a] and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
|
||||
< (o[b] and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
|
||||
local function val2str(t, name, indent, path, plainindex, level)
|
||||
local ttype, level = type(t), (level or 0)
|
||||
local spath, sname = safename(path, name)
|
||||
local tag = plainindex and
|
||||
((type(name) == "number") and '' or name..space..'='..space) or
|
||||
(name ~= nil and sname..space..'='..space or '')
|
||||
if seen[t] then
|
||||
table.insert(sref, spath..space..'='..space..seen[t])
|
||||
return tag..'nil'..comment('ref', level)
|
||||
elseif badtype[ttype] then return tag..globerr(t, level)
|
||||
elseif ttype == 'function' then
|
||||
seen[t] = spath
|
||||
local ok, res = pcall(string.dump, t)
|
||||
local func = ok and ((opts.nocode and "function() end" or
|
||||
"loadstring("..safestr(res)..",'@serialized')")..comment(t, level))
|
||||
return tag..(func or globerr(t, level))
|
||||
elseif ttype == "table" then
|
||||
if level >= maxl then return tag..'{}'..comment('max', level) end
|
||||
seen[t] = spath
|
||||
if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
|
||||
local maxn, o, out = #t, {}, {}
|
||||
for key = 1, maxn do table.insert(o, key) end
|
||||
for key in pairs(t) do if not o[key] then table.insert(o, key) end end
|
||||
if opts.sortkeys then alphanumsort(o, opts.sortkeys) end
|
||||
for n, key in ipairs(o) do
|
||||
local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse
|
||||
if opts.ignore and opts.ignore[value] -- skip ignored values; do nothing
|
||||
or sparse and value == nil then -- skipping nils; do nothing
|
||||
elseif ktype == 'table' or ktype == 'function' then
|
||||
if not seen[key] and not globals[key] then
|
||||
table.insert(sref, 'local '..val2str(key,gensym(key),indent)) end
|
||||
table.insert(sref, seen[t]..'['..(seen[key] or globals[key] or gensym(key))
|
||||
..']'..space..'='..space..(seen[value] or val2str(value,nil,indent)))
|
||||
else
|
||||
if badtype[ktype] then plainindex, key = true, '['..globerr(key, level+1)..']' end
|
||||
table.insert(out,val2str(value,key,indent,spath,plainindex,level+1))
|
||||
end
|
||||
end
|
||||
local prefix = string.rep(indent or '', level)
|
||||
local head = indent and '{\n'..prefix..indent or '{'
|
||||
local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
|
||||
local tail = indent and "\n"..prefix..'}' or '}'
|
||||
return (custom and custom(tag,head,body,tail) or tag..head..body..tail)..comment(t, level)
|
||||
else return tag..safestr(t) end -- handle all other types
|
||||
end
|
||||
local sepr = indent and "\n" or ";"..space
|
||||
local body = val2str(t, name, indent) -- this call also populates sref
|
||||
local tail = #sref>0 and table.concat(sref, sepr)..sepr or ''
|
||||
return not name and body or "do local "..body..sepr..tail.."return "..name..sepr.."end"
|
||||
end
|
||||
|
||||
local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end
|
||||
return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,
|
||||
dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end,
|
||||
line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end,
|
||||
block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end }
|
||||
end)() ---- end of Serpent module
|
||||
|
||||
-----------------------------------------------------------
|
||||
|
||||
local m = {}
|
||||
|
||||
function m.ok(test, name)
|
||||
tb:ok(test, name)
|
||||
end
|
||||
|
||||
local parms = {comment = false}
|
||||
function m.is(got, expected, name)
|
||||
if type(got) == 'table' then got = serpent.line(got, parms) end
|
||||
if type(expected) == 'table' then expected = serpent.line(expected, parms) end
|
||||
local pass = got == expected
|
||||
if got == nil then pass = nil end
|
||||
tb:ok(pass, name, not pass and
|
||||
" got: " .. tostring(got) ..
|
||||
"\n expected: " .. tostring(expected))
|
||||
end
|
||||
|
||||
function m.isnt(got, expected, name)
|
||||
if type(got) == 'table' then got = serpent.line(got, parms) end
|
||||
if type(expected) == 'table' then expected = serpent.line(expected, parms) end
|
||||
local pass = got ~= expected
|
||||
if got == nil then pass = nil end
|
||||
tb:ok(pass, name, not pass and
|
||||
" got: " .. tostring(got) ..
|
||||
"\n expected: anything else")
|
||||
end
|
||||
|
||||
function m.like(got, pattern, name)
|
||||
if type(pattern) ~= 'string' then
|
||||
return tb:ok(false, name, "pattern isn't a string : " .. tostring(pattern))
|
||||
end
|
||||
|
||||
local pass = tostring(got):match(pattern)
|
||||
if got == nil then pass = nil end
|
||||
tb:ok(pass, name, not pass and
|
||||
" '" .. tostring(got) .. "'" ..
|
||||
"\n doesn't match '" .. pattern .. "'")
|
||||
end
|
||||
|
||||
function m.unlike(got, pattern, name)
|
||||
if type(pattern) ~= 'string' then
|
||||
return tb:ok(false, name, "pattern isn't a string : " .. tostring(pattern))
|
||||
end
|
||||
|
||||
local pass = not tostring(got):match(pattern)
|
||||
if got == nil then pass = nil end
|
||||
tb:ok(pass, name, not pass and
|
||||
" '" .. tostring(got) .. "'" ..
|
||||
"\n matches '" .. pattern .. "'")
|
||||
end
|
||||
|
||||
local cmp = {
|
||||
['<'] = function (a, b) return a < b end,
|
||||
['<='] = function (a, b) return a <= b end,
|
||||
['>'] = function (a, b) return a > b end,
|
||||
['>='] = function (a, b) return a >= b end,
|
||||
['=='] = function (a, b) return a == b end,
|
||||
['~='] = function (a, b) return a ~= b end,
|
||||
}
|
||||
|
||||
function m.cmp_ok(this, op, that, name)
|
||||
local f = cmp[op]
|
||||
if not f then
|
||||
return tb:ok(false, name, "unknown operator : " .. tostring(op))
|
||||
end
|
||||
|
||||
local pass = f(this, that)
|
||||
if this == nil then pass = nil end
|
||||
tb:ok(pass, name, not pass and
|
||||
" " .. tostring(this) ..
|
||||
"\n " .. op ..
|
||||
"\n " .. tostring(that))
|
||||
end
|
||||
|
||||
function m.type_ok(val, t, name)
|
||||
if type(t) ~= 'string' then
|
||||
return tb:ok(false, name, "type isn't a string : " .. tostring(t))
|
||||
end
|
||||
|
||||
if type(val) == t then
|
||||
tb:ok(true, name)
|
||||
else
|
||||
tb:ok(false, name,
|
||||
" " .. tostring(val) .. " isn't a '" .. t .."', it's a '" .. type(val) .. "'")
|
||||
end
|
||||
end
|
||||
|
||||
function m.diag(...)
|
||||
tb:diag(...)
|
||||
end
|
||||
|
||||
function m.report()
|
||||
local total, good, skipped = tb:done_testing(true)
|
||||
if total == 0 then return end
|
||||
local failed = total - good - skipped
|
||||
local sum = ("(%d/%d/%d)."):format(good, skipped, total)
|
||||
local num, msg = 0, ""
|
||||
if good > 0 then
|
||||
num, msg = good, msg .. "passed " .. good
|
||||
end
|
||||
if failed > 0 then
|
||||
num, msg = failed, msg .. (#msg > 0 and (skipped > 0 and ", " or " and ") or "")
|
||||
.. "failed " .. failed
|
||||
end
|
||||
if skipped > 0 then
|
||||
num, msg = skipped, msg .. (#msg > 0 and ((good > 0 and failed > 0 and ',' or '') .." and ") or "")
|
||||
.. "skipped " .. skipped
|
||||
end
|
||||
msg = ("Looks like you %s test%s of %d %s"):format(msg, (num > 1 and 's' or ''), total, sum)
|
||||
if skipped == total then msg = "Looks like you skipped all tests " .. sum end
|
||||
if good == total then msg = "All tests passed " .. sum end
|
||||
tb:note(("1..%d # %s"):format(total, msg))
|
||||
end
|
||||
|
||||
function m.ismain()
|
||||
for l = 3, 64 do -- only check up to 64 level; no more needed
|
||||
local info = debug.getinfo(l)
|
||||
if not info then return true end
|
||||
if info.func == require then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- this is needed to call report() when the test object is destroyed
|
||||
if _VERSION >= "Lua 5.2" then
|
||||
setmetatable(m, {__gc = m.report})
|
||||
else
|
||||
-- keep sentinel alive until 'm' is garbage collected
|
||||
m.sentinel = newproxy(true)
|
||||
getmetatable(m.sentinel).__gc = m.report
|
||||
end
|
||||
|
||||
for k, v in pairs(m) do -- injection
|
||||
_G[k] = v
|
||||
end
|
||||
|
||||
return m
|
||||
15
spec/lua.lua
15
spec/lua.lua
@@ -1,6 +1,8 @@
|
||||
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
|
||||
---------------------------------------------------------
|
||||
|
||||
local funcdef = "([A-Za-z_][A-Za-z0-9_%.%:]*)%s*"
|
||||
local funccall = "([A-Za-z_][A-Za-z0-9_]*)%s*"
|
||||
return {
|
||||
exts = {"lua"},
|
||||
lexer = wxstc.wxSTC_LEX_LUA,
|
||||
@@ -8,23 +10,18 @@ return {
|
||||
linecomment = "--",
|
||||
sep = "%.:",
|
||||
isfncall = function(str)
|
||||
return string.find(str,"([A-Za-z0-9_]+)%s*%(")
|
||||
return string.find(str, funccall .. "%(")
|
||||
end,
|
||||
isfndef = function(str)
|
||||
local l
|
||||
local s,e,cap,par = string.find(str,"function%s+([A-Za-z0-9_]+%s-[%.%:]?%s-[A-Za-z0-9_]*)%s*(%(.-%))")
|
||||
local s,e,cap,par = string.find(str, "function%s+" .. funcdef .. "(%(.-%))")
|
||||
-- try to match without brackets now, but only at the beginning of the line
|
||||
if (not s) then
|
||||
s,e,cap = string.find(str,"^%s*function%s+([A-Za-z0-9_]+%s-[%.%:]?%s-[A-Za-z0-9_]*)%s*")
|
||||
s,e,cap = string.find(str, "^%s*function%s+" .. funcdef)
|
||||
end
|
||||
-- try to match "foo = function()"
|
||||
if (not s) then
|
||||
s,e,cap,cap1,cap2,par = string.find(str,"(([A-Za-z0-9_]+%s-[%.%:]?%s-)([A-Za-z0-9_]*))%s*=%s*function%s*(%(.-%))")
|
||||
-- check if we captured 'local foo =' instead of 'foo.bar ='
|
||||
if cap1 and cap2 and string.len(cap2) > 0 and not string.find(cap1,'[%.%:]') then
|
||||
cap = cap2
|
||||
s = s + string.len(cap1)
|
||||
end
|
||||
s,e,cap,par = string.find(str, funcdef .. "=%s*function%s*(%(.-%))")
|
||||
end
|
||||
if (s) then
|
||||
l = string.find(string.sub(str,1,s-1),"local%s+$")
|
||||
|
||||
16
src/defs.lua
16
src/defs.lua
@@ -166,6 +166,10 @@ config = {
|
||||
|
||||
singleinstanceport = 0xe493,
|
||||
-- UDP port for single instance communication
|
||||
|
||||
activateoutput = false, -- activate output/console on Run/Debug/Compile
|
||||
unhidewxwindow = false, -- try to unhide a wx window
|
||||
allowinteractivescript = false, -- allow interaction in the output window
|
||||
}
|
||||
|
||||
-- application engine
|
||||
@@ -306,12 +310,10 @@ debuginterface = {
|
||||
interpreter = {
|
||||
name = "",
|
||||
description = "",
|
||||
api = {"apifile_without_extension"} -- optional to limit loaded lua apis
|
||||
frun = function(self,wfilename,withdebugger)
|
||||
end,
|
||||
fprojdir = function(self,wfilename)
|
||||
return "projpath_from_filename" -- optional
|
||||
end,
|
||||
fattachdebug = function(self) end, -- optional
|
||||
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 = false, -- (opt) if scratchpad requires handling for external loop
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ local function addAPI(apifile,only,subapis,known) -- relative to API directory
|
||||
end
|
||||
|
||||
local function loadallAPIs (only,subapis,known)
|
||||
for i,dir in ipairs(FileSysGet(".\\api\\*.*",wx.wxDIR)) do
|
||||
local files = FileSysGet(dir.."\\*.*",wx.wxFILE)
|
||||
for i,dir in ipairs(FileSysGet("./api/*",wx.wxDIR)) do
|
||||
local files = FileSysGet(dir.."/*.*",wx.wxFILE)
|
||||
for i,file in ipairs(files) do
|
||||
if file:match "%.lua$" then
|
||||
addAPI(file,only,subapis,known)
|
||||
@@ -239,7 +239,7 @@ end
|
||||
|
||||
function GetTipInfo(editor, content, short)
|
||||
local caller = content:match("([%w_]+)%(%s*$")
|
||||
local class = caller and content:match("([%w_%.]+)[%.:]"..caller.."%(%s*$")
|
||||
local class = caller and content:match("([%w_]+)[%.:]"..caller.."%(%s*$")
|
||||
local tip = editor.api.tip
|
||||
|
||||
local classtab = short and tip.shortfinfoclass or tip.finfoclass
|
||||
|
||||
@@ -29,7 +29,7 @@ end
|
||||
function LoadFile(filePath, editor, file_must_exist)
|
||||
filePath = wx.wxFileName(filePath):GetFullPath()
|
||||
local cmpName = string.lower(string.gsub(filePath, "\\", "/"))
|
||||
|
||||
|
||||
-- prevent files from being reopened again
|
||||
if (not editor) then
|
||||
for id, doc in pairs(openDocuments) do
|
||||
@@ -54,12 +54,10 @@ function LoadFile(filePath, editor, file_must_exist)
|
||||
return nil
|
||||
end
|
||||
|
||||
if not editor then
|
||||
editor = findDocumentToReuse()
|
||||
end
|
||||
if not editor then
|
||||
editor = CreateEditor(wx.wxFileName(filePath):GetFullName() or "untitled.lua")
|
||||
end
|
||||
local current = editor and editor:GetCurrentPos()
|
||||
editor = editor
|
||||
or findDocumentToReuse()
|
||||
or CreateEditor(wx.wxFileName(filePath):GetFullName() or "untitled.lua")
|
||||
|
||||
editor:Clear()
|
||||
editor:ClearAll()
|
||||
@@ -67,6 +65,7 @@ function LoadFile(filePath, editor, file_must_exist)
|
||||
editor:MarkerDeleteAll(BREAKPOINT_MARKER)
|
||||
editor:MarkerDeleteAll(CURRENT_LINE_MARKER)
|
||||
editor:AppendText(file_text)
|
||||
if current then editor:GotoPos(current) end
|
||||
if (ide.config.editor.autotabs) then
|
||||
local found = string.find(file_text,"\t") ~= nil
|
||||
editor:SetUseTabs(found)
|
||||
@@ -83,8 +82,10 @@ function LoadFile(filePath, editor, file_must_exist)
|
||||
IndicateFunctions(editor)
|
||||
|
||||
SettingsAppendFileToHistory(filePath)
|
||||
|
||||
SetEditorSelection(nil)
|
||||
|
||||
-- activate the editor; this is needed for those cases when the editor is
|
||||
-- created from some other element, for example, from a project tree.
|
||||
SetEditorSelection()
|
||||
|
||||
return editor
|
||||
end
|
||||
@@ -114,7 +115,7 @@ function OpenFile(event)
|
||||
"",
|
||||
"",
|
||||
exts,
|
||||
wx.wxOPEN + wx.wxFILE_MUST_EXIST)
|
||||
wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST)
|
||||
if fileDialog:ShowModal() == wx.wxID_OK then
|
||||
if not LoadFile(fileDialog:GetPath(), nil, true) then
|
||||
wx.wxMessageBox("Unable to load file '"..fileDialog:GetPath().."'.",
|
||||
@@ -181,12 +182,15 @@ function SaveFileAs(editor)
|
||||
fn:GetPath(wx.wxPATH_GET_VOLUME),
|
||||
fn:GetFullName(),
|
||||
exts,
|
||||
wx.wxSAVE)
|
||||
wx.wxFD_SAVE)
|
||||
|
||||
if fileDialog:ShowModal() == wx.wxID_OK then
|
||||
local filePath = fileDialog:GetPath()
|
||||
|
||||
if SaveFile(editor, filePath) then
|
||||
SetEditorSelection() -- update title of the editor
|
||||
FileTreeRefresh() -- refresh the tree to reflect the new file
|
||||
FileTreeMarkSelected(filePath)
|
||||
SetupKeywords(editor, GetFileExt(filePath))
|
||||
IndicateFunctions(editor)
|
||||
if MarkupStyle then MarkupStyle(editor) end
|
||||
@@ -253,7 +257,7 @@ local function removePage(index)
|
||||
notebook:SetSelection(prevIndex)
|
||||
end
|
||||
|
||||
SetEditorSelection(nil) -- will use notebook GetSelection to update
|
||||
SetEditorSelection() -- will use notebook GetSelection to update
|
||||
end
|
||||
|
||||
function ClosePage(selection)
|
||||
@@ -261,6 +265,19 @@ function ClosePage(selection)
|
||||
local id = editor:GetId()
|
||||
if SaveModifiedDialog(editor, true) ~= wx.wxID_CANCEL then
|
||||
DynamicWordsRemoveAll(editor)
|
||||
local debugger = ide.debugger
|
||||
-- check if the window with the scratchpad running is being closed
|
||||
if debugger and debugger.scratchpad and debugger.scratchpad.editor == editor then
|
||||
DebuggerScratchpadOff()
|
||||
end
|
||||
-- check if the debugger is running and is using the current window
|
||||
-- abort the debugger if the current marker is in the window being closed
|
||||
-- also abort the debugger if it is running, as we don't know what
|
||||
-- window will need to be activated when the debugger is paused
|
||||
if debugger and debugger.pid and
|
||||
(debugger.running or editor:MarkerNext(0, CURRENT_LINE_MARKER_VALUE) >= 0) then
|
||||
debugger.terminate()
|
||||
end
|
||||
removePage(ide.openDocuments[id].index)
|
||||
end
|
||||
end
|
||||
@@ -297,8 +314,13 @@ function SaveOnExit(allow_cancel)
|
||||
if (SaveModifiedDialog(document.editor, allow_cancel) == wx.wxID_CANCEL) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
document.isModified = false
|
||||
-- if all documents have been saved or refused to save, then mark those that
|
||||
-- are still modified as not modified (they don't need to be saved)
|
||||
-- to keep their tab names correct
|
||||
for id, document in pairs(openDocuments) do
|
||||
if document.isModified then SetDocumentModified(id, false) end
|
||||
end
|
||||
|
||||
return true
|
||||
@@ -383,20 +405,23 @@ function ClearAllCurrentLineMarkers()
|
||||
end
|
||||
end
|
||||
|
||||
function CompileProgram(editor)
|
||||
local editorText = editor:GetText()
|
||||
function CompileProgram(editor, quiet)
|
||||
-- remove shebang line (#!) as it throws a compilation error as
|
||||
-- loadstring() doesn't allow it even though lua/loadfile accepts it.
|
||||
-- replace with a new line to keep the number of lines the same.
|
||||
local editorText = editor:GetText():gsub("^#!.-\n", "\n")
|
||||
local id = editor:GetId()
|
||||
local filePath = DebuggerMakeFileName(editor, openDocuments[id].filePath)
|
||||
local ret, errMsg, line_num = wxlua.CompileLuaScript(editorText, filePath)
|
||||
if ide.frame.menuBar:IsChecked(ID_CLEAROUTPUT) then
|
||||
ClearOutput()
|
||||
end
|
||||
local _, errMsg, line_num = wxlua.CompileLuaScript(editorText, filePath)
|
||||
|
||||
if ide.frame.menuBar:IsChecked(ID_CLEAROUTPUT) then ClearOutput() end
|
||||
|
||||
if line_num > -1 then
|
||||
DisplayOutput("Compilation error on line number :"..tostring(line_num).."\n"..errMsg.."\n")
|
||||
editor:GotoLine(line_num-1)
|
||||
DisplayOutput("Compilation error on line "..tostring(line_num)..":\n"..
|
||||
errMsg:gsub("Lua:.-\n", "").."\n")
|
||||
if not quiet then editor:GotoLine(line_num-1) end
|
||||
else
|
||||
DisplayOutput("Compilation successful.\n")
|
||||
if not quiet then DisplayOutput("Compilation successful.\n") end
|
||||
end
|
||||
|
||||
return line_num == -1 -- return true if it compiled ok
|
||||
@@ -474,6 +499,7 @@ function ShowFullScreen(setFullScreen)
|
||||
beforeFullScreenPerspective = uimgr:SavePerspective()
|
||||
uimgr:GetPane("bottomnotebook"):Show(false)
|
||||
uimgr:GetPane("projpanel"):Show(false)
|
||||
SetEditorSelection() -- make sure the focus is on the editor
|
||||
elseif beforeFullScreenPerspective then
|
||||
uimgr:LoadPerspective(beforeFullScreenPerspective)
|
||||
beforeFullScreenPerspective = nil
|
||||
@@ -481,15 +507,16 @@ function ShowFullScreen(setFullScreen)
|
||||
|
||||
uimgr:GetPane("toolBar"):Show(not setFullScreen)
|
||||
uimgr:Update()
|
||||
frame:ShowFullScreen(setFullScreen)
|
||||
-- protect from systems that don't have ShowFullScreen (GTK on linux?)
|
||||
pcall(function() frame:ShowFullScreen(setFullScreen) end)
|
||||
end
|
||||
|
||||
function CloseWindow(event)
|
||||
exitingProgram = true -- don't handle focus events
|
||||
ide.exitingProgram = true -- don't handle focus events
|
||||
|
||||
if not SaveOnExit(event:CanVeto()) then
|
||||
event:Veto()
|
||||
exitingProgram = false
|
||||
ide.exitingProgram = false
|
||||
return
|
||||
end
|
||||
|
||||
@@ -499,9 +526,14 @@ function CloseWindow(event)
|
||||
SettingsSaveView()
|
||||
SettingsSaveFramePosition(ide.frame, "MainFrame")
|
||||
SettingsSaveEditorSettings()
|
||||
DebuggerCloseWatchWindow()
|
||||
DebuggerKillClient()
|
||||
if DebuggerCloseWatchWindow then DebuggerCloseWatchWindow() end
|
||||
if DebuggerCloseStackWindow then DebuggerCloseStackWindow() end
|
||||
if DebuggerShutdown then DebuggerShutdown() end
|
||||
ide.settings:delete() -- always delete the config
|
||||
event:Skip()
|
||||
|
||||
-- without explicit exit() the IDE crashes with SIGILL exception when closed
|
||||
-- on MacOS compiled under 64bit with wxwidgets 2.9.3
|
||||
if ide.osname == "Macintosh" then os.exit() end
|
||||
end
|
||||
frame:Connect(wx.wxEVT_CLOSE_WINDOW, CloseWindow)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Integration with MobDebug
|
||||
-- Copyright Paul Kulchenko 2011
|
||||
-- Copyright 2011-12 Paul Kulchenko
|
||||
-- Original authors: Lomtik Software (J. Winwood & John Labenski)
|
||||
-- Luxinia Dev (Eike Decker & Christoph Kubisch)
|
||||
|
||||
@@ -12,31 +12,132 @@ local debugger = ide.debugger
|
||||
debugger.server = nil -- DebuggerServer object when debugging, else nil
|
||||
debugger.running = false -- true when the debuggee is running
|
||||
debugger.listening = false -- true when the debugger is listening for a client
|
||||
debugger.portnumber = 8171 -- the port # to use for debugging
|
||||
debugger.portnumber = mobdebug.port or 8171 -- the port # to use for debugging
|
||||
debugger.watchWindow = nil -- the watchWindow, nil when not created
|
||||
debugger.watchListCtrl = nil -- the child listctrl in the watchWindow
|
||||
debugger.watchCtrl = nil -- the child ctrl in the watchWindow
|
||||
debugger.stackWindow = nil -- the stackWindow, nil when not created
|
||||
debugger.stackCtrl = nil -- the child ctrl in the stackWindow
|
||||
debugger.hostname = (function() -- check what address is resolvable
|
||||
local addr = wx.wxIPV4address()
|
||||
for _, host in ipairs({wx.wxGetHostName(), wx.wxGetFullHostName()}) do
|
||||
if addr:Hostname(host) then return host end
|
||||
end
|
||||
return "localhost" -- last resort; no known good hostname
|
||||
end)()
|
||||
|
||||
local notebook = ide.frame.notebook
|
||||
|
||||
local function updateWatchesSync()
|
||||
local watchListCtrl = debugger.watchListCtrl
|
||||
if watchListCtrl and debugger.server and not debugger.running then
|
||||
for idx = 0, watchListCtrl:GetItemCount() - 1 do
|
||||
local expression = watchListCtrl:GetItemText(idx)
|
||||
local value, _, error = debugger.evaluate(expression)
|
||||
watchListCtrl:SetItem(idx, 1, value or ('error: ' .. error))
|
||||
local watchCtrl = debugger.watchCtrl
|
||||
if watchCtrl and debugger.server
|
||||
and not debugger.running and not debugger.scratchpad then
|
||||
for idx = 0, watchCtrl:GetItemCount() - 1 do
|
||||
local expression = watchCtrl:GetItemText(idx)
|
||||
local _, values, error = debugger.evaluate(expression)
|
||||
if error then error = error:gsub("%[.-%]:%d+:%s+","")
|
||||
elseif #values == 0 then values = {'nil'} end
|
||||
watchCtrl:SetItem(idx, 1, error and ('error: '..error) or values[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local simpleType = {['nil'] = true, ['string'] = true, ['number'] = true, ['boolean'] = true}
|
||||
local stackItemValue = {}
|
||||
local function checkIfExpandable(value, item)
|
||||
local expandable = type(value) == 'table' and next(value) ~= nil
|
||||
and not stackItemValue[value] -- only expand first time
|
||||
if expandable then -- cache table value to expand when requested
|
||||
stackItemValue[item:GetValue()] = value
|
||||
stackItemValue[value] = item:GetValue() -- to avoid circular refs
|
||||
end
|
||||
return expandable
|
||||
end
|
||||
|
||||
local function updateStackSync()
|
||||
local stackCtrl = debugger.stackCtrl
|
||||
if stackCtrl and debugger.server
|
||||
and not debugger.running and not debugger.scratchpad then
|
||||
local stack = debugger.stack()
|
||||
if not stack or #stack == 0 then stackCtrl:DeleteAllItems(); return end
|
||||
stackCtrl:Freeze()
|
||||
stackCtrl:DeleteAllItems()
|
||||
local params = {comment = false, nocode = true}
|
||||
local root = stackCtrl:AddRoot("Stack")
|
||||
stackItemValue = {} -- reset cache of items in the stack
|
||||
for _,frame in ipairs(stack) do
|
||||
-- "main chunk at line 24"
|
||||
-- "foo() at line 13 (defined at foobar.lua:11)"
|
||||
-- call = { source.name, source.source, source.linedefined,
|
||||
-- source.currentline, source.what, source.namewhat, source.short_src }
|
||||
local call = frame[1]
|
||||
local func = call[5] == "main" and "main chunk"
|
||||
or call[5] == "C" and (call[1] or "C function")
|
||||
or call[5] == "tail" and "tail call"
|
||||
or (call[1] or "anonymous function")
|
||||
local text = func ..
|
||||
(call[4] == -1 and '' or " at line "..call[4]) ..
|
||||
(call[5] ~= "main" and call[5] ~= "Lua" and ''
|
||||
or (call[3] > 0 and " (defined at "..call[2]..":"..call[3]..")"
|
||||
or " (defined in "..call[2]..")"))
|
||||
local callitem = stackCtrl:AppendItem(root, text, 0)
|
||||
for name,val in pairs(frame[2]) do
|
||||
local value, comment = val[1], val[2]
|
||||
local text = ("%s = %s%s"):
|
||||
format(name, mobdebug.line(value, params),
|
||||
simpleType[type(value)] and "" or (" --[["..comment.."]]"))
|
||||
local item = stackCtrl:AppendItem(callitem, text, 1)
|
||||
if checkIfExpandable(value, item) then
|
||||
stackCtrl:SetItemHasChildren(item, true)
|
||||
end
|
||||
end
|
||||
for name,val in pairs(frame[3]) do
|
||||
local value, comment = val[1], val[2]
|
||||
local text = ("%s = %s%s"):
|
||||
format(name, mobdebug.line(value, params),
|
||||
simpleType[type(value)] and "" or (" --[["..comment.."]]"))
|
||||
local item = stackCtrl:AppendItem(callitem, text, 2)
|
||||
if checkIfExpandable(value, item) then
|
||||
stackCtrl:SetItemHasChildren(item, true)
|
||||
end
|
||||
end
|
||||
stackCtrl:SortChildren(callitem)
|
||||
stackCtrl:Expand(callitem)
|
||||
end
|
||||
stackCtrl:EnsureVisible(stackCtrl:GetFirstChild(root))
|
||||
stackCtrl:Thaw()
|
||||
end
|
||||
end
|
||||
|
||||
local function updateStackAndWatches()
|
||||
if debugger.server and not debugger.running then
|
||||
copas.addthread(function() updateStackSync() updateWatchesSync() end)
|
||||
end
|
||||
end
|
||||
|
||||
local function updateWatches()
|
||||
if debugger.watchListCtrl and debugger.server and not debugger.running then
|
||||
copas.addthread(updateWatchesSync)
|
||||
if debugger.server and not debugger.running then
|
||||
copas.addthread(function() updateWatchesSync() end)
|
||||
end
|
||||
end
|
||||
|
||||
local function killClient()
|
||||
if (debugger.pid) then
|
||||
-- using SIGTERM for some reason kills not only the debugee process,
|
||||
-- but also some system processes, which leads to a blue screen crash
|
||||
-- (at least on Windows Vista SP2)
|
||||
local ret = wx.wxProcess.Kill(debugger.pid, wx.wxSIGKILL, wx.wxKILL_CHILDREN)
|
||||
if ret == wx.wxKILL_OK then
|
||||
DisplayOutput(("Program stopped (pid: %d).\n"):format(debugger.pid))
|
||||
elseif ret ~= wx.wxKILL_NO_PROCESS then
|
||||
DisplayOutput(("Unable to stop program (pid: %d), code %d.\n")
|
||||
:format(debugger.pid, ret))
|
||||
end
|
||||
debugger.pid = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function activateDocument(fileName, line)
|
||||
if (not fileName and line) then return end
|
||||
if not fileName then return end
|
||||
|
||||
if not wx.wxIsAbsolutePath(fileName) then
|
||||
fileName = wx.wxGetCwd().."/"..fileName
|
||||
@@ -46,43 +147,79 @@ local function activateDocument(fileName, line)
|
||||
fileName = wx.wxUnix2DosFilename(fileName)
|
||||
end
|
||||
|
||||
local fileFound = false
|
||||
for id, document in pairs(ide.openDocuments) do
|
||||
local activated
|
||||
for _, document in pairs(ide.openDocuments) do
|
||||
local editor = document.editor
|
||||
-- for running in cygwin, use same type of separators
|
||||
filePath = string.gsub(document.filePath, "\\", "/")
|
||||
local filePath = string.gsub(document.filePath, "\\", "/")
|
||||
local fileName = string.gsub(fileName, "\\", "/")
|
||||
if string.upper(filePath) == string.upper(fileName) then
|
||||
local selection = document.index
|
||||
notebook:SetSelection(selection)
|
||||
SetEditorSelection(selection)
|
||||
ClearAllCurrentLineMarkers()
|
||||
editor:MarkerAdd(line-1, CURRENT_LINE_MARKER)
|
||||
editor:EnsureVisibleEnforcePolicy(line-1)
|
||||
fileFound = true
|
||||
if line then
|
||||
editor:MarkerAdd(line-1, CURRENT_LINE_MARKER)
|
||||
editor:EnsureVisibleEnforcePolicy(line-1)
|
||||
end
|
||||
activated = editor
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return fileFound
|
||||
return activated ~= nil, activated
|
||||
end
|
||||
|
||||
debugger.shell = function(expression)
|
||||
local function reSetBreakpoints()
|
||||
-- remove all breakpoints that may still be present from the last session
|
||||
-- this only matters for those remote clients that reload scripts
|
||||
-- without resetting their breakpoints
|
||||
debugger.handle("delallb")
|
||||
|
||||
-- go over all windows and find all breakpoints
|
||||
if (not debugger.scratchpad) then
|
||||
for _, document in pairs(ide.openDocuments) do
|
||||
local editor = document.editor
|
||||
local filePath = document.filePath
|
||||
local line = editor:MarkerNext(0, BREAKPOINT_MARKER_VALUE)
|
||||
while line ~= -1 do
|
||||
debugger.handle("setb " .. filePath .. " " .. (line+1))
|
||||
line = editor:MarkerNext(line + 1, BREAKPOINT_MARKER_VALUE)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
debugger.shell = function(expression, isstatement)
|
||||
if debugger.server and not debugger.running then
|
||||
copas.addthread(function ()
|
||||
local addedret = false
|
||||
local value, _, err = debugger.handle('exec ' .. expression)
|
||||
if err and (err:find("'=' expected near '<eof>'") or
|
||||
err:find("unexpected symbol near '")) then
|
||||
value, _, err = debugger.handle('eval ' .. expression:gsub("^%s*=%s*",""))
|
||||
addedret = true
|
||||
-- exec command is not expected to return anything.
|
||||
-- eval command returns 0 or more results.
|
||||
-- 'values' has a list of serialized results returned.
|
||||
-- as it is not possible to distinguish between 0 and nil returned,
|
||||
-- 'nil' is always returned in this case.
|
||||
-- the first value returned by eval command is not used;
|
||||
-- this may need to be taken into account by other debuggers.
|
||||
local addedret, forceexpression = true, expression:match("^%s*=%s*")
|
||||
expression = expression:gsub("^%s*=%s*","")
|
||||
local _, values, err = debugger.evaluate(expression)
|
||||
if not forceexpression and err and
|
||||
(err:find("'<eof>' expected near '") or
|
||||
err:find("'%(' expected near") or
|
||||
err:find("unexpected symbol near '")) then
|
||||
_, values, err = debugger.execute(expression)
|
||||
addedret = false
|
||||
end
|
||||
|
||||
if err then
|
||||
if addedret then err = err:gsub('^%[string "return ', '[string "') end
|
||||
DisplayShellErr(err)
|
||||
elseif addedret or (value ~= nil and value ~= 'nil') then
|
||||
DisplayShell(value)
|
||||
elseif addedret or #values > 0 then
|
||||
-- if empty table is returned, then show nil if this was an expression
|
||||
if #values == 0 and (forceexpression or not isstatement) then
|
||||
values = {'nil'}
|
||||
end
|
||||
DisplayShell((table.unpack or unpack)(values))
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -90,11 +227,17 @@ end
|
||||
|
||||
debugger.listen = function()
|
||||
local server = socket.bind("*", debugger.portnumber)
|
||||
DisplayOutput("Started debugger server; clients can connect to "..wx.wxGetHostName()..":"..debugger.portnumber..".\n")
|
||||
DisplayOutput(("Debugger server started at %s:%d.\n")
|
||||
:format(debugger.hostname, debugger.portnumber))
|
||||
copas.autoclose = false
|
||||
copas.addserver(server, function (skt)
|
||||
SetAllEditorsReadOnly(true)
|
||||
if debugger.server then
|
||||
DisplayOutput("Refused a request to start a new debugging session as there is one in progress already.\n")
|
||||
return
|
||||
end
|
||||
|
||||
local options = debugger.options or {}
|
||||
if not debugger.scratchpad then SetAllEditorsReadOnly(true) end
|
||||
local wxfilepath = GetEditorFileAndCurInfo()
|
||||
local startfile = options.startfile or wxfilepath:GetFullPath()
|
||||
local basedir = options.basedir
|
||||
@@ -105,32 +248,22 @@ debugger.listen = function()
|
||||
debugger.server = copas.wrap(skt)
|
||||
debugger.socket = skt
|
||||
debugger.loop = false
|
||||
debugger.scratchable = false
|
||||
debugger.stats = {line = 0}
|
||||
|
||||
-- load the remote file into the debugger
|
||||
-- set basedir first, before loading to make sure that the path is correct
|
||||
debugger.handle("basedir " .. debugger.basedir)
|
||||
|
||||
-- remove all breakpoints that may still be present from the last session
|
||||
-- this only matters for those remote clients that reload scripts
|
||||
-- without resetting their breakpoints
|
||||
debugger.handle("delallb")
|
||||
|
||||
-- go over all windows and find all breakpoints
|
||||
for id, document in pairs(ide.openDocuments) do
|
||||
local editor = document.editor
|
||||
local filePath = document.filePath
|
||||
line = editor:MarkerNext(0, BREAKPOINT_MARKER_VALUE)
|
||||
while line ~= -1 do
|
||||
debugger.handle("setb " .. filePath .. " " .. (line+1))
|
||||
line = editor:MarkerNext(line + 1, BREAKPOINT_MARKER_VALUE)
|
||||
end
|
||||
end
|
||||
reSetBreakpoints()
|
||||
|
||||
if (options.run) then
|
||||
local file, line = debugger.handle("run")
|
||||
activateDocument(file, line)
|
||||
elseif (debugger.scratchpad) then
|
||||
debugger.scratchpad.updated = true
|
||||
else
|
||||
local file, line = debugger.handle("load " .. startfile)
|
||||
local file, line, err = debugger.loadfile(startfile)
|
||||
-- "load" can work in two ways: (1) it can load the requested file
|
||||
-- OR (2) it can "refuse" to load it if the client was started
|
||||
-- with start() method, which can't load new files
|
||||
@@ -154,25 +287,36 @@ debugger.listen = function()
|
||||
if activated then
|
||||
debugger.basedir = path
|
||||
debugger.handle("basedir " .. debugger.basedir)
|
||||
-- reset breakpoints again as basedir has changed
|
||||
reSetBreakpoints()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not activated then
|
||||
DisplayOutput("Can't find file '" .. file .. "' to activate for debugging; open the file before debugging.\n")
|
||||
DisplayOutput(("Can't find file '%s' to activate for debugging; open the file in the editor before debugging.\n")
|
||||
:format(file))
|
||||
return debugger.terminate()
|
||||
end
|
||||
elseif err then
|
||||
DisplayOutput(("Can't debug the script in the active editor window. Compilation error:\n%s\n")
|
||||
:format(err))
|
||||
return debugger.terminate()
|
||||
else
|
||||
debugger.scratchable = true
|
||||
activateDocument(startfile, 1)
|
||||
end
|
||||
end
|
||||
|
||||
if (not options.noshell) then
|
||||
ShellSupportRemote(debugger.shell, debugger.pid)
|
||||
if (not options.noshell and not debugger.scratchpad) then
|
||||
ShellSupportRemote(debugger.shell)
|
||||
end
|
||||
|
||||
DisplayOutput("Started remote debugging session (base directory: '" .. debugger.basedir .. "').\n")
|
||||
updateStackSync()
|
||||
updateWatchesSync()
|
||||
|
||||
DisplayOutput(("Debugging session started in '%s'.\n")
|
||||
:format(debugger.basedir))
|
||||
end)
|
||||
debugger.listening = true
|
||||
end
|
||||
@@ -189,7 +333,7 @@ debugger.handle = function(command, server)
|
||||
end
|
||||
|
||||
debugger.running = true
|
||||
local file, line, err = mobdebug.handle(command, server and server or debugger.server)
|
||||
local file, line, err = mobdebug.handle(command, server or debugger.server)
|
||||
debugger.running = false
|
||||
|
||||
return file, line, err
|
||||
@@ -197,7 +341,6 @@ end
|
||||
|
||||
debugger.exec = function(command)
|
||||
if debugger.server and not debugger.running then
|
||||
|
||||
copas.addthread(function ()
|
||||
local out
|
||||
while true do
|
||||
@@ -212,10 +355,12 @@ debugger.exec = function(command)
|
||||
file = debugger.basedir .. file
|
||||
end
|
||||
if activateDocument(file, line) then
|
||||
debugger.stats.line = debugger.stats.line + 1
|
||||
if debugger.loop then
|
||||
updateStackSync()
|
||||
updateWatchesSync()
|
||||
else
|
||||
updateWatches()
|
||||
updateStackAndWatches()
|
||||
return
|
||||
end
|
||||
else
|
||||
@@ -227,22 +372,25 @@ debugger.exec = function(command)
|
||||
end
|
||||
end
|
||||
|
||||
debugger.updateBreakpoint = function(command)
|
||||
debugger.handleAsync = function(command)
|
||||
if debugger.server and not debugger.running then
|
||||
copas.addthread(function ()
|
||||
debugger.handle(command)
|
||||
end)
|
||||
copas.addthread(function () debugger.handle(command) end)
|
||||
end
|
||||
end
|
||||
|
||||
debugger.loadfile = function(file)
|
||||
return debugger.handle("load " .. file)
|
||||
end
|
||||
debugger.loadstring = function(file, string)
|
||||
return debugger.handle("loadstring '" .. file .. "' " .. string)
|
||||
end
|
||||
debugger.update = function() copas.step(0) end
|
||||
debugger.terminate = function()
|
||||
if debugger.server then
|
||||
if debugger.pid then -- if there is PID, try local kill
|
||||
DebuggerKillClient()
|
||||
else -- otherwise, trace graceful exit for the remote process
|
||||
debugger.exec("exit")
|
||||
copas.step(1) -- process 'exit' right away; doesn't guarantee the response
|
||||
killClient()
|
||||
else -- otherwise, try graceful exit for the remote process
|
||||
debugger.breaknow("exit")
|
||||
end
|
||||
DebuggerStop()
|
||||
end
|
||||
@@ -256,28 +404,42 @@ debugger.over = function() debugger.exec("over") end
|
||||
debugger.out = function() debugger.exec("out") end
|
||||
debugger.run = function() debugger.exec("run") end
|
||||
debugger.evaluate = function(expression) return debugger.handle('eval ' .. expression) end
|
||||
debugger.breaknow = function()
|
||||
debugger.execute = function(expression) return debugger.handle('exec ' .. expression) end
|
||||
debugger.stack = function() return debugger.handle('stack') end
|
||||
debugger.breaknow = function(command)
|
||||
-- stop if we're running a "trace" command
|
||||
debugger.loop = false
|
||||
|
||||
-- force a step command; don't use copas interface as it checks
|
||||
-- force suspend command; don't use copas interface as it checks
|
||||
-- for the other side "reading" and the other side is not reading anything.
|
||||
-- use the "original" socket to write a "step" command.
|
||||
-- use the "original" socket to send "suspend" command.
|
||||
-- this will only break on the next Lua command.
|
||||
if debugger.socket then
|
||||
local running = debugger.running
|
||||
-- this needs to be short as it will block the UI
|
||||
debugger.socket:settimeout(0.25)
|
||||
local file, line, err = debugger.handle("step", debugger.socket)
|
||||
local file, line, err = debugger.handle(command or "suspend", debugger.socket)
|
||||
debugger.socket:settimeout(0)
|
||||
-- restore running status
|
||||
debugger.running = running
|
||||
-- don't need to do anything else as the earlier call (run, step, etc.)
|
||||
-- will get the results (file, line) back and will update the UI
|
||||
return file, line, err
|
||||
end
|
||||
end
|
||||
debugger.breakpoint = function(file, line, state)
|
||||
debugger.updateBreakpoint((state and "setb " or "delb ") .. file .. " " .. line)
|
||||
debugger.handleAsync((state and "setb " or "delb ") .. file .. " " .. line)
|
||||
end
|
||||
debugger.quickeval = function(var, callback)
|
||||
if debugger.server and not debugger.running then
|
||||
copas.addthread(function ()
|
||||
local _, values, err = debugger.evaluate(var)
|
||||
local val = err
|
||||
and err:gsub("%[.-%]:%d+:%s*","error: ")
|
||||
or (var .. " = " .. (#values > 0 and values[1] or 'nil'))
|
||||
if callback then callback(val) end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------------
|
||||
@@ -289,19 +451,9 @@ function DebuggerAttachDefault(options)
|
||||
debugger.listen()
|
||||
end
|
||||
|
||||
function DebuggerKillClient()
|
||||
if (debugger.pid) then
|
||||
-- using SIGTERM for some reason kills not only the debugee process,
|
||||
-- but also some system processes, which leads to a blue screen crash
|
||||
-- (at least on Windows Vista SP2)
|
||||
local ret = wx.wxProcess.Kill(debugger.pid, wx.wxSIGKILL, wx.wxKILL_CHILDREN)
|
||||
if ret == wx.wxKILL_OK then
|
||||
DisplayOutput("Stopped process (pid: "..debugger.pid..").\n")
|
||||
elseif ret ~= wx.wxKILL_NO_PROCESS then
|
||||
DisplayOutput("Unable to stop process (pid: "..debugger.pid.."), code "..tostring(ret)..".\n")
|
||||
end
|
||||
debugger.pid = nil
|
||||
end
|
||||
function DebuggerShutdown()
|
||||
if debugger.server then debugger.terminate() end
|
||||
if debugger.pid then killClient() end
|
||||
end
|
||||
|
||||
function DebuggerStop()
|
||||
@@ -309,33 +461,108 @@ function DebuggerStop()
|
||||
debugger.server = nil
|
||||
debugger.pid = nil
|
||||
SetAllEditorsReadOnly(false)
|
||||
ShellSupportRemote(nil, 0)
|
||||
ShellSupportRemote(nil)
|
||||
ClearAllCurrentLineMarkers()
|
||||
DisplayOutput("Completed debugging session.\n")
|
||||
DebuggerScratchpadOff()
|
||||
DisplayOutput(("Debugging session completed (traced %d instruction%s).\n")
|
||||
:format(debugger.stats.line, debugger.stats.line == 1 and '' or 's'))
|
||||
end
|
||||
end
|
||||
|
||||
function DebuggerCreateStackWindow()
|
||||
DisplayOutput("Not Yet Implemented\n")
|
||||
end
|
||||
|
||||
function DebuggerCloseStackWindow()
|
||||
|
||||
if (debugger.stackWindow) then
|
||||
SettingsSaveFramePosition(debugger.stackWindow, "StackWindow")
|
||||
debugger.stackCtrl = nil
|
||||
debugger.stackWindow = nil
|
||||
end
|
||||
end
|
||||
|
||||
function DebuggerCloseWatchWindow()
|
||||
if (debugger.watchWindow) then
|
||||
SettingsSaveFramePosition(debugger.watchWindow, "WatchWindow")
|
||||
debugger.watchListCtrl = nil
|
||||
debugger.watchCtrl = nil
|
||||
debugger.watchWindow = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- need imglist to be a file local variable as SetImageList takes ownership
|
||||
-- of it and if done inside a function, icons do not work as expected
|
||||
local imglist = wx.wxImageList(16,16)
|
||||
do
|
||||
local getBitmap = (ide.app.createbitmap or wx.wxArtProvider.GetBitmap)
|
||||
local size = wx.wxSize(16,16)
|
||||
-- 0 = stack call
|
||||
imglist:Add(getBitmap(wx.wxART_GO_FORWARD, wx.wxART_OTHER, size))
|
||||
-- 1 = local variables
|
||||
imglist:Add(getBitmap(wx.wxART_LIST_VIEW, wx.wxART_OTHER, size))
|
||||
-- 2 = upvalues
|
||||
imglist:Add(getBitmap(wx.wxART_REPORT_VIEW, wx.wxART_OTHER, size))
|
||||
end
|
||||
|
||||
function DebuggerCreateStackWindow()
|
||||
if (debugger.stackWindow) then return updateStackAndWatches() end
|
||||
local width = 360
|
||||
local stackWindow = wx.wxFrame(ide.frame, wx.wxID_ANY,
|
||||
"Stack Window",
|
||||
wx.wxDefaultPosition, wx.wxSize(width, 200),
|
||||
wx.wxDEFAULT_FRAME_STYLE + wx.wxFRAME_FLOAT_ON_PARENT)
|
||||
|
||||
debugger.stackWindow = stackWindow
|
||||
|
||||
local stackCtrl = wx.wxTreeCtrl(stackWindow, ID "debug.stack",
|
||||
wx.wxDefaultPosition, wx.wxDefaultSize,
|
||||
wx.wxTR_LINES_AT_ROOT + wx.wxTR_HAS_BUTTONS + wx.wxTR_SINGLE + wx.wxTR_HIDE_ROOT)
|
||||
|
||||
debugger.stackCtrl = stackCtrl
|
||||
|
||||
stackCtrl:SetImageList(imglist)
|
||||
stackWindow:CentreOnParent()
|
||||
SettingsRestoreFramePosition(stackWindow, "StackWindow")
|
||||
stackWindow:Show(true)
|
||||
|
||||
stackWindow:Connect(wx.wxEVT_CLOSE_WINDOW,
|
||||
function (event)
|
||||
DebuggerCloseStackWindow()
|
||||
stackWindow = nil
|
||||
stackCtrl = nil
|
||||
event:Skip()
|
||||
end)
|
||||
|
||||
stackCtrl:Connect( wx.wxEVT_COMMAND_TREE_ITEM_EXPANDING,
|
||||
function (event)
|
||||
local item_id = event:GetItem()
|
||||
local count = stackCtrl:GetChildrenCount(item_id, false)
|
||||
if count > 0 then return true end
|
||||
|
||||
local image = stackCtrl:GetItemImage(item_id)
|
||||
local num = 1
|
||||
for name,value in pairs(stackItemValue[item_id:GetValue()]) do
|
||||
local strval = mobdebug.line(value, {comment = false, nocode = true})
|
||||
local text = type(name) == "number"
|
||||
and (num == name and strval or ("[%s] = %s"):format(name, strval))
|
||||
or ("%s = %s"):format(name, strval)
|
||||
local item = stackCtrl:AppendItem(item_id, text, image)
|
||||
if checkIfExpandable(value, item) then
|
||||
stackCtrl:SetItemHasChildren(item, true)
|
||||
end
|
||||
num = num + 1
|
||||
end
|
||||
|
||||
stackCtrl:SortChildren(item_id)
|
||||
return true
|
||||
end)
|
||||
stackCtrl:Connect( wx.wxEVT_COMMAND_TREE_ITEM_COLLAPSED,
|
||||
function() return true end)
|
||||
|
||||
updateStackAndWatches()
|
||||
end
|
||||
|
||||
function DebuggerCreateWatchWindow()
|
||||
local width = 200
|
||||
if (debugger.watchWindow) then return updateWatches() end
|
||||
local width = 360
|
||||
local watchWindow = wx.wxFrame(ide.frame, wx.wxID_ANY,
|
||||
"Watch Window",
|
||||
wx.wxDefaultPosition, wx.wxSize(width, 160),
|
||||
wx.wxDefaultPosition, wx.wxSize(width, 200),
|
||||
wx.wxDEFAULT_FRAME_STYLE + wx.wxFRAME_FLOAT_ON_PARENT)
|
||||
|
||||
debugger.watchWindow = watchWindow
|
||||
@@ -350,31 +577,31 @@ function DebuggerCreateWatchWindow()
|
||||
watchMenuBar:Append(watchMenu, "&Watches")
|
||||
watchWindow:SetMenuBar(watchMenuBar)
|
||||
|
||||
local watchListCtrl = wx.wxListCtrl(watchWindow, ID_WATCH_LISTCTRL,
|
||||
local watchCtrl = wx.wxListCtrl(watchWindow, ID_WATCH_LISTCTRL,
|
||||
wx.wxDefaultPosition, wx.wxDefaultSize,
|
||||
wx.wxLC_REPORT + wx.wxLC_EDIT_LABELS)
|
||||
|
||||
debugger.watchListCtrl = watchListCtrl
|
||||
debugger.watchCtrl = watchCtrl
|
||||
|
||||
local info = wx.wxListItem()
|
||||
info:SetMask(wx.wxLIST_MASK_TEXT + wx.wxLIST_MASK_WIDTH)
|
||||
info:SetText("Expression")
|
||||
info:SetWidth(width * 0.45)
|
||||
watchListCtrl:InsertColumn(0, info)
|
||||
info:SetWidth(width * 0.32)
|
||||
watchCtrl:InsertColumn(0, info)
|
||||
|
||||
info:SetText("Value")
|
||||
info:SetWidth(width * 0.45)
|
||||
watchListCtrl:InsertColumn(1, info)
|
||||
info:SetWidth(width * 0.56)
|
||||
watchCtrl:InsertColumn(1, info)
|
||||
|
||||
watchWindow:CentreOnParent()
|
||||
SettingsRestoreFramePosition(watchWindow, "WatchWindow")
|
||||
watchWindow:Show(true)
|
||||
|
||||
local function findSelectedWatchItem()
|
||||
local count = watchListCtrl:GetSelectedItemCount()
|
||||
local count = watchCtrl:GetSelectedItemCount()
|
||||
if count > 0 then
|
||||
for idx = 0, watchListCtrl:GetItemCount() - 1 do
|
||||
if watchListCtrl:GetItemState(idx, wx.wxLIST_STATE_FOCUSED) ~= 0 then
|
||||
for idx = 0, watchCtrl:GetItemCount() - 1 do
|
||||
if watchCtrl:GetItemState(idx, wx.wxLIST_STATE_FOCUSED) ~= 0 then
|
||||
return idx
|
||||
end
|
||||
end
|
||||
@@ -386,64 +613,61 @@ function DebuggerCreateWatchWindow()
|
||||
function (event)
|
||||
DebuggerCloseWatchWindow()
|
||||
watchWindow = nil
|
||||
watchListCtrl = nil
|
||||
watchCtrl = nil
|
||||
event:Skip()
|
||||
end)
|
||||
|
||||
watchWindow:Connect(ID_ADDWATCH, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
local row = watchListCtrl:InsertItem(watchListCtrl:GetItemCount(), "Expr")
|
||||
watchListCtrl:SetItem(row, 0, "Expr")
|
||||
watchListCtrl:SetItem(row, 1, "Value")
|
||||
watchListCtrl:EditLabel(row)
|
||||
function ()
|
||||
local row = watchCtrl:InsertItem(watchCtrl:GetItemCount(), "Expr")
|
||||
watchCtrl:SetItem(row, 0, "Expr")
|
||||
watchCtrl:SetItem(row, 1, "Value")
|
||||
watchCtrl:EditLabel(row)
|
||||
end)
|
||||
|
||||
watchWindow:Connect(ID_EDITWATCH, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
function ()
|
||||
local row = findSelectedWatchItem()
|
||||
if row >= 0 then
|
||||
watchListCtrl:EditLabel(row)
|
||||
watchCtrl:EditLabel(row)
|
||||
end
|
||||
end)
|
||||
watchWindow:Connect(ID_EDITWATCH, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
event:Enable(watchListCtrl:GetSelectedItemCount() > 0)
|
||||
event:Enable(watchCtrl:GetSelectedItemCount() > 0)
|
||||
end)
|
||||
|
||||
watchWindow:Connect(ID_REMOVEWATCH, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
function ()
|
||||
local row = findSelectedWatchItem()
|
||||
if row >= 0 then
|
||||
watchListCtrl:DeleteItem(row)
|
||||
watchCtrl:DeleteItem(row)
|
||||
end
|
||||
end)
|
||||
watchWindow:Connect(ID_REMOVEWATCH, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
event:Enable(watchListCtrl:GetSelectedItemCount() > 0)
|
||||
event:Enable(watchCtrl:GetSelectedItemCount() > 0)
|
||||
end)
|
||||
|
||||
watchWindow:Connect(ID_EVALUATEWATCH, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
updateWatches()
|
||||
end)
|
||||
function () updateWatches() end)
|
||||
watchWindow:Connect(ID_EVALUATEWATCH, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
event:Enable(watchListCtrl:GetItemCount() > 0)
|
||||
event:Enable(watchCtrl:GetItemCount() > 0)
|
||||
end)
|
||||
|
||||
watchListCtrl:Connect(wx.wxEVT_COMMAND_LIST_END_LABEL_EDIT,
|
||||
watchCtrl:Connect(wx.wxEVT_COMMAND_LIST_END_LABEL_EDIT,
|
||||
function (event)
|
||||
watchListCtrl:SetItem(event:GetIndex(), 0, event:GetText())
|
||||
updateWatches()
|
||||
if #(event:GetText()) > 0 then
|
||||
watchCtrl:SetItem(event:GetIndex(), 0, event:GetText())
|
||||
updateWatches()
|
||||
end
|
||||
event:Skip()
|
||||
end)
|
||||
end
|
||||
|
||||
function DebuggerMakeFileName(editor, filePath)
|
||||
if not filePath then
|
||||
filePath = "file"..tostring(editor)
|
||||
end
|
||||
return filePath
|
||||
return filePath or editor:GetText()
|
||||
end
|
||||
|
||||
function DebuggerToggleBreakpoint(editor, line)
|
||||
@@ -465,3 +689,218 @@ function DebuggerToggleBreakpoint(editor, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- scratchpad functions
|
||||
|
||||
function DebuggerRefreshScratchpad()
|
||||
if debugger.scratchpad and debugger.scratchpad.updated then
|
||||
if debugger.scratchpad.running then
|
||||
-- break the current execution first
|
||||
-- don't try too frequently to avoid overwhelming the debugger
|
||||
local now = os.clock()
|
||||
if now - debugger.scratchpad.running > 0.250 then
|
||||
debugger.breaknow()
|
||||
debugger.scratchpad.running = now
|
||||
end
|
||||
else
|
||||
local clear = ide.frame.menuBar:IsChecked(ID_CLEAROUTPUT)
|
||||
local scratchpadEditor = debugger.scratchpad.editor
|
||||
-- take editor text and remove shebang line
|
||||
local code = scratchpadEditor:GetText():gsub("^#!.-\n", "\n")
|
||||
local filePath = DebuggerMakeFileName(scratchpadEditor,
|
||||
ide.openDocuments[scratchpadEditor:GetId()].filePath)
|
||||
|
||||
-- this is a special error message that is generated at the very end
|
||||
-- of each script to avoid exiting the (debugee) scratchpad process.
|
||||
-- these errors are handled and not reported to the user
|
||||
local errormsg = 'execution suspended at ' .. os.clock()
|
||||
local stopper = "\ndo error('" .. errormsg .. "') end"
|
||||
-- store if interpreter requires a special handling for external loop
|
||||
local extloop = ide.interpreter.scratchextloop
|
||||
|
||||
local function reloadScratchpadCode()
|
||||
debugger.scratchpad.running = os.clock()
|
||||
debugger.scratchpad.updated = false
|
||||
debugger.scratchpad.runs = (debugger.scratchpad.runs or 0) + 1
|
||||
|
||||
-- the code can be running in two ways under scratchpad:
|
||||
-- 1. controlled by the application, requires stopper (most apps)
|
||||
-- 2. controlled by some external loop (for example, love2d).
|
||||
-- in the first case we need to reload the app after each change
|
||||
-- in the second case, we need to load the app once and then
|
||||
-- "execute" new code to reflect the changes (with some limitations).
|
||||
local _, _, err
|
||||
if extloop then -- if the execution is controlled by an external loop
|
||||
if debugger.scratchpad.runs == 1
|
||||
then _, _, err = debugger.loadstring(filePath, code)
|
||||
else _, _, err = debugger.execute(code) end
|
||||
else _, _, err = debugger.loadstring(filePath, code .. stopper) end
|
||||
|
||||
local prefix = "Compilation error"
|
||||
|
||||
if clear then ClearOutput() end
|
||||
|
||||
if not err then
|
||||
_, _, err = debugger.handle("run")
|
||||
prefix = "Execution error"
|
||||
end
|
||||
if err and not err:find(errormsg) then
|
||||
local line = err:match('.*%[string "[%w:/%\\_%-%.]+"%]:(%d+)%s*:')
|
||||
-- check if the line number in the error matches the line we added
|
||||
-- to stop the script; if so, compile it the usual way and report.
|
||||
-- the usual way is not used all the time because it is slow
|
||||
if prefix == "Compilation error" and
|
||||
tonumber(line) == scratchpadEditor:GetLineCount()+1 then
|
||||
_, err, line = wxlua.CompileLuaScript(code, filePath)
|
||||
err = err:gsub("Lua:.-\n", "") -- remove "syntax error" part
|
||||
end
|
||||
DisplayOutput(prefix .. (line and " on line " .. line or "") .. ":\n"
|
||||
.. err:gsub('stack traceback:.+', ''):gsub('\n+$', '') .. "\n")
|
||||
end
|
||||
debugger.scratchpad.running = false
|
||||
end
|
||||
|
||||
copas.addthread(reloadScratchpadCode)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local numberStyle = wxstc.wxSTC_LUA_NUMBER
|
||||
|
||||
function DebuggerScratchpadOn(editor)
|
||||
debugger.scratchpad = {editor = editor}
|
||||
|
||||
-- check if the debugger is already running; this happens when
|
||||
-- scratchpad is turned on after external script has connected
|
||||
if debugger.server then
|
||||
debugger.scratchpad.updated = true
|
||||
ClearAllCurrentLineMarkers()
|
||||
SetAllEditorsReadOnly(false)
|
||||
ShellSupportRemote(nil) -- disable remote shell
|
||||
DebuggerRefreshScratchpad()
|
||||
elseif not ProjectDebug(true, "scratchpad") then
|
||||
debugger.scratchpad = nil
|
||||
return
|
||||
end
|
||||
|
||||
local scratchpadEditor = editor
|
||||
scratchpadEditor:StyleSetUnderline(numberStyle, true)
|
||||
|
||||
scratchpadEditor:Connect(wxstc.wxEVT_STC_MODIFIED, function(event)
|
||||
local evtype = event:GetModificationType()
|
||||
if (bit.band(evtype,wxstc.wxSTC_MOD_INSERTTEXT) ~= 0 or
|
||||
bit.band(evtype,wxstc.wxSTC_MOD_DELETETEXT) ~= 0 or
|
||||
bit.band(evtype,wxstc.wxSTC_PERFORMED_UNDO) ~= 0 or
|
||||
bit.band(evtype,wxstc.wxSTC_PERFORMED_REDO) ~= 0) then
|
||||
debugger.scratchpad.updated = true
|
||||
end
|
||||
event:Skip()
|
||||
end)
|
||||
|
||||
scratchpadEditor:Connect(wx.wxEVT_LEFT_DOWN, function(event)
|
||||
local scratchpad = debugger.scratchpad
|
||||
|
||||
local point = event:GetPosition()
|
||||
local pos = scratchpadEditor:PositionFromPoint(point)
|
||||
|
||||
-- are we over a number in the scratchpad? if not, it's not our event
|
||||
if ((not scratchpad) or
|
||||
(bit.band(scratchpadEditor:GetStyleAt(pos),31) ~= numberStyle)) then
|
||||
event:Skip()
|
||||
return
|
||||
end
|
||||
|
||||
-- find start position and length of the number
|
||||
local text = scratchpadEditor:GetText()
|
||||
|
||||
local nstart = pos
|
||||
while nstart >= 0
|
||||
and (bit.band(scratchpadEditor:GetStyleAt(nstart),31) == numberStyle)
|
||||
do nstart = nstart - 1 end
|
||||
|
||||
local nend = pos
|
||||
while nend < string.len(text)
|
||||
and (bit.band(scratchpadEditor:GetStyleAt(nend),31) == numberStyle)
|
||||
do nend = nend + 1 end
|
||||
|
||||
-- check if there is minus sign right before the number and include it
|
||||
if nstart >= 0 and scratchpadEditor:GetTextRange(nstart,nstart+1) == '-' then
|
||||
nstart = nstart - 1
|
||||
end
|
||||
scratchpad.start = nstart + 1
|
||||
scratchpad.length = nend - nstart - 1
|
||||
scratchpad.origin = tonumber(scratchpadEditor:GetTextRange(nstart+1,nend))
|
||||
if scratchpad.origin then
|
||||
scratchpad.point = point
|
||||
scratchpadEditor:CaptureMouse()
|
||||
end
|
||||
end)
|
||||
|
||||
scratchpadEditor:Connect(wx.wxEVT_LEFT_UP, function(event)
|
||||
if debugger.scratchpad and debugger.scratchpad.point then
|
||||
debugger.scratchpad.point = nil
|
||||
debugger.scratchpad.editor:ReleaseMouse()
|
||||
wx.wxSetCursor(wx.wxNullCursor) -- restore cursor
|
||||
else event:Skip() end
|
||||
end)
|
||||
|
||||
scratchpadEditor:Connect(wx.wxEVT_MOTION, function(event)
|
||||
local point = event:GetPosition()
|
||||
local pos = scratchpadEditor:PositionFromPoint(point)
|
||||
local scratchpad = debugger.scratchpad
|
||||
local ipoint = scratchpad and scratchpad.point
|
||||
|
||||
-- record the fact that we are over a number or dragging slider
|
||||
scratchpad.over = scratchpad and
|
||||
(ipoint or (bit.band(scratchpadEditor:GetStyleAt(pos),31) == numberStyle))
|
||||
|
||||
if ipoint then
|
||||
-- calculate difference in point position
|
||||
local dx = point.x - ipoint.x
|
||||
local dy = - (point.y - ipoint.y) -- invert dy as y increases down
|
||||
|
||||
-- re-calculate the value
|
||||
local startpos = scratchpad.start
|
||||
local endpos = scratchpad.start+scratchpad.length
|
||||
local num = tonumber(scratchpad.origin) + dx/10
|
||||
|
||||
-- update length
|
||||
scratchpad.length = string.len(num)
|
||||
|
||||
-- update the value in the document
|
||||
scratchpadEditor:SetTargetStart(startpos)
|
||||
scratchpadEditor:SetTargetEnd(endpos)
|
||||
scratchpadEditor:ReplaceTarget("" .. num)
|
||||
else event:Skip() end
|
||||
end)
|
||||
|
||||
scratchpadEditor:Connect(wx.wxEVT_SET_CURSOR, function(event)
|
||||
if (debugger.scratchpad and debugger.scratchpad.over) then
|
||||
event:SetCursor(wx.wxCursor(wx.wxCURSOR_SIZEWE))
|
||||
else event:Skip() end
|
||||
end)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function DebuggerScratchpadOff()
|
||||
if not debugger.scratchpad then return end
|
||||
|
||||
local scratchpadEditor = debugger.scratchpad.editor
|
||||
scratchpadEditor:StyleSetUnderline(numberStyle, false)
|
||||
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wxstc.wxEVT_STC_MODIFIED)
|
||||
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_MOTION)
|
||||
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_LEFT_DOWN)
|
||||
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_LEFT_UP)
|
||||
scratchpadEditor:Disconnect(wx.wxID_ANY, wx.wxID_ANY, wx.wxEVT_SET_CURSOR)
|
||||
|
||||
debugger.scratchpad = nil
|
||||
debugger.terminate()
|
||||
|
||||
-- disable menu if it is still enabled
|
||||
-- (as this may be called when the debugger is being shut down)
|
||||
local menuBar = ide.frame.menuBar
|
||||
if menuBar:IsChecked(ID_RUNNOW) then menuBar:Check(ID_RUNNOW, false) end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
---------------------------------------------------------
|
||||
local wxkeywords = nil -- a string of the keywords for scintilla of wxLua's wx.XXX items
|
||||
|
||||
local in_evt_focus = false -- true when in editor focus event to avoid recursion
|
||||
local editorID = 100 -- window id to create editor pages with, incremented for new editors
|
||||
|
||||
local openDocuments = ide.openDocuments
|
||||
local ignoredFilesList = ide.ignoredFilesList
|
||||
local statusBar = ide.frame.statusBar
|
||||
local notebook = ide.frame.notebook
|
||||
local funclist = ide.frame.toolBar.funclist
|
||||
@@ -17,10 +15,10 @@ local projcombobox = ide.frame.projpanel.projcombobox
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Update the statusbar text of the frame using the given editor.
|
||||
-- Only update if the text has changed.
|
||||
statusTextTable = { "OVR?", "R/O?", "Cursor Pos" }
|
||||
local statusTextTable = { "OVR?", "R/O?", "Cursor Pos" }
|
||||
|
||||
-- set funclist font to be the same as the combobox in the project dropdown
|
||||
funclist:SetFont(projcombobox:GetFont())
|
||||
funclist:SetFont(ide.font.fNormal)
|
||||
|
||||
local function updateStatusText(editor)
|
||||
local texts = { "", "", "" }
|
||||
@@ -62,6 +60,11 @@ local function updateBraceMatch(editor)
|
||||
pos = (match[char] and pos) or (charp and match[charp] and posp)
|
||||
|
||||
if (pos) then
|
||||
-- don't match brackets in markup comments
|
||||
local style = bit.band(editor:GetStyleAt(pos), 31)
|
||||
if MarkupIsSpecial and MarkupIsSpecial(style)
|
||||
or editor.spec.iscomment[style] then return end
|
||||
|
||||
local pos2 = editor:BraceMatch(pos)
|
||||
if (pos2 == wxstc.wxSTC_INVALID_POSITION) then
|
||||
editor:BraceBadLight(pos)
|
||||
@@ -102,7 +105,7 @@ local function isFileAlteredOnDisk(editor)
|
||||
wx.wxMessageBox(fileName.." is no longer on the disk.",
|
||||
GetIDEString("editormessage"),
|
||||
wx.wxOK + wx.wxCENTRE, ide.frame)
|
||||
elseif modTime:IsValid() and oldModTime:IsEarlierThan(modTime) then
|
||||
elseif not editor:GetReadOnly() and modTime:IsValid() and oldModTime:IsEarlierThan(modTime) then
|
||||
local ret = wx.wxMessageBox(fileName.." has been modified on disk.\nDo you want to reload it?",
|
||||
GetIDEString("editormessage"),
|
||||
wx.wxYES_NO + wx.wxCENTRE, ide.frame)
|
||||
@@ -122,7 +125,8 @@ function GetEditor(selection)
|
||||
selection = notebook:GetSelection()
|
||||
end
|
||||
local editor
|
||||
if (selection >= 0) and (selection < notebook:GetPageCount()) and (notebook:GetPage(selection):GetClassInfo():GetClassName()=="wxStyledTextCtrl") then
|
||||
if (selection >= 0) and (selection < notebook:GetPageCount())
|
||||
and (notebook:GetPage(selection):GetClassInfo():GetClassName()=="wxStyledTextCtrl") then
|
||||
editor = notebook:GetPage(selection):DynamicCast("wxStyledTextCtrl")
|
||||
end
|
||||
return editor
|
||||
@@ -143,7 +147,6 @@ function SetEditorSelection(selection)
|
||||
|
||||
editor:SetFocus()
|
||||
editor:SetSTCFocus(true)
|
||||
isFileAlteredOnDisk(editor)
|
||||
local id = editor:GetId()
|
||||
if openDocuments[id] and openDocuments[id].filePath then
|
||||
FileTreeMarkSelected(openDocuments[id].filePath)
|
||||
@@ -157,7 +160,7 @@ function GetEditorFileAndCurInfo(nochecksave)
|
||||
return
|
||||
end
|
||||
|
||||
local id = editor:GetId();
|
||||
local id = editor:GetId()
|
||||
local filepath = openDocuments[id].filePath
|
||||
if (nochecksave and not filepath) then
|
||||
return
|
||||
@@ -203,8 +206,8 @@ function CreateEditor(name)
|
||||
editor:SetBufferedDraw(true)
|
||||
editor:StyleClearAll()
|
||||
|
||||
editor:SetFont(ide.font)
|
||||
editor:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.font)
|
||||
editor:SetFont(ide.font.eNormal)
|
||||
editor:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.font.eNormal)
|
||||
|
||||
editor:SetTabWidth(ide.config.editor.tabwidth or 4)
|
||||
editor:SetIndent(ide.config.editor.tabwidth or 4)
|
||||
@@ -303,7 +306,6 @@ function CreateEditor(name)
|
||||
DynamicWordsRem("pre",editor,nil,editor:LineFromPosition(event:GetPosition()), numlines)
|
||||
end
|
||||
if (bit.band(evtype,wxstc.wxSTC_MOD_BEFOREINSERT) ~= 0) then
|
||||
local pos = event:GetPosition()
|
||||
DynamicWordsRem("pre",editor,nil,editor:LineFromPosition(event:GetPosition()), 0)
|
||||
end
|
||||
end)
|
||||
@@ -319,7 +321,7 @@ function CreateEditor(name)
|
||||
local linestart = editor:PositionFromLine(line)
|
||||
local localpos = pos-linestart
|
||||
|
||||
linetxtopos = linetx:sub(1,localpos)
|
||||
local linetxtopos = linetx:sub(1,localpos)
|
||||
|
||||
if (ch == char_CR and eol==2) or (ch == char_LF and eol==0) then
|
||||
if (line > 0) then
|
||||
@@ -361,24 +363,25 @@ function CreateEditor(name)
|
||||
end)
|
||||
|
||||
editor:Connect(wxstc.wxEVT_STC_SAVEPOINTREACHED,
|
||||
function (event)
|
||||
function ()
|
||||
SetDocumentModified(editor:GetId(), false)
|
||||
end)
|
||||
|
||||
editor:Connect(wxstc.wxEVT_STC_SAVEPOINTLEFT,
|
||||
function (event)
|
||||
function ()
|
||||
SetDocumentModified(editor:GetId(), true)
|
||||
end)
|
||||
|
||||
editor:Connect(wxstc.wxEVT_STC_UPDATEUI,
|
||||
function (event)
|
||||
function ()
|
||||
updateStatusText(editor)
|
||||
updateBraceMatch(editor)
|
||||
for e,iv in ipairs(editor.ev) do
|
||||
for _,iv in ipairs(editor.ev) do
|
||||
local line = editor:LineFromPosition(iv[1])
|
||||
IndicateFunctions(editor,line,line+iv[2])
|
||||
if MarkupStyle then MarkupStyle(editor,line,line+iv[2]) end
|
||||
if MarkupStyle then MarkupStyle(editor,line,line+iv[2]+1) end
|
||||
end
|
||||
if MarkupStyleRefresh then MarkupStyleRefresh(editor, editor.ev) end
|
||||
editor.ev = {}
|
||||
end)
|
||||
|
||||
@@ -393,13 +396,35 @@ function CreateEditor(name)
|
||||
event:Skip()
|
||||
end)
|
||||
|
||||
local inhandler = false
|
||||
editor:Connect(wx.wxEVT_SET_FOCUS,
|
||||
function (event)
|
||||
event:Skip()
|
||||
if ide.in_evt_focus or exitingProgram then return end
|
||||
ide.in_evt_focus = true
|
||||
if inhandler or ide.exitingProgram then return end
|
||||
inhandler = true
|
||||
isFileAlteredOnDisk(editor)
|
||||
ide.in_evt_focus = false
|
||||
inhandler = false
|
||||
end)
|
||||
|
||||
editor:Connect(wx.wxEVT_KEY_DOWN,
|
||||
function (event)
|
||||
local keycode = event:GetKeyCode()
|
||||
local first, last = 0, notebook:GetPageCount()-1
|
||||
if keycode == wx.WXK_ESCAPE and frame:IsFullScreen() then
|
||||
ShowFullScreen(false)
|
||||
elseif event:ControlDown() and
|
||||
(keycode == wx.WXK_PAGEUP or keycode == wx.WXK_TAB and event:ShiftDown()) then
|
||||
if notebook:GetSelection() == first
|
||||
then notebook:SetSelection(last)
|
||||
else notebook:AdvanceSelection(false) end
|
||||
elseif event:ControlDown() and
|
||||
(keycode == wx.WXK_PAGEDOWN or keycode == wx.WXK_TAB) then
|
||||
if notebook:GetSelection() == last
|
||||
then notebook:SetSelection(first)
|
||||
else notebook:AdvanceSelection(true) end
|
||||
else
|
||||
event:Skip()
|
||||
end
|
||||
end)
|
||||
|
||||
if notebook:AddPage(editor, name, true) then
|
||||
@@ -423,10 +448,10 @@ function GetSpec(ext,forcespec)
|
||||
-- search proper spec
|
||||
-- allow forcespec for "override"
|
||||
if ext and not spec then
|
||||
for i,curspec in pairs(ide.specs) do
|
||||
exts = curspec.exts
|
||||
for _,curspec in pairs(ide.specs) do
|
||||
local exts = curspec.exts
|
||||
if (exts) then
|
||||
for n,curext in ipairs(exts) do
|
||||
for _,curext in ipairs(exts) do
|
||||
if (curext == ext) then
|
||||
spec = curspec
|
||||
break
|
||||
@@ -450,16 +475,11 @@ function IndicateFunctions(editor, lines, linee)
|
||||
|
||||
if (lines < 0) then return end
|
||||
|
||||
local isfunc = editor.spec.isfncall
|
||||
local iscomment = editor.spec.iscomment
|
||||
local iskeyword0 = editor.spec.iskeyword0
|
||||
local isfncall = editor.spec.isfncall
|
||||
local isinvalid = {}
|
||||
for i,v in pairs(iscomment) do
|
||||
isinvalid[i] = v
|
||||
end
|
||||
for i,v in pairs(iskeyword0) do
|
||||
isinvalid[i] = v
|
||||
end
|
||||
for i,v in pairs(editor.spec.iscomment) do isinvalid[i] = v end
|
||||
for i,v in pairs(editor.spec.iskeyword0) do isinvalid[i] = v end
|
||||
for i,v in pairs(editor.spec.isstring) do isinvalid[i] = v end
|
||||
|
||||
local INDICS_MASK = wxstc.wxSTC_INDICS_MASK
|
||||
local INDIC0_MASK = wxstc.wxSTC_INDIC0_MASK
|
||||
@@ -476,13 +496,13 @@ function IndicateFunctions(editor, lines, linee)
|
||||
while from do
|
||||
tx = from==1 and tx or string.sub(tx,from)
|
||||
|
||||
local f,t,w = isfunc(tx)
|
||||
local f,t,w = isfncall(tx)
|
||||
|
||||
if (f) then
|
||||
local p = ls+f+off
|
||||
local s = bit.band(editor:GetStyleAt(p),31)
|
||||
editor:StartStyling(p,INDICS_MASK)
|
||||
editor:SetStyling(t-f,isinvalid[s] and 0 or (INDIC0_MASK + 1))
|
||||
editor:SetStyling(#w,isinvalid[s] and 0 or (INDIC0_MASK + 1))
|
||||
off = off + t
|
||||
end
|
||||
from = t and (t+1)
|
||||
@@ -509,11 +529,11 @@ function SetupKeywords(editor, ext, forcespec, styles, font, fontitalic)
|
||||
-- Get the items in the global "wx" table for autocompletion
|
||||
if not wxkeywords then
|
||||
local keyword_table = {}
|
||||
for index, value in pairs(wx) do
|
||||
for index in pairs(wx) do
|
||||
table.insert(keyword_table, "wx."..index.." ")
|
||||
end
|
||||
|
||||
for index, value in pairs(wxstc) do
|
||||
for index in pairs(wxstc) do
|
||||
table.insert(keyword_table, "wxstc."..index.." ")
|
||||
end
|
||||
|
||||
@@ -535,7 +555,7 @@ function SetupKeywords(editor, ext, forcespec, styles, font, fontitalic)
|
||||
end
|
||||
|
||||
StylesApplyToEditor(styles or ide.config.styles, editor,
|
||||
font or ide.font,fontitalic or ide.fontItalic,lexerstyleconvert)
|
||||
font or ide.font.eNormal,fontitalic or ide.font.eItalic,lexerstyleconvert)
|
||||
end
|
||||
|
||||
----------------------------------------------------
|
||||
@@ -563,9 +583,13 @@ funclist:Connect(wx.wxEVT_SET_FOCUS,
|
||||
local linee = editor:GetLineCount()-1
|
||||
for line=lines,linee do
|
||||
local tx = editor:GetLine(line)
|
||||
local s,e,cap,l = editor.spec.isfndef(tx)
|
||||
local s,_,cap,l = editor.spec.isfndef(tx)
|
||||
if (s) then
|
||||
funclist:Append((l and " " or "")..cap,line)
|
||||
local ls = editor:PositionFromLine(line)
|
||||
local style = bit.band(editor:GetStyleAt(ls+s),31)
|
||||
if not (editor.spec.iscomment[style] or editor.spec.isstring[style]) then
|
||||
funclist:Append((l and " " or "")..cap,line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -23,25 +23,22 @@ ide.filetree = {
|
||||
root_id = nil,
|
||||
rootdir = "",
|
||||
},
|
||||
imglist,
|
||||
|
||||
newfiledir,
|
||||
}
|
||||
local filetree = ide.filetree
|
||||
local frame = ide.frame
|
||||
|
||||
-- generic tree
|
||||
-- ------------
|
||||
|
||||
do
|
||||
local getBitmap = (ide.app.createbitmap or wx.wxArtProvider.GetBitmap)
|
||||
local size = wx.wxSize(16, 16)
|
||||
filetree.imglist = wx.wxImageList(16,16)
|
||||
-- 0 = directory
|
||||
filetree.imglist:Add(wx.wxArtProvider.GetBitmap(wx.wxART_FOLDER, wx.wxART_OTHER, size))
|
||||
filetree.imglist:Add(getBitmap(wx.wxART_FOLDER, wx.wxART_OTHER, size))
|
||||
-- 1 = file known spec
|
||||
filetree.imglist:Add(wx.wxArtProvider.GetBitmap(wx.wxART_HELP_PAGE, wx.wxART_OTHER, size))
|
||||
filetree.imglist:Add(getBitmap(wx.wxART_HELP_PAGE, wx.wxART_OTHER, size))
|
||||
-- 2 = file rest
|
||||
filetree.imglist:Add(wx.wxArtProvider.GetBitmap(wx.wxART_NORMAL_FILE, wx.wxART_OTHER, size))
|
||||
filetree.imglist:Add(getBitmap(wx.wxART_NORMAL_FILE, wx.wxART_OTHER, size))
|
||||
end
|
||||
|
||||
local function treeAddDir(tree,parent_id,rootdir)
|
||||
@@ -54,11 +51,10 @@ local function treeAddDir(tree,parent_id,rootdir)
|
||||
end
|
||||
|
||||
local curr
|
||||
local search = rootdir..string_Pathsep.."*.*"
|
||||
local dirs = FileSysGet(search,wx.wxDIR)
|
||||
local search = rootdir..string_Pathsep.."*"
|
||||
|
||||
-- append directories
|
||||
for i,dir in ipairs(dirs) do
|
||||
for _,dir in ipairs(FileSysGet(search,wx.wxDIR)) do
|
||||
local name = dir:match("%"..string_Pathsep.."("..stringset_File.."+)$")
|
||||
local icon = 0
|
||||
local item = items[name .. icon]
|
||||
@@ -82,10 +78,9 @@ local function treeAddDir(tree,parent_id,rootdir)
|
||||
end
|
||||
|
||||
-- then append files
|
||||
local files = FileSysGet(search,wx.wxFILE)
|
||||
for i,file in ipairs(files) do
|
||||
for _,file in ipairs(FileSysGet(search,wx.wxFILE)) do
|
||||
local name = file:match("%"..string_Pathsep.."("..stringset_File.."+)$")
|
||||
local known = GetSpec(GetFileExt(fname))
|
||||
local known = GetSpec(GetFileExt(name))
|
||||
local icon = known and 1 or 2
|
||||
local item = items[name .. icon]
|
||||
if item then -- existing item
|
||||
@@ -131,10 +126,8 @@ end
|
||||
|
||||
local function treeSetRoot(tree,treedata,rootdir)
|
||||
tree:DeleteAllItems()
|
||||
|
||||
if (not wx.wxDirExists(rootdir)) then
|
||||
treedata.root_id = nil
|
||||
tree:AddRoot("Invalid")
|
||||
return
|
||||
end
|
||||
|
||||
@@ -160,10 +153,7 @@ local function treeSetConnectorsAndIcons(tree,treedata)
|
||||
return true
|
||||
end )
|
||||
tree:Connect( wx.wxEVT_COMMAND_TREE_ITEM_COLLAPSED,
|
||||
function( event )
|
||||
-- don't need to do anything here
|
||||
return true
|
||||
end )
|
||||
function() return true end )
|
||||
tree:Connect( wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED,
|
||||
function( event )
|
||||
local item_id = event:GetItem()
|
||||
@@ -185,21 +175,6 @@ local function treeSetConnectorsAndIcons(tree,treedata)
|
||||
end
|
||||
end
|
||||
end )
|
||||
tree:Connect( wx.wxEVT_COMMAND_TREE_SEL_CHANGED,
|
||||
function( event )
|
||||
local item_id = event:GetItem()
|
||||
|
||||
-- set "newfile-path"
|
||||
local isfile = tree:GetItemImage(item_id) ~= 0
|
||||
filetree.newfiledir = treeGetItemFullName(tree,treedata,item_id)
|
||||
|
||||
if (isfile) then
|
||||
-- remove file
|
||||
filetree.newfiledir = wx.wxFileName(filetree.newfiledir):GetPath(wx.wxPATH_GET_VOLUME)
|
||||
end
|
||||
|
||||
filetree.newfiledir = filetree.newfiledir..string_Pathsep
|
||||
end )
|
||||
end
|
||||
|
||||
-- project
|
||||
@@ -221,7 +196,8 @@ local projtree = wx.wxTreeCtrl(projpanel, ID "filetree.projtree",
|
||||
or (wx.wxTR_HAS_BUTTONS + wx.wxTR_SINGLE + wx.wxTR_HIDE_ROOT))
|
||||
|
||||
-- use the same font in the combobox as is used in the filetree
|
||||
projcombobox:SetFont(projtree:GetFont())
|
||||
projtree:SetFont(ide.font.fNormal)
|
||||
projcombobox:SetFont(ide.font.fNormal)
|
||||
|
||||
local projTopSizer = wx.wxBoxSizer( wx.wxHORIZONTAL );
|
||||
projTopSizer:Add(projcombobox, 1, wx.wxALL + wx.wxALIGN_LEFT + wx.wxGROW, 0)
|
||||
@@ -330,7 +306,7 @@ end
|
||||
|
||||
local curr_id
|
||||
function FileTreeMarkSelected(file)
|
||||
if not file then return end
|
||||
if not file or not filetree.projdirText or #filetree.projdirText == 0 then return end
|
||||
local item_id = findItem(projtree, file)
|
||||
if curr_id ~= item_id then
|
||||
if curr_id and projtree:IsBold(curr_id) then
|
||||
@@ -343,3 +319,7 @@ function FileTreeMarkSelected(file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function FileTreeRefresh()
|
||||
treeSetRoot(projtree,filetree.projdata,filetree.projdirText)
|
||||
end
|
||||
|
||||
@@ -219,9 +219,8 @@ end
|
||||
|
||||
local function ProcInFiles(startdir,mask,subdirs,replace)
|
||||
if (subdirs) then
|
||||
local dirs = FileSysGet(startdir..string_Pathsep.."*.*",wx.wxDIR)
|
||||
local dirs = FileSysGet(startdir..string_Pathsep.."*",wx.wxDIR)
|
||||
for i,dir in ipairs(dirs) do
|
||||
--DisplayOutput(dir.."\n")
|
||||
ProcInFiles(dir,mask,true,replace)
|
||||
end
|
||||
end
|
||||
@@ -230,8 +229,6 @@ local function ProcInFiles(startdir,mask,subdirs,replace)
|
||||
for i,file in ipairs(files) do
|
||||
findReplace.curfilename = file
|
||||
|
||||
--DisplayOutput(file.."\n")
|
||||
|
||||
-- load file
|
||||
local handle = io.open(file, "rb")
|
||||
if handle then
|
||||
|
||||
@@ -10,33 +10,23 @@ BREAKPOINT_MARKER_VALUE = 2 -- = 2^BREAKPOINT_MARKER
|
||||
CURRENT_LINE_MARKER = 2
|
||||
CURRENT_LINE_MARKER_VALUE = 4 -- = 2^CURRENT_LINE_MARKER
|
||||
|
||||
-- Globals
|
||||
local font = nil -- fonts to use for the editor
|
||||
local fontItalic = nil
|
||||
local ofont = nil -- fonts to use for the outputshell
|
||||
local ofontItalic = nil
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Pick some reasonable fixed width fonts to use for the editor
|
||||
if wx.__WXMSW__ then
|
||||
font = wx.wxFont(ide.config.editor.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_NORMAL, wx.wxFONTWEIGHT_NORMAL, false, ide.config.editor.fontname or "Courier New")
|
||||
fontItalic = wx.wxFont(ide.config.editor.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_ITALIC, wx.wxFONTWEIGHT_NORMAL, false, ide.config.editor.fontname or "Courier New")
|
||||
else
|
||||
font = wx.wxFont(ide.config.editor.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_NORMAL, wx.wxFONTWEIGHT_NORMAL, false, ide.config.editor.fontname or "")
|
||||
fontItalic = wx.wxFont(ide.config.editor.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_ITALIC, wx.wxFONTWEIGHT_NORMAL, false, ide.config.editor.fontname or "")
|
||||
local function setFont(style, config, name, size)
|
||||
return wx.wxFont(config.fontsize or size or 10, wx.wxFONTFAMILY_MODERN, style,
|
||||
wx.wxFONTWEIGHT_NORMAL, false, config.fontname or name,
|
||||
config.fontencoding or wx.wxFONTENCODING_DEFAULT)
|
||||
end
|
||||
ide.font.eNormal = setFont(wx.wxFONTSTYLE_NORMAL, ide.config.editor, wx.__WXMSW__ and "Courier New" or "")
|
||||
ide.font.eItalic = setFont(wx.wxFONTSTYLE_ITALIC, ide.config.editor, wx.__WXMSW__ and "Courier New" or "")
|
||||
|
||||
if wx.__WXMSW__ then
|
||||
ofont = wx.wxFont(ide.config.outputshell.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_NORMAL, wx.wxFONTWEIGHT_NORMAL, false, ide.config.outputshell.fontname or "Courier New")
|
||||
ofontItalic = wx.wxFont(ide.config.outputshell.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_ITALIC, wx.wxFONTWEIGHT_NORMAL, false, ide.config.outputshell.fontname or "Courier New")
|
||||
else
|
||||
ofont = wx.wxFont(ide.config.outputshell.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_NORMAL, wx.wxFONTWEIGHT_NORMAL, false, ide.config.outputshell.fontname or "")
|
||||
ofontItalic = wx.wxFont(ide.config.outputshell.fontsize or 10, wx.wxFONTFAMILY_MODERN, wx.wxFONTSTYLE_ITALIC, wx.wxFONTWEIGHT_NORMAL, false, ide.config.outputshell.fontname or "")
|
||||
end
|
||||
ide.font.oNormal = setFont(wx.wxFONTSTYLE_NORMAL, ide.config.outputshell, wx.__WXMSW__ and "Courier New" or "")
|
||||
ide.font.oItalic = setFont(wx.wxFONTSTYLE_ITALIC, ide.config.outputshell, wx.__WXMSW__ and "Courier New" or "")
|
||||
|
||||
ide.font = font
|
||||
ide.fontItalic = fontItalic
|
||||
ide.ofont = ofont
|
||||
ide.ofontItalic = ofontItalic
|
||||
-- treeCtrl font requires slightly different handling
|
||||
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
|
||||
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Create the wxFrame
|
||||
@@ -57,7 +47,6 @@ local function createFrame()
|
||||
end)
|
||||
|
||||
local menuBar = wx.wxMenuBar()
|
||||
frame:SetMenuBar(menuBar)
|
||||
frame.menuBar = menuBar
|
||||
|
||||
local statusBar = frame:CreateStatusBar( 5 )
|
||||
@@ -79,23 +68,24 @@ local function createToolBar(frame)
|
||||
local funclist = wx.wxChoice.new(toolBar,ID "toolBar.funclist",wx.wxDefaultPosition, wx.wxSize.new(240,16))
|
||||
|
||||
-- note: Ususally the bmp size isn't necessary, but the HELP icon is not the right size in MSW
|
||||
local getBitmap = (ide.app.createbitmap or wx.wxArtProvider.GetBitmap)
|
||||
local toolBmpSize = toolBar:GetToolBitmapSize()
|
||||
toolBar:AddTool(ID_NEW, "New", wx.wxArtProvider.GetBitmap(wx.wxART_NORMAL_FILE, wx.wxART_TOOLBAR, toolBmpSize), "Create an empty document")
|
||||
toolBar:AddTool(ID_OPEN, "Open", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_OPEN, wx.wxART_TOOLBAR, toolBmpSize), "Open an existing document")
|
||||
toolBar:AddTool(ID_SAVE, "Save", wx.wxArtProvider.GetBitmap(wx.wxART_FILE_SAVE, wx.wxART_TOOLBAR, toolBmpSize), "Save the current document")
|
||||
toolBar:AddTool(ID_SAVEALL, "Save All", wx.wxArtProvider.GetBitmap(wx.wxART_NEW_DIR, wx.wxART_TOOLBAR, toolBmpSize), "Save all documents")
|
||||
toolBar:AddTool(ID_NEW, "New", getBitmap(wx.wxART_NORMAL_FILE, wx.wxART_TOOLBAR, toolBmpSize), "Create an empty document")
|
||||
toolBar:AddTool(ID_OPEN, "Open", getBitmap(wx.wxART_FILE_OPEN, wx.wxART_TOOLBAR, toolBmpSize), "Open an existing document")
|
||||
toolBar:AddTool(ID_SAVE, "Save", getBitmap(wx.wxART_FILE_SAVE, wx.wxART_TOOLBAR, toolBmpSize), "Save the current document")
|
||||
toolBar:AddTool(ID_SAVEALL, "Save All", getBitmap(wx.wxART_NEW_DIR, wx.wxART_TOOLBAR, toolBmpSize), "Save all documents")
|
||||
toolBar:AddSeparator()
|
||||
toolBar:AddTool(ID_CUT, "Cut", wx.wxArtProvider.GetBitmap(wx.wxART_CUT, wx.wxART_TOOLBAR, toolBmpSize), "Cut the selection")
|
||||
toolBar:AddTool(ID_COPY, "Copy", wx.wxArtProvider.GetBitmap(wx.wxART_COPY, wx.wxART_TOOLBAR, toolBmpSize), "Copy the selection")
|
||||
toolBar:AddTool(ID_PASTE, "Paste", wx.wxArtProvider.GetBitmap(wx.wxART_PASTE, wx.wxART_TOOLBAR, toolBmpSize), "Paste text from the clipboard")
|
||||
toolBar:AddTool(ID_CUT, "Cut", getBitmap(wx.wxART_CUT, wx.wxART_TOOLBAR, toolBmpSize), "Cut the selection")
|
||||
toolBar:AddTool(ID_COPY, "Copy", getBitmap(wx.wxART_COPY, wx.wxART_TOOLBAR, toolBmpSize), "Copy the selection")
|
||||
toolBar:AddTool(ID_PASTE, "Paste", getBitmap(wx.wxART_PASTE, wx.wxART_TOOLBAR, toolBmpSize), "Paste text from the clipboard")
|
||||
toolBar:AddSeparator()
|
||||
toolBar:AddTool(ID_UNDO, "Undo", wx.wxArtProvider.GetBitmap(wx.wxART_UNDO, wx.wxART_TOOLBAR, toolBmpSize), "Undo last edit")
|
||||
toolBar:AddTool(ID_REDO, "Redo", wx.wxArtProvider.GetBitmap(wx.wxART_REDO, wx.wxART_TOOLBAR, toolBmpSize), "Redo last undo")
|
||||
toolBar:AddTool(ID_UNDO, "Undo", getBitmap(wx.wxART_UNDO, wx.wxART_TOOLBAR, toolBmpSize), "Undo last edit")
|
||||
toolBar:AddTool(ID_REDO, "Redo", getBitmap(wx.wxART_REDO, wx.wxART_TOOLBAR, toolBmpSize), "Redo last undo")
|
||||
toolBar:AddSeparator()
|
||||
toolBar:AddTool(ID_FIND, "Find", wx.wxArtProvider.GetBitmap(wx.wxART_FIND, wx.wxART_TOOLBAR, toolBmpSize), "Find text")
|
||||
toolBar:AddTool(ID_REPLACE, "Replace", wx.wxArtProvider.GetBitmap(wx.wxART_FIND_AND_REPLACE, wx.wxART_TOOLBAR, toolBmpSize), "Find and replace text")
|
||||
toolBar:AddTool(ID_FIND, "Find", getBitmap(wx.wxART_FIND, wx.wxART_TOOLBAR, toolBmpSize), "Find text")
|
||||
toolBar:AddTool(ID_REPLACE, "Replace", getBitmap(wx.wxART_FIND_AND_REPLACE, wx.wxART_TOOLBAR, toolBmpSize), "Find and replace text")
|
||||
toolBar:AddSeparator()
|
||||
toolBar:AddTool(ID "debug.projectdir.fromfile", "Update", wx.wxArtProvider.GetBitmap(wx.wxART_GO_DIR_UP , wx.wxART_TOOLBAR, toolBmpSize), "Sets projectdir from file")
|
||||
toolBar:AddTool(ID "debug.projectdir.fromfile", "Update", getBitmap(wx.wxART_GO_DIR_UP , wx.wxART_TOOLBAR, toolBmpSize), "Sets projectdir from file")
|
||||
toolBar:AddSeparator()
|
||||
toolBar:AddControl(funclist)
|
||||
toolBar:Realize()
|
||||
@@ -113,26 +103,27 @@ local function createNotebook(frame)
|
||||
wxaui.wxAUI_NB_DEFAULT_STYLE + wxaui.wxAUI_NB_TAB_EXTERNAL_MOVE
|
||||
+ wx.wxNO_BORDER)
|
||||
|
||||
-- the following group of event handlers allows the active editor
|
||||
-- to get/keep focus after execution of Run and other commands
|
||||
local current -- the currently active editor, needed by the focus selection
|
||||
local function onPageChange(event)
|
||||
current = event:GetSelection() -- update the active editor reference
|
||||
SetEditorSelection(current)
|
||||
event:Skip() -- skip to let page change
|
||||
end
|
||||
|
||||
notebook:Connect(wx.wxEVT_SET_FOCUS, -- Notepad tabs shouldn't be selectable,
|
||||
function (event)
|
||||
SetEditorSelection(current) -- select the currently active editor
|
||||
end)
|
||||
notebook:Connect(wx.wxEVT_COMMAND_NOTEBOOK_PAGE_CHANGED, onPageChange)
|
||||
notebook:Connect(wxaui.wxEVT_COMMAND_AUINOTEBOOK_PAGE_CHANGED, onPageChange)
|
||||
|
||||
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(wx.wxEVT_SET_FOCUS, -- Notepad tabs shouldn't be selectable,
|
||||
function (event)
|
||||
SetEditorSelection(current) -- select the currently active editor
|
||||
end)
|
||||
|
||||
frame.notebook = notebook
|
||||
return notebook
|
||||
end
|
||||
|
||||
@@ -37,14 +37,13 @@ ID_FIND_IN_FILES = NewID()
|
||||
ID_REPLACE_IN_FILES = NewID()
|
||||
ID_GOTOLINE = NewID()
|
||||
ID_SORT = NewID()
|
||||
-- Debug menu
|
||||
-- Project/Debug menu
|
||||
ID_TOGGLEBREAKPOINT = NewID()
|
||||
ID_COMPILE = NewID()
|
||||
ID_RUN = NewID()
|
||||
ID_RUNNOW = NewID()
|
||||
ID_ATTACH_DEBUG = NewID()
|
||||
ID_START_DEBUG = NewID()
|
||||
--ID_USECONSOLE = NewID()
|
||||
|
||||
ID_STOP_DEBUG = NewID()
|
||||
ID_STEP = NewID()
|
||||
ID_STEP_OVER = NewID()
|
||||
@@ -52,10 +51,12 @@ ID_STEP_OUT = NewID()
|
||||
ID_CONTINUE = NewID()
|
||||
ID_BREAK = NewID()
|
||||
ID_TRACE = NewID()
|
||||
--ID_VIEWCALLSTACK = NewID()
|
||||
--ID_VIEWWATCHWINDOW = NewID()
|
||||
ID_VIEWCALLSTACK = NewID()
|
||||
ID_VIEWWATCHWINDOW = NewID()
|
||||
ID_FULLSCREEN = NewID()
|
||||
ID_CLEAROUTPUT = NewID()
|
||||
ID_DEBUGGER_PORT = NewID()
|
||||
ID_PROJECTDIR = NewID()
|
||||
ID_INTERPRETER = NewID()
|
||||
-- Help menu
|
||||
ID_ABOUT = wx.wxID_ABOUT
|
||||
-- Watch window menu items
|
||||
@@ -65,8 +66,6 @@ ID_EDITWATCH = NewID()
|
||||
ID_REMOVEWATCH = NewID()
|
||||
ID_EVALUATEWATCH = NewID()
|
||||
|
||||
ID_WORKDIR_CHOSE = NewID()
|
||||
|
||||
local ids = {}
|
||||
function ID (name)
|
||||
ids[name] = ids[name] or NewID()
|
||||
|
||||
180
src/editor/inspect.lua
Normal file
180
src/editor/inspect.lua
Normal file
@@ -0,0 +1,180 @@
|
||||
-- Integration with LuaInspect
|
||||
-- (C) 2012 Paul Kulchenko
|
||||
|
||||
local M, LA, LI, T = {}
|
||||
local FAST = true
|
||||
|
||||
local function init()
|
||||
if LA then return end
|
||||
|
||||
require "metalua"
|
||||
LA = require "luainspect.ast"
|
||||
LI = require "luainspect.init"
|
||||
T = require "luainspect.types"
|
||||
|
||||
if FAST then
|
||||
LI.eval_comments = function () end
|
||||
LI.infer_values = function () end
|
||||
end
|
||||
end
|
||||
|
||||
function M.warnings_from_string(src, file)
|
||||
init()
|
||||
|
||||
local ast, err, linenum, colnum = LA.ast_from_string(src, file)
|
||||
if err then return nil, err, linenum, colnum end
|
||||
|
||||
if FAST then
|
||||
LI.inspect(ast, nil, src)
|
||||
LA.ensure_parents_marked(ast)
|
||||
else
|
||||
local tokenlist = LA.ast_to_tokenlist(ast, src)
|
||||
LI.inspect(ast, tokenlist, src)
|
||||
LI.mark_related_keywords(ast, tokenlist, src)
|
||||
end
|
||||
|
||||
return M.show_warnings(ast)
|
||||
end
|
||||
|
||||
function M.show_warnings(top_ast)
|
||||
local warnings = {}
|
||||
local function warn(msg, linenum, path)
|
||||
warnings[#warnings+1] = (path or "?") .. "(" .. (linenum or 0) .. "): " .. msg
|
||||
end
|
||||
local function known(o) return not T.istype[o] end
|
||||
local isseen, globseen = {}, {}
|
||||
LA.walk(top_ast, function(ast)
|
||||
local line = ast.lineinfo and ast.lineinfo.first[1] or 0
|
||||
local path = ast.lineinfo and ast.lineinfo.first[4] or '?'
|
||||
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.first[1]
|
||||
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'
|
||||
local fname = assignment and type(func[1][1][1]) == 'string' and func[1][1][1]
|
||||
-- "function foo(bar)" => func.tag == 'Set'
|
||||
-- "local function foo(bar)" => func.tag == 'Localrec'
|
||||
-- "local _, foo = 1, function(bar)" => func.tag == 'Local'
|
||||
-- "print(function(bar) end)" => func.tag == nil
|
||||
warn("unused parameter '" .. name .. "'" ..
|
||||
(func and assignment
|
||||
and (fname and func.tag
|
||||
and (" in function '" .. fname .. "'")
|
||||
or " in anonymous function")
|
||||
or ""),
|
||||
line, path)
|
||||
end
|
||||
else
|
||||
if 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 as ast.seevalue relies on value evaluation,
|
||||
-- which is very slow even on simple and short scripts
|
||||
if not FAST and ast.isfield and not(known(ast.seevalue.value) and ast.seevalue.value ~= nil) then
|
||||
warn("unknown field " .. name, ast.lineinfo.first[1], path)
|
||||
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] then
|
||||
isseen[vast.parent] = true
|
||||
warn("function '" .. name .. "': " .. note, line, path)
|
||||
end
|
||||
end)
|
||||
return warnings
|
||||
end
|
||||
|
||||
local frame = ide.frame
|
||||
local menu = frame.menuBar:GetMenu(frame.menuBar:FindMenu("&Project"))
|
||||
local ID_ANALYZE = ID "debug.analyze"
|
||||
|
||||
-- insert after "Compile" item
|
||||
for item = 0, menu:GetMenuItemCount()-1 do
|
||||
if menu:FindItemByPosition(item):GetId() == ID_COMPILE then
|
||||
menu:Insert(item+1, ID_ANALYZE, "Analyze\tShift-F7", "Analyze the source code")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local debugger = ide.debugger
|
||||
local openDocuments = ide.openDocuments
|
||||
|
||||
local function analyzeProgram(editor)
|
||||
local editorText = editor:GetText()
|
||||
local id = editor:GetId()
|
||||
local filePath = DebuggerMakeFileName(editor, openDocuments[id].filePath)
|
||||
|
||||
if frame.menuBar:IsChecked(ID_CLEAROUTPUT) then ClearOutput() end
|
||||
DisplayOutput("Analizing the source code")
|
||||
frame:Update()
|
||||
|
||||
local warn, err = M.warnings_from_string(editorText, filePath)
|
||||
if err then -- report compilation error
|
||||
DisplayOutput(": not completed\n")
|
||||
return false
|
||||
end
|
||||
|
||||
DisplayOutput((": %s warning%s.\n")
|
||||
:format(#warn > 0 and #warn or 'no', #warn == 1 and '' or 's'))
|
||||
DisplayOutputNoMarker(table.concat(warn, "\n") .. "\n")
|
||||
|
||||
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) end
|
||||
end)
|
||||
frame:Connect(ID_ANALYZE, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
local editor = GetEditor()
|
||||
event:Enable((debugger.server == nil and debugger.pid == nil) and (editor ~= nil))
|
||||
end)
|
||||
@@ -39,7 +39,7 @@ ide.iofilters["GermanUtf8Ascii"] = {
|
||||
}
|
||||
local lst = "["
|
||||
for k in pairs(charconv) do lst = lst .. k end
|
||||
lst = "]"
|
||||
lst = lst.."]"
|
||||
|
||||
return content:gsub(lst,charconv)
|
||||
end,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
-- Copyright (C) Paul Kulchenko 2011-2012
|
||||
-- update styles for comment markup
|
||||
-- styles for comment markup
|
||||
|
||||
local styles = ide.config.styles
|
||||
local comment = styles.comment
|
||||
local MD_MARK_ITAL = '_' -- italic
|
||||
local MD_MARK_BOLD = '*' -- bold
|
||||
local MD_MARK_LINK = '~' -- link
|
||||
local MD_MARK_HEAD = '!' -- header
|
||||
local MD_MARK_CODE = '@' -- code
|
||||
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_LSEP = ';' -- link separator (between text and link)
|
||||
local MD_MARK_MARK = ' ' -- separator
|
||||
local MD_LINK_NEWWINDOW = '+' -- indicator to open a new window for links
|
||||
local markup = {
|
||||
@@ -21,10 +24,15 @@ local markup = {
|
||||
[MD_MARK_MARK] = {st=31, fg=comment.fg, bg=comment.bg, v=false},
|
||||
}
|
||||
|
||||
-- allow other editor features to recognize this special markup
|
||||
function MarkupIsSpecial(style) return style == 31 end
|
||||
|
||||
local function q(s) return s:gsub('(.)','%%%1') end
|
||||
|
||||
local MD_MARK_PTRN = '' -- combination of all markup marks that can start styling
|
||||
for key,value in pairs(markup) do
|
||||
styles[key] = value
|
||||
if key ~= MD_MARK_MARK then MD_MARK_PTRN = MD_MARK_PTRN .. "%" .. key end
|
||||
if key ~= MD_MARK_MARK then MD_MARK_PTRN = MD_MARK_PTRN .. q(key) end
|
||||
end
|
||||
|
||||
function MarkupHotspotClick(pos, editor)
|
||||
@@ -35,19 +43,16 @@ function MarkupHotspotClick(pos, editor)
|
||||
end
|
||||
local line = editor:LineFromPosition(pos)
|
||||
local tx = editor:GetLine(line)
|
||||
pos = pos + 1 - editor:PositionFromLine(line) -- turn into relative position
|
||||
pos = pos + #MD_MARK_LINK - editor:PositionFromLine(line) -- turn into relative position
|
||||
|
||||
-- find the separator on the right side of the position
|
||||
local poss = string.find(tx, MD_MARK_LSEP, pos, true)
|
||||
local pose = string.find(tx, MD_MARK_LINK, pos, true)
|
||||
|
||||
if (poss and pose) then
|
||||
local text = string.sub(tx, poss+1, pose-1)
|
||||
-- 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 filepath = ide.openDocuments[editor:GetId()].filePath
|
||||
local _,_,shell = string.find(text, [[^macro:shell%((.*%S)%)$]])
|
||||
local _,_,http = string.find(text, [[^(http:%S+)$]])
|
||||
local _,_,http = string.find(text, [[^(https?:%S+)$]])
|
||||
local _,_,command = string.find(text, [[^macro:(%w+)$]])
|
||||
local bottomnotebook = ide.frame.bottomnotebook
|
||||
if shell then
|
||||
ShellExecuteCode(shell)
|
||||
elseif command == 'run' then -- run the current file
|
||||
@@ -56,7 +61,7 @@ function MarkupHotspotClick(pos, editor)
|
||||
ProjectDebug()
|
||||
elseif http then -- open the URL in a new browser window
|
||||
wx.wxLaunchDefaultBrowser(http, 0)
|
||||
else
|
||||
elseif filepath then -- only check for saved files
|
||||
-- check if requested to open in a new window
|
||||
local newwindow = string.find(text, MD_LINK_NEWWINDOW, 1, true) -- plain search
|
||||
if newwindow then text = string.gsub(text, "^%" .. MD_LINK_NEWWINDOW, "") end
|
||||
@@ -66,7 +71,8 @@ function MarkupHotspotClick(pos, editor)
|
||||
local filename = wx.wxFileName(name)
|
||||
filename:Normalize() -- remove .., ., and other similar elements
|
||||
if filename:FileExists() and
|
||||
(newindow or SaveModifiedDialog(editor, true) ~= wx.wxID_CANCEL) then
|
||||
(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
|
||||
@@ -78,22 +84,35 @@ local function ismarkup (tx)
|
||||
local start = 1
|
||||
while true do
|
||||
-- find a separator first
|
||||
local st,_,sep = string.find(tx, "(["..MD_MARK_PTRN.."])", start)
|
||||
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
|
||||
s,e,cap = string.find(tx,"^(%"..MD_MARK_HEAD..".+[%w%p])", start)
|
||||
-- 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
|
||||
local any = "[^%"..MD_MARK_LINK.."%"..MD_MARK_LSEP.."]-"
|
||||
s,e,cap = string.find(tx,"^(%"..MD_MARK_LINK.."[%w%p]"..any.."[%w%p]"..
|
||||
"%"..MD_MARK_LSEP.."[%w%p]"..any.."[%w%p]"..
|
||||
"%"..MD_MARK_LINK..")", st)
|
||||
else
|
||||
s,e,cap = string.find(tx,"^(%"..sep.."[%w%p][^%"..sep.."]-[%w%p]%"..sep..")", st)
|
||||
if not s then s,e,cap = string.find(tx,"^(%"..sep.."[^%s%"..sep.."]%"..sep..")", st) end
|
||||
-- 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 then return s,e,cap,sep end
|
||||
if s and -- selected markup is surrounded by spaces or punctuation
|
||||
(s == start or tx:sub(s-1, s-1):match("[%s%p]")) and
|
||||
(e-s == #tx-1 or tx:sub(e+1, e+1):match("[%s%p]"))
|
||||
then return s,e,cap,sep end
|
||||
start = st+1
|
||||
end
|
||||
end
|
||||
@@ -105,15 +124,14 @@ function MarkupStyle(editor, lines, linee)
|
||||
-- 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 linee = linee or editor:GetLineCount()-1
|
||||
local linef = editor:GetLineCount()
|
||||
local linee = linee or editor:GetLineCount()
|
||||
|
||||
local iscomment = {}
|
||||
for i,v in pairs(editor.spec.iscomment) do
|
||||
iscomment[i] = v
|
||||
end
|
||||
|
||||
for line=lines,linef do
|
||||
for line=lines,linee do
|
||||
local tx = editor:GetLine(line)
|
||||
local ls = editor:PositionFromLine(line)
|
||||
|
||||
@@ -129,20 +147,23 @@ function MarkupStyle(editor, lines, linee)
|
||||
if (f) then
|
||||
local p = ls+f+off
|
||||
local s = bit.band(editor:GetStyleAt(p), 31)
|
||||
if iscomment[s] then
|
||||
editor:StartStyling(p, 31)
|
||||
editor:SetStyling(1, markup[MD_MARK_MARK].st)
|
||||
local endmark = 1
|
||||
-- only style comments and only those that are not at the beginning
|
||||
-- of the file to avoid styling shebang (#!) lines
|
||||
if iscomment[s] and p > 0 then
|
||||
local smark = #mark
|
||||
local emark = #mark -- assumes end mark is the same length as start mark
|
||||
if mark == MD_MARK_HEAD then
|
||||
endmark = 0
|
||||
-- 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 pipe = w:find(MD_MARK_LSEP)
|
||||
if pipe then
|
||||
endmark = #w-pipe+1
|
||||
end
|
||||
local lsep = w:find(q(MD_MARK_LINZ)..q(MD_MARK_LINA))
|
||||
if lsep then emark = #w-lsep+#MD_MARK_LINT end
|
||||
end
|
||||
editor:SetStyling(t-f-endmark, markup[mark].st or markup[MD_MARK_MARK].st)
|
||||
editor:SetStyling(endmark, markup[MD_MARK_MARK].st)
|
||||
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
|
||||
@@ -151,3 +172,31 @@ function MarkupStyle(editor, lines, linee)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- this could work by calling MarkupStyle directly from EVT_UPDATEUI,
|
||||
-- but the styling didn't work correctly as the style on block comments
|
||||
-- (which is used to identify where the markup should be applied)
|
||||
-- was not always correct during UPDATEUI event.
|
||||
-- to rectify this, we style immediately (by calling MarkupStyle
|
||||
-- from UPDATEUI), but also store the starting point and re-style during
|
||||
-- the next UPDATEUI/IDLE event when the block comment style is correct.
|
||||
local needStyle = {}
|
||||
local frame
|
||||
function MarkupStyleRefresh(editor, ev)
|
||||
if not frame then
|
||||
frame = ide.frame
|
||||
frame:Connect(wx.wxEVT_IDLE,
|
||||
function(event) MarkupStyleRefresh(); event:Skip() end)
|
||||
end
|
||||
|
||||
if not ev or #ev == 0 then -- no new records, refresh deferred ones
|
||||
for ed,line in pairs(needStyle) do
|
||||
MarkupStyle(ed, line)
|
||||
needStyle[ed] = nil
|
||||
end
|
||||
else -- store records from the event table to defer refresh
|
||||
for _,pos in ipairs(ev) do
|
||||
needStyle[editor] = editor:LineFromPosition(pos[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,5 +8,3 @@ dofile "src/editor/menu_search.lua"
|
||||
dofile "src/editor/menu_view.lua"
|
||||
dofile "src/editor/menu_project.lua"
|
||||
dofile "src/editor/menu_tools.lua"
|
||||
|
||||
ide.frame:SetMenuBar(ide.frame.menuBar )
|
||||
|
||||
@@ -17,11 +17,11 @@ local editMenu = wx.wxMenu{
|
||||
{ ID_UNDO, "&Undo\tCtrl-Z", "Undo the last action" },
|
||||
{ ID_REDO, "&Redo\tCtrl-Y", "Redo the last action undone" },
|
||||
{ },
|
||||
{ ID "edit.showtooltip", "Show &Tooltip\tCtrl+T", "Show tooltip for current position. Place cursor after opening bracket of function."},
|
||||
{ ID_AUTOCOMPLETE, "Complete &Identifier\tCtrl+K", "Complete the current identifier" },
|
||||
{ ID "edit.showtooltip", "Show &Tooltip\tCtrl-T", "Show tooltip for current position. Place cursor after opening bracket of function."},
|
||||
{ ID_AUTOCOMPLETE, "Complete &Identifier\tCtrl-K", "Complete the current identifier" },
|
||||
{ ID_AUTOCOMPLETE_ENABLE, "Auto complete Identifiers", "Auto complete while typing", wx.wxITEM_CHECK },
|
||||
{ },
|
||||
{ ID_COMMENT, "C&omment/Uncomment\tCtrl-Q", "Comment or uncomment current or selected lines"},
|
||||
{ ID_COMMENT, "C&omment/Uncomment\tCtrl-U", "Comment or uncomment current or selected lines"},
|
||||
{ },
|
||||
{ ID_FOLD, "&Fold/Unfold all\tF12", "Fold or unfold all code folds"},
|
||||
{ ID "edit.cleardynamics", "Clear &Dynamic Words", "Resets the dynamic word list for autcompletion."},
|
||||
@@ -35,13 +35,13 @@ function OnUpdateUIEditMenu(event) -- enable if there is a valid focused editor
|
||||
event:Enable(editor ~= nil)
|
||||
end
|
||||
|
||||
local shellboxeditor = ide.frame.bottomnotebook.shellbox
|
||||
local othereditors = { frame.bottomnotebook.shellbox, frame.bottomnotebook.errorlog }
|
||||
|
||||
function OnEditMenu(event)
|
||||
local menu_id = event:GetId()
|
||||
local editor = GetEditor()
|
||||
if shellboxeditor:FindFocus():GetId() == shellboxeditor:GetId() then
|
||||
editor = shellboxeditor
|
||||
for _,e in pairs(othereditors) do
|
||||
if e:FindFocus():GetId() == e:GetId() then editor = e end
|
||||
end
|
||||
if editor == nil then return end
|
||||
|
||||
@@ -93,22 +93,38 @@ frame:Connect(ID "edit.cleardynamics", wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
frame:Connect(ID "edit.showtooltip", wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
local editor = GetEditor()
|
||||
|
||||
if (editor:CallTipActive()) then
|
||||
editor:CallTipCancel()
|
||||
return
|
||||
end
|
||||
|
||||
local pos = editor:GetCurrentPos()
|
||||
local line = editor:GetCurrentLine()
|
||||
local linetx = editor:GetLine(line)
|
||||
local linestart = editor:PositionFromLine(line)
|
||||
local localpos = pos-linestart
|
||||
|
||||
linetxtopos = linetx:sub(1,localpos)
|
||||
local ident = "([a-zA-Z_0-9][a-zA-Z_0-9%.%:]*)"
|
||||
local linetxtopos = linetx:sub(1,localpos)
|
||||
linetxtopos = linetxtopos..")"
|
||||
linetxtopos = linetxtopos:match("([a-zA-Z_0-9%.%:]+)%b()$")
|
||||
linetxtopos = linetxtopos:match(ident .. "%b()$")
|
||||
|
||||
local tip = linetxtopos and GetTipInfo(editor,linetxtopos.."(",false)
|
||||
if tip then
|
||||
if(editor:CallTipActive()) then
|
||||
editor:CallTipCancel()
|
||||
editor:CallTipShow(pos, tip)
|
||||
else
|
||||
-- check if we have a selected text or an identifier
|
||||
-- for an identifier, check fragments on the left and on the right.
|
||||
-- this is to match 'io' in 'i^o.print' and 'io.print' in 'io.pr^int'
|
||||
local left = linetx:sub(1,localpos):match(ident.."$")
|
||||
local right = linetx:sub(localpos+1,#linetx):match("^[a-zA-Z_0-9]*")
|
||||
local var = editor:GetSelectionStart() ~= editor:GetSelectionEnd()
|
||||
and editor:GetSelectedText()
|
||||
or left and left..right or nil
|
||||
if var and ide.debugger then
|
||||
ide.debugger.quickeval(var, function(val) editor:CallTipShow(pos, val) end)
|
||||
end
|
||||
editor:CallTipShow(pos,tip)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
@@ -7,20 +7,19 @@ local ide = ide
|
||||
local frame = ide.frame
|
||||
local menuBar = frame.menuBar
|
||||
local openDocuments = ide.openDocuments
|
||||
local debugger = ide.debugger
|
||||
|
||||
local fileMenu = wx.wxMenu({
|
||||
{ ID_NEW, "&New\tCtrl-N", "Create an empty document" },
|
||||
{ ID_OPEN, "&Open...\tCtrl-O", "Open an existing document" },
|
||||
{ ID_CLOSE, "&Close page\tCtrl+W", "Close the current editor window" },
|
||||
{ ID_CLOSE, "&Close page\tCtrl-W", "Close the current editor window" },
|
||||
{ },
|
||||
{ ID_SAVE, "&Save\tCtrl-S", "Save the current document" },
|
||||
{ ID_SAVEAS, "Save &As...\tAlt-Shift-S", "Save the current document to a file with a new name" },
|
||||
{ ID_SAVEALL, "Save A&ll...\tCtrl-Shift-S", "Save all open documents" },
|
||||
{ ID_SAVEALL, "Save A&ll...", "Save all open documents" },
|
||||
{ },
|
||||
--{ ID "file.recentfiles", "Recent files",},
|
||||
{ },
|
||||
{ ID_EXIT, "E&xit\tAlt-X", "Exit Program" }})
|
||||
{ ID_EXIT, "E&xit", "Exit Program" }})
|
||||
menuBar:Append(fileMenu, "&File")
|
||||
|
||||
local filehistorymenu = wx.wxMenu({})
|
||||
@@ -105,12 +104,11 @@ frame:Connect(ID_CLOSE, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
end)
|
||||
frame:Connect(ID_CLOSE, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
event:Enable((GetEditor() ~= nil) and (debugger.server == nil))
|
||||
event:Enable(GetEditor() ~= nil)
|
||||
end)
|
||||
|
||||
frame:Connect(ID_EXIT, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
if not SaveOnExit(true) then return end
|
||||
frame:Close() -- will handle wxEVT_CLOSE_WINDOW
|
||||
DebuggerCloseWatchWindow()
|
||||
frame:Close() -- this will trigger wxEVT_CLOSE_WINDOW
|
||||
end)
|
||||
|
||||
@@ -43,19 +43,19 @@ end
|
||||
|
||||
local debugTab = {
|
||||
{ ID_RUN, "&Run\tF6", "Execute the current project/file" },
|
||||
{ ID_RUNNOW, "Run as Scratchpad\tCtrl-F6", "Execute the current project/file and keep updating the code to see immediate results", wx.wxITEM_CHECK },
|
||||
{ ID_COMPILE, "&Compile\tF7", "Test compile the Lua file" },
|
||||
{ ID_START_DEBUG, "Start &Debugging\tF5", "Start a debugging session" },
|
||||
{ ID_ATTACH_DEBUG, "&Start Debugger Server\tShift-F6", "Allow a client to start a debugging session" },
|
||||
{ ID_ATTACH_DEBUG, "&Start Debugger Server", "Allow a client to start a debugging session" },
|
||||
{ },
|
||||
{ ID_STOP_DEBUG, "S&top Debugging\tShift-F12", "Stop the currently running process" },
|
||||
{ ID_STEP, "St&ep\tF11", "Step into the next line" },
|
||||
{ ID_STEP_OVER, "Step &Over\tF10", "Step over the next line" },
|
||||
{ ID_STEP_OUT, "Step O&ut\tShift-F10", "Step out of the current function" },
|
||||
{ ID_TRACE, "Tr&ace", "Trace execution showing each executed line" },
|
||||
{ ID_BREAK, "&Break", "Stop execution of the program at the next executed line of code" },
|
||||
{ ID_BREAK, "&Break\tShift-F9", "Stop execution of the program at the next executed line of code" },
|
||||
{ },
|
||||
{ ID_TOGGLEBREAKPOINT, "Toggle &Breakpoint\tF9", "Toggle Breakpoint" },
|
||||
--{ ID "view.debug.callstack", "V&iew Call Stack", "View the call stack" },
|
||||
{ ID_TOGGLEBREAKPOINT, "Toggle Break&point\tF9", "Toggle Breakpoint" },
|
||||
{ },
|
||||
{ ID_CLEAROUTPUT, "C&lear Output Window", "Clear the output window before compiling or debugging", wx.wxITEM_CHECK },
|
||||
}
|
||||
@@ -71,8 +71,8 @@ local targetDirMenu = wx.wxMenu{
|
||||
{ID "debug.projectdir.currentdir",""}
|
||||
}
|
||||
|
||||
debugMenu:Append(0,"Lua &interpreter",targetMenu,"Set the interpreter to be used")
|
||||
debugMenu:Append(0,"Project directory",targetDirMenu,"Set the project directory to be used")
|
||||
debugMenu:Append(ID_INTERPRETER,"Lua &Interpreter",targetMenu,"Set the interpreter to be used")
|
||||
debugMenu:Append(ID_PROJECTDIR,"Project Directory",targetDirMenu,"Set the project directory to be used")
|
||||
menuBar:Append(debugMenu, "&Project")
|
||||
|
||||
-----------------------------
|
||||
@@ -161,14 +161,14 @@ do
|
||||
menuBar:Check(defaultid, true)
|
||||
end
|
||||
|
||||
local function getNameToRun()
|
||||
local function getNameToRun(skipcheck)
|
||||
local editor = GetEditor()
|
||||
|
||||
-- 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 CompileProgram(editor)) then
|
||||
(not CompileProgram(editor, true) and not skipcheck)) then
|
||||
return
|
||||
end
|
||||
|
||||
@@ -179,7 +179,7 @@ local function getNameToRun()
|
||||
return wx.wxFileName(openDocuments[id].filePath)
|
||||
end
|
||||
|
||||
local function activateOutput()
|
||||
function ActivateOutput()
|
||||
if not ide.config.activateoutput then return end
|
||||
-- show output/errorlog pane
|
||||
uimgr:GetPane("bottomnotebook"):Show(true)
|
||||
@@ -192,27 +192,40 @@ local function activateOutput()
|
||||
end
|
||||
|
||||
local function runInterpreter(wfilename, withdebugger)
|
||||
activateOutput()
|
||||
ActivateOutput()
|
||||
|
||||
ClearAllCurrentLineMarkers()
|
||||
if not wfilename then return end
|
||||
local pid = ide.interpreter:frun(wfilename, withdebugger)
|
||||
debugger.pid = pid
|
||||
debugger.pid = ide.interpreter:frun(wfilename, withdebugger)
|
||||
return debugger.pid
|
||||
end
|
||||
|
||||
function ProjectRun()
|
||||
runInterpreter(getNameToRun())
|
||||
function ProjectRun(skipcheck)
|
||||
local fname = getNameToRun(skipcheck)
|
||||
if not fname then return end
|
||||
runInterpreter(fname)
|
||||
return true
|
||||
end
|
||||
|
||||
function ProjectDebug()
|
||||
local debuggers = {
|
||||
debug = "require('mobdebug').loop('%s',%d)",
|
||||
scratchpad = "require('mobdebug').scratchpad('%s',%d)"
|
||||
}
|
||||
|
||||
function ProjectDebug(skipcheck, debtype)
|
||||
if (debugger.server ~= nil) then
|
||||
if (not debugger.running) then
|
||||
ClearAllCurrentLineMarkers()
|
||||
debugger.run()
|
||||
end
|
||||
else
|
||||
runInterpreter(getNameToRun(), true)
|
||||
local debcall = (debuggers[debtype or "debug"]):
|
||||
format(ide.debugger.hostname, ide.debugger.portnumber)
|
||||
local fname = getNameToRun(skipcheck)
|
||||
if not fname then return end
|
||||
runInterpreter(fname, debcall)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-----------------------
|
||||
@@ -233,7 +246,7 @@ frame:Connect(ID_TOGGLEBREAKPOINT, wx.wxEVT_UPDATE_UI,
|
||||
frame:Connect(ID_COMPILE, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
local editor = GetEditor()
|
||||
activateOutput()
|
||||
ActivateOutput()
|
||||
CompileProgram(editor)
|
||||
end)
|
||||
frame:Connect(ID_COMPILE, wx.wxEVT_UPDATE_UI,
|
||||
@@ -242,13 +255,31 @@ frame:Connect(ID_COMPILE, wx.wxEVT_UPDATE_UI,
|
||||
event:Enable((debugger.server == nil and debugger.pid == nil) and (editor ~= nil))
|
||||
end)
|
||||
|
||||
frame:Connect(ID_RUN, wx.wxEVT_COMMAND_MENU_SELECTED, ProjectRun)
|
||||
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 event:IsChecked() then
|
||||
if not DebuggerScratchpadOn(GetEditor()) then
|
||||
menuBar:Check(ID_RUNNOW, false) -- disable if couldn't start scratchpad
|
||||
end
|
||||
else DebuggerScratchpadOff() 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
|
||||
(editor ~= nil) and ((debugger.server == nil or debugger.scratchable)
|
||||
and debugger.pid == nil or debugger.scratchpad ~= nil))
|
||||
end)
|
||||
|
||||
frame:Connect(ID_ATTACH_DEBUG, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
ClearAllCurrentLineMarkers()
|
||||
@@ -257,16 +288,19 @@ frame:Connect(ID_ATTACH_DEBUG, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
frame:Connect(ID_ATTACH_DEBUG, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
local editor = GetEditor()
|
||||
event:Enable((ide.interpreter) and (ide.interpreter.fattachdebug) and
|
||||
(not debugger.listening) and (debugger.server == nil) and (editor ~= nil))
|
||||
event:Enable((ide.interpreter) and (ide.interpreter.fattachdebug)
|
||||
and (not debugger.listening) and (debugger.server == nil)
|
||||
and (editor ~= nil) and (not debugger.scratchpad))
|
||||
end)
|
||||
|
||||
frame:Connect(ID_START_DEBUG, wx.wxEVT_COMMAND_MENU_SELECTED, ProjectDebug)
|
||||
frame:Connect(ID_START_DEBUG, wx.wxEVT_COMMAND_MENU_SELECTED, function () ProjectDebug() end)
|
||||
frame:Connect(ID_START_DEBUG, 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) or (debugger.server ~= nil and not debugger.running)) and (editor ~= nil))
|
||||
((debugger.server == nil and debugger.pid == nil) or
|
||||
(debugger.server ~= nil and not debugger.running)) and
|
||||
(editor ~= nil) and (not debugger.scratchpad))
|
||||
local label = (debugger.server ~= nil)
|
||||
and debugMenuRun.continue or debugMenuRun.start
|
||||
if debugMenu:GetLabel(ID_START_DEBUG) ~= label then
|
||||
@@ -277,8 +311,7 @@ frame:Connect(ID_START_DEBUG, wx.wxEVT_UPDATE_UI,
|
||||
frame:Connect(ID_STOP_DEBUG, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
ClearAllCurrentLineMarkers()
|
||||
if debugger.server then debugger.terminate() end
|
||||
if debugger.pid then DebuggerKillClient() end
|
||||
DebuggerShutdown()
|
||||
end)
|
||||
frame:Connect(ID_STOP_DEBUG, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
@@ -299,7 +332,8 @@ frame:Connect(ID_STEP, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
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))
|
||||
event:Enable((debugger.server ~= nil) and (not debugger.running)
|
||||
and (editor ~= nil) and (not debugger.scratchpad))
|
||||
end)
|
||||
|
||||
frame:Connect(ID_STEP_OVER, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
@@ -310,7 +344,8 @@ frame:Connect(ID_STEP_OVER, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
frame:Connect(ID_STEP_OVER, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
local editor = GetEditor()
|
||||
event:Enable((debugger.server ~= nil) and (not debugger.running) and (editor ~= nil))
|
||||
event:Enable((debugger.server ~= nil) and (not debugger.running)
|
||||
and (editor ~= nil) and (not debugger.scratchpad))
|
||||
end)
|
||||
|
||||
frame:Connect(ID_STEP_OUT, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
@@ -321,7 +356,8 @@ frame:Connect(ID_STEP_OUT, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
frame:Connect(ID_STEP_OUT, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
local editor = GetEditor()
|
||||
event:Enable((debugger.server ~= nil) and (not debugger.running) and (editor ~= nil))
|
||||
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,
|
||||
@@ -332,7 +368,8 @@ frame:Connect(ID_TRACE, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
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))
|
||||
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,
|
||||
@@ -344,25 +381,13 @@ frame:Connect(ID_BREAK, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
frame:Connect(ID_BREAK, wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
local editor = GetEditor()
|
||||
event:Enable((debugger.server ~= nil) and (debugger.running) and (editor ~= nil))
|
||||
event:Enable((debugger.server ~= nil) and (debugger.running)
|
||||
and (editor ~= nil) and (not debugger.scratchpad))
|
||||
end)
|
||||
|
||||
--[[
|
||||
frame:Connect(ID "view.debug.callstack", wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
if debugger.server then
|
||||
DebuggerCreateStackWindow()
|
||||
end
|
||||
end)
|
||||
frame:Connect(ID "view.debug.callstack", wx.wxEVT_UPDATE_UI,
|
||||
function (event)
|
||||
event:Enable((debugger.server ~= nil) and (not debugger.running))
|
||||
end)
|
||||
]]
|
||||
|
||||
frame:Connect(wx.wxEVT_IDLE,
|
||||
function(event)
|
||||
if (debugger.update) then
|
||||
debugger.update()
|
||||
end
|
||||
if (debugger.update) then debugger.update() end
|
||||
if (debugger.scratchpad) then DebuggerRefreshScratchpad() end
|
||||
event:Skip() -- let other EVT_IDLE handlers to work on the event
|
||||
end)
|
||||
|
||||
@@ -13,10 +13,10 @@ local findMenu = wx.wxMenu{
|
||||
{ ID_FIND, "&Find\tCtrl-F", "Find the specified text" },
|
||||
{ ID_FINDNEXT, "Find &Next\tF3", "Find the next occurrence of the specified text" },
|
||||
{ ID_FINDPREV, "Find &Previous\tShift-F3", "Repeat the search backwards in the file" },
|
||||
{ ID_REPLACE, "&Replace\tCtrl-H", "Replaces the specified text with different text" },
|
||||
{ ID_REPLACE, "&Replace\tCtrl-R", "Replaces the specified text with different text" },
|
||||
{ },
|
||||
{ ID_FIND_IN_FILES, "Find &In Files\tCtrl-Shift-F", " Find specified text in files"},
|
||||
{ ID_REPLACE_IN_FILES, "Re&place In Files\tCtrl-Shift-H", " Replace specified text in files"},
|
||||
{ ID_REPLACE_IN_FILES, "Re&place In Files\tCtrl-Shift-R", " Replace specified text in files"},
|
||||
{ },
|
||||
{ ID_GOTOLINE, "&Goto line\tCtrl-G", "Go to a selected line" },
|
||||
{ },
|
||||
|
||||
@@ -6,17 +6,16 @@ local frame = ide.frame
|
||||
local menuBar = frame.menuBar
|
||||
local uimgr = frame.uimgr
|
||||
|
||||
local debugger = ide.debugger
|
||||
|
||||
local viewMenu = wx.wxMenu{
|
||||
local viewMenu = wx.wxMenu {
|
||||
-- NYI { ID "view.preferences", "&Preferences...", "Brings up dialog for settings (TODO)" },
|
||||
-- NYI { },
|
||||
{ ID "view.filetree.show", "Project/&FileTree Window\tCtrl-Alt-P", "View the project/filetree window" },
|
||||
{ ID "view.output.show", "&Output/Shell Window\tCtrl-Alt-O", "View the output/shell window" },
|
||||
{ ID "view.debug.watches", "&Watch Window", "View the Watch window" },
|
||||
{ ID "view.filetree.show", "Project/&FileTree Window\tCtrl-Shift-P", "View the project/filetree window" },
|
||||
{ ID "view.output.show", "&Output/Console Window\tCtrl-Shift-O", "View the output/console window" },
|
||||
{ ID_VIEWWATCHWINDOW, "&Watch Window\tCtrl-Shift-W", "View the Watch window" },
|
||||
{ ID_VIEWCALLSTACK, "&Stack Window\tCtrl-Shift-S", "View the Stack window" },
|
||||
{ },
|
||||
{ ID "view.defaultlayout", "&Default Layout", "Reset to default layout"},
|
||||
{ ID "view.fullscreen", "Full &Screen\tCtrl-Alt-F", "Switch to or from full screen mode"},
|
||||
{ ID_FULLSCREEN, "Full &Screen\tCtrl-Shift-A", "Switch to or from full screen mode"},
|
||||
{ ID "view.style.loadconfig", "&Load Config Style...", "Load and apply style from config file (must contain .styles)"},
|
||||
}
|
||||
menuBar:Append(viewMenu, "&View")
|
||||
@@ -46,12 +45,12 @@ frame:Connect(ID "view.filetree.show", wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
uimgr:Update()
|
||||
end)
|
||||
|
||||
frame:Connect(ID "view.fullscreen", wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event) ShowFullScreen(not frame:IsFullScreen()) end)
|
||||
|
||||
frame:Connect(ID "view.debug.watches", wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event)
|
||||
if not debugger.watchWindow then
|
||||
DebuggerCreateWatchWindow()
|
||||
end
|
||||
frame:Connect(ID_FULLSCREEN, wx.wxEVT_COMMAND_MENU_SELECTED, function ()
|
||||
pcall(function() ShowFullScreen(not frame:IsFullScreen()) end)
|
||||
end)
|
||||
|
||||
frame:Connect(ID_VIEWWATCHWINDOW, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event) DebuggerCreateWatchWindow() end)
|
||||
|
||||
frame:Connect(ID_VIEWCALLSTACK, wx.wxEVT_COMMAND_MENU_SELECTED,
|
||||
function (event) DebuggerCreateStackWindow() end)
|
||||
|
||||
@@ -9,17 +9,22 @@ local errorlog = bottomnotebook.errorlog
|
||||
|
||||
-------
|
||||
-- setup errorlog
|
||||
local INPUT_MARKER = 3
|
||||
local INPUT_MARKER_VALUE = 2^INPUT_MARKER
|
||||
|
||||
errorlog:Show(true)
|
||||
errorlog:SetFont(ide.ofont)
|
||||
errorlog:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.ofont)
|
||||
errorlog:SetFont(ide.font.oNormal)
|
||||
errorlog:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.font.oNormal)
|
||||
errorlog:StyleClearAll()
|
||||
errorlog:SetMarginWidth(1, 16) -- marker margin
|
||||
errorlog:SetMarginType(1, wxstc.wxSTC_MARGIN_SYMBOL);
|
||||
errorlog:MarkerDefine(CURRENT_LINE_MARKER, wxstc.wxSTC_MARK_ARROWS, wx.wxBLACK, wx.wxWHITE)
|
||||
errorlog:MarkerDefine(INPUT_MARKER, wxstc.wxSTC_MARK_CHARACTER+string.byte('>'),
|
||||
wx.wxColour(127, 127, 127), wx.wxColour(240, 240, 240))
|
||||
errorlog:SetReadOnly(true)
|
||||
StylesApplyToEditor(ide.config.stylesoutshell,errorlog,ide.ofont,ide.ofontItalic)
|
||||
StylesApplyToEditor(ide.config.stylesoutshell,errorlog,ide.font.oNormal,ide.font.oItalic)
|
||||
|
||||
function ClearOutput(event)
|
||||
function ClearOutput()
|
||||
errorlog:SetReadOnly(false)
|
||||
errorlog:ClearAll()
|
||||
errorlog:SetReadOnly(true)
|
||||
@@ -33,9 +38,11 @@ function DisplayOutputNoMarker(...)
|
||||
message = message..tostring(v)..(i<cnt and "\t" or "")
|
||||
end
|
||||
|
||||
local current = errorlog:GetReadOnly()
|
||||
errorlog:SetReadOnly(false)
|
||||
errorlog:AppendText(message)
|
||||
errorlog:SetReadOnly(true)
|
||||
errorlog:EmptyUndoBuffer()
|
||||
errorlog:SetReadOnly(current)
|
||||
errorlog:GotoPos(errorlog:GetLength())
|
||||
end
|
||||
function DisplayOutput(...)
|
||||
@@ -45,7 +52,9 @@ end
|
||||
|
||||
local streamins = {}
|
||||
local streamerrs = {}
|
||||
local streamouts = {}
|
||||
local customprocs = {}
|
||||
local textout = '' -- this is a buffer for any text sent to external scripts
|
||||
|
||||
function CommandLineRunning(uid)
|
||||
for pid,custom in pairs(customprocs) do
|
||||
@@ -71,7 +80,9 @@ end
|
||||
pcall(function () return require 'winapi' end)
|
||||
local pid = nil
|
||||
local function unHideWxWindow(pidAssign)
|
||||
if pidAssign ~= nil then
|
||||
-- skip if not configured to do anything
|
||||
if not ide.config.unhidewxwindow then return end
|
||||
if pidAssign then
|
||||
pid = pidAssign > 0 and pidAssign or nil
|
||||
end
|
||||
if pid and winapi then
|
||||
@@ -81,37 +92,39 @@ local function unHideWxWindow(pidAssign)
|
||||
end)
|
||||
if win and not win:is_visible() then
|
||||
win:show()
|
||||
notebook:SetFocus() -- set focus back to the IDE window
|
||||
pid = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function nameTab(tab, name)
|
||||
local index = bottomnotebook:GetPageIndex(tab)
|
||||
if index then bottomnotebook:SetPageText(index, name) end
|
||||
end
|
||||
|
||||
function CommandLineRun(cmd,wdir,tooutput,nohide,stringcallback,uid,endcallback)
|
||||
if (not cmd) then return 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, "\\", "/")
|
||||
exename = string.match(exename,'%/*([^%/]+%.%w+)') or exename
|
||||
exename = string.match(exename,'%/*([^%/]+%.%w+)[%s%"]') or exename
|
||||
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
|
||||
DisplayOutput("Conflicting Process still running: "..cmd.."\n")
|
||||
DisplayOutput(("Program can't start because conflicting process is running as '%s'.\n")
|
||||
:format(cmd))
|
||||
return
|
||||
end
|
||||
|
||||
DisplayOutput("Running program: "..cmd.."\n")
|
||||
DisplayOutput(("Program starting as '%s'.\n"):format(cmd))
|
||||
|
||||
local pid = -1
|
||||
local proc = nil
|
||||
local customproc
|
||||
|
||||
if (tooutput) then
|
||||
customproc = wx.wxProcess(errorlog)
|
||||
customproc:Redirect()
|
||||
|
||||
proc = customproc
|
||||
end
|
||||
local proc = wx.wxProcess(errorlog)
|
||||
if (tooutput) then proc:Redirect() end -- redirect the output if requested
|
||||
|
||||
-- manipulate working directory
|
||||
local oldcwd
|
||||
@@ -121,42 +134,64 @@ function CommandLineRun(cmd,wdir,tooutput,nohide,stringcallback,uid,endcallback)
|
||||
end
|
||||
|
||||
-- launch process
|
||||
local pid = (proc and wx.wxExecute(cmd, wx.wxEXEC_ASYNC + (nohide and wx.wxEXEC_NOHIDE or 0),proc) or
|
||||
wx.wxExecute(cmd, wx.wxEXEC_ASYNC + (nohide and wx.wxEXEC_NOHIDE or 0)))
|
||||
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
|
||||
|
||||
-- check process
|
||||
if not pid or pid == -1 then
|
||||
DisplayOutputNoMarker("Unknown ERROR Running program!\n")
|
||||
customproc = nil
|
||||
-- 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
|
||||
DisplayOutput(("Program unable to run as '%s'\n"):format(cmd))
|
||||
return
|
||||
else
|
||||
DisplayOutput(
|
||||
"Process: "..uid..", pid:"..tostring(pid)..
|
||||
", started in '"..(wdir and wdir or wx.wxFileName.GetCwd()).."'\n")
|
||||
customprocs[pid] = {proc=customproc, uid=uid, endcallback=endcallback}
|
||||
end
|
||||
|
||||
DisplayOutput(("Program '%s' started in '%s' (pid: %d).\n")
|
||||
:format(uid, (wdir and wdir or wx.wxFileName.GetCwd()), pid))
|
||||
customprocs[pid] = {proc=proc, uid=uid, endcallback=endcallback, started = os.clock()}
|
||||
|
||||
local streamin = proc and proc:GetInputStream()
|
||||
local streamerr = proc and proc:GetErrorStream()
|
||||
local streamout = proc and proc:GetOutputStream()
|
||||
if (streamin) then
|
||||
streamins[pid] = {stream=streamin, callback=stringcallback}
|
||||
end
|
||||
if (streamerr) then
|
||||
streamerrs[pid] = {stream=streamerr, callback=stringcallback}
|
||||
end
|
||||
if (streamout) then
|
||||
streamouts[pid] = {stream=streamout, callback=stringcallback, out=true}
|
||||
end
|
||||
|
||||
unHideWxWindow(pid)
|
||||
nameTab(errorlog, "Output (running)")
|
||||
|
||||
return pid
|
||||
end
|
||||
|
||||
local inputBound -- to track where partial output ends for input editing purposes
|
||||
local function getInputLine()
|
||||
local totalLines = errorlog:GetLineCount()
|
||||
return errorlog:MarkerPrevious(totalLines+1, INPUT_MARKER_VALUE)
|
||||
end
|
||||
local function getInputText(bound)
|
||||
return errorlog:GetTextRange(
|
||||
errorlog:PositionFromLine(getInputLine())+(bound or 0), errorlog:GetLength())
|
||||
end
|
||||
local function updateInputMarker()
|
||||
local lastline = errorlog:GetLineCount()-1
|
||||
errorlog:MarkerDeleteAll(INPUT_MARKER)
|
||||
errorlog:MarkerAdd(lastline, INPUT_MARKER)
|
||||
inputBound = #getInputText()
|
||||
end
|
||||
|
||||
local function getStreams()
|
||||
local function displayStream(tab)
|
||||
for i,v in pairs(tab) do
|
||||
local function readStream(tab)
|
||||
for _,v in pairs(tab) do
|
||||
while(v.stream:CanRead()) do
|
||||
local str = v.stream:Read(4096)
|
||||
local pfn
|
||||
@@ -168,34 +203,66 @@ local function getStreams()
|
||||
else
|
||||
DisplayOutputNoMarker(str)
|
||||
end
|
||||
if str and ide.config.allowinteractivescript and
|
||||
(getInputLine() > -1 or errorlog:GetReadOnly()) then
|
||||
ActivateOutput()
|
||||
updateInputMarker()
|
||||
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
|
||||
|
||||
displayStream(streamins)
|
||||
displayStream(streamerrs)
|
||||
readStream(streamins)
|
||||
readStream(streamerrs)
|
||||
sendStream(streamouts)
|
||||
end
|
||||
|
||||
errorlog:Connect(wx.wxEVT_END_PROCESS, function(event)
|
||||
local pid = event:GetPid()
|
||||
if (pid ~= -1) then
|
||||
getStreams()
|
||||
-- delete markers and set focus to the editor if there is an input marker
|
||||
if errorlog:MarkerPrevious(errorlog:GetLineCount(), INPUT_MARKER_VALUE) > -1 then
|
||||
errorlog:MarkerDeleteAll(INPUT_MARKER)
|
||||
local editor = GetEditor()
|
||||
-- check if editor still exists; it may not if the window is closed
|
||||
if editor then editor:SetFocus() end
|
||||
end
|
||||
nameTab(errorlog, "Output")
|
||||
local runtime = os.clock() - customprocs[pid].started
|
||||
|
||||
streamins[pid] = nil
|
||||
streamerrs[pid] = nil
|
||||
streamouts[pid] = nil
|
||||
if (customprocs[pid].endcallback) then
|
||||
customprocs[pid].endcallback()
|
||||
end
|
||||
customprocs[pid] = nil
|
||||
unHideWxWindow(0)
|
||||
DisplayOutput("Program finished (pid: "..pid..").\n")
|
||||
DebuggerStop()
|
||||
DisplayOutput(("Program completed in %.2f seconds (pid: %d).\n")
|
||||
:format(runtime, pid))
|
||||
end
|
||||
end)
|
||||
|
||||
errorlog:Connect(wx.wxEVT_IDLE, function(event)
|
||||
if (#streamins or #streamerrs) then
|
||||
getStreams()
|
||||
end
|
||||
errorlog:Connect(wx.wxEVT_IDLE, function()
|
||||
if (#streamins or #streamerrs) then getStreams() end
|
||||
unHideWxWindow()
|
||||
end)
|
||||
|
||||
@@ -211,7 +278,7 @@ local jumptopatterns = {
|
||||
}
|
||||
|
||||
errorlog:Connect(wxstc.wxEVT_STC_DOUBLECLICK,
|
||||
function(event)
|
||||
function()
|
||||
local line = errorlog:GetCurrentLine()
|
||||
local linetx = errorlog:GetLine(line)
|
||||
-- try to detect a filename + line
|
||||
@@ -221,7 +288,7 @@ errorlog:Connect(wxstc.wxEVT_STC_DOUBLECLICK,
|
||||
local jumpline
|
||||
local jumplinepos
|
||||
|
||||
for i,pattern in ipairs(jumptopatterns) do
|
||||
for _,pattern in ipairs(jumptopatterns) do
|
||||
fname,jumpline,jumplinepos = linetx:match(pattern)
|
||||
if (fname and jumpline) then
|
||||
break
|
||||
@@ -240,5 +307,92 @@ errorlog:Connect(wxstc.wxEVT_STC_DOUBLECLICK,
|
||||
editor:SetFocus()
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
@@ -233,7 +233,6 @@ end
|
||||
|
||||
-----------------------------------
|
||||
|
||||
|
||||
local function saveNotebook(nb)
|
||||
local cnt = nb:GetPageCount()
|
||||
|
||||
@@ -250,7 +249,6 @@ local function saveNotebook(nb)
|
||||
|
||||
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)
|
||||
@@ -303,14 +301,14 @@ local function loadNotebook(nb,str,fnIdConvert)
|
||||
if (not str) then return end
|
||||
local cnt = nb:GetPageCount()
|
||||
local sel = nb:GetSelection()
|
||||
|
||||
|
||||
-- store old pages
|
||||
local currentpages = {}
|
||||
for i=1,cnt do
|
||||
local id = nb:GetPageText(i-1)
|
||||
local newid = fnIdConvert and fnIdConvert(id) or id
|
||||
currentpages[newid] = {page = nb:GetPage(i-1), text = id, index = i-1}
|
||||
currentpages[newid] = currentpages[newid] or {}
|
||||
table.insert(currentpages[newid], {page = nb:GetPage(i-1), text = id, index = i-1})
|
||||
end
|
||||
|
||||
-- remove them
|
||||
@@ -318,7 +316,7 @@ local function loadNotebook(nb,str,fnIdConvert)
|
||||
nb:RemovePage(i-1)
|
||||
end
|
||||
|
||||
-- readd them and perform splits
|
||||
-- read them and perform splits
|
||||
local direction
|
||||
local splits = {
|
||||
X = wx.wxRIGHT,
|
||||
@@ -337,13 +335,13 @@ local function loadNotebook(nb,str,fnIdConvert)
|
||||
local instr = cmd:match("<(%w)>")
|
||||
if (not instr) then
|
||||
local id = fnIdConvert and fnIdConvert(cmd) or cmd
|
||||
local page = currentpages[id]
|
||||
if (page) then
|
||||
local pageind = next(currentpages[id] or {})
|
||||
if (pageind) then
|
||||
local page = currentpages[id][pageind]
|
||||
currentpages[id][pageind] = nil
|
||||
|
||||
nb:AddPage(page.page, page.text)
|
||||
currentpages[id] = nil
|
||||
if (direction) then
|
||||
nb:Split(t, direction)
|
||||
end
|
||||
if (direction) then nb:Split(t, direction) end
|
||||
finishPage(page)
|
||||
end
|
||||
end
|
||||
@@ -351,18 +349,18 @@ local function loadNotebook(nb,str,fnIdConvert)
|
||||
end
|
||||
|
||||
-- add anything we forgot
|
||||
for i,page in pairs(currentpages) do
|
||||
nb:AddPage(page.page, page.text)
|
||||
finishPage(page)
|
||||
for _,pagelist in pairs(currentpages) do
|
||||
for _,page in pairs(pagelist) do
|
||||
nb:AddPage(page.page, page.text)
|
||||
finishPage(page)
|
||||
end
|
||||
end
|
||||
|
||||
if (newsel) then
|
||||
nb:SetSelection(newsel)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
function SettingsRestoreView()
|
||||
local listname = "/view"
|
||||
local path = settings:GetPath()
|
||||
@@ -394,7 +392,7 @@ function SettingsRestoreView()
|
||||
local layout = settingsReadSafe(settings,"nbbtmlayout",layoutcur)
|
||||
if (layout ~= layoutcur) then
|
||||
loadNotebook(ide.frame.bottomnotebook,layout,
|
||||
function(name) return name:match("console") or name end)
|
||||
function(name) return name:match("Output") or name end)
|
||||
end
|
||||
|
||||
settings:SetPath(path)
|
||||
@@ -424,6 +422,7 @@ function SettingsRestoreEditorSettings()
|
||||
ide.config.interpreter = settingsReadSafe(settings,"interpreter",ide.config.interpreter)
|
||||
ProjectSetInterpreter(ide.config.interpreter)
|
||||
end
|
||||
|
||||
function SettingsSaveEditorSettings()
|
||||
local listname = "/editor"
|
||||
local path = settings:GetPath()
|
||||
|
||||
@@ -8,14 +8,11 @@ local ide = ide
|
||||
local bottomnotebook = ide.frame.bottomnotebook
|
||||
local out = bottomnotebook.shellbox
|
||||
|
||||
local OUTPUT_MARKER = 3
|
||||
local remotesend
|
||||
|
||||
local OUTPUT_MARKER = 3
|
||||
local OUTPUT_MARKER_VALUE = 8 -- = 2^OUTPUT_MARKER
|
||||
|
||||
local frame = ide.frame
|
||||
out:SetFont(ide.ofont)
|
||||
out:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.ofont)
|
||||
out:SetFont(ide.font.oNormal)
|
||||
out:StyleSetFont(wxstc.wxSTC_STYLE_DEFAULT, ide.font.oNormal)
|
||||
out:StyleClearAll()
|
||||
out:SetBufferedDraw(true)
|
||||
|
||||
@@ -35,7 +32,7 @@ out:MarkerDefine(BREAKPOINT_MARKER, wxstc.wxSTC_MARK_BACKGROUND, wx.wxBLACK, wx.
|
||||
out:MarkerDefine(OUTPUT_MARKER, wxstc.wxSTC_MARK_BACKGROUND, wx.wxBLACK, wx.wxColour(240, 240, 240))
|
||||
out:SetReadOnly(false)
|
||||
|
||||
SetupKeywords(out,"lua",nil,ide.config.stylesoutshell,ide.ofont,ide.ofontItalic)
|
||||
SetupKeywords(out,"lua",nil,ide.config.stylesoutshell,ide.font.oNormal,ide.font.oItalic)
|
||||
|
||||
local function getPromptLine()
|
||||
local totalLines = out:GetLineCount()
|
||||
@@ -59,11 +56,12 @@ local function positionInLine(line)
|
||||
return out:GetCurrentPos() - out:PositionFromLine(line)
|
||||
end
|
||||
|
||||
local function caretOnPromptLine(disallowLeftmost)
|
||||
local function caretOnPromptLine(disallowLeftmost, line)
|
||||
local promptLine = getPromptLine()
|
||||
local currentLine = line or out:GetCurrentLine()
|
||||
local boundary = disallowLeftmost and 0 or -1
|
||||
return (out:GetCurrentLine() > promptLine
|
||||
or out:GetCurrentLine() == promptLine and positionInLine(promptLine) > boundary)
|
||||
return (currentLine > promptLine
|
||||
or currentLine == promptLine and positionInLine(promptLine) > boundary)
|
||||
end
|
||||
|
||||
local function chomp(line)
|
||||
@@ -109,6 +107,8 @@ 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 = ''
|
||||
@@ -155,6 +155,7 @@ 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
|
||||
@@ -200,7 +201,7 @@ local function createenv ()
|
||||
end
|
||||
|
||||
local function relativeFilepath(file)
|
||||
local name,level = luafilepath(3)
|
||||
local name = luafilepath(3)
|
||||
return (file and name) and name.."/"..file or file or name
|
||||
end
|
||||
|
||||
@@ -235,28 +236,32 @@ end
|
||||
|
||||
local env = createenv()
|
||||
|
||||
local function packResults(status, ...) return status, {...} end
|
||||
|
||||
local function executeShellCode(tx)
|
||||
if tx == nil or tx == '' then return end
|
||||
|
||||
DisplayShellPrompt('')
|
||||
|
||||
local addedret = false
|
||||
local fn,err
|
||||
if remotesend then
|
||||
remotesend(tx)
|
||||
else
|
||||
fn,err = loadstring(tx)
|
||||
-- for statement queries create the return
|
||||
if err and (err:find("'=' expected near '<eof>'") or
|
||||
err:find("unexpected symbol near '")) then
|
||||
local errmore
|
||||
fn,errmore = loadstring("return "..tx:gsub("^%s*=%s*",""))
|
||||
addedret = not errmore
|
||||
end
|
||||
-- try to compile as statement
|
||||
local _, err = loadstring(tx)
|
||||
local isstatement = not err
|
||||
|
||||
if remotesend then remotesend(tx, isstatement); return end
|
||||
|
||||
local addedret, forceexpression = true, tx:match("^%s*=%s*")
|
||||
tx = tx:gsub("^%s*=%s*","")
|
||||
fn, err = loadstring("return "..tx)
|
||||
if not forceexpression and err and
|
||||
(err:find("'<eof>' expected near '") or
|
||||
err:find("'%(' expected near") or
|
||||
err:find("unexpected symbol near '")) then
|
||||
fn, err = loadstring(tx)
|
||||
addedret = false
|
||||
end
|
||||
|
||||
if fn == nil and err then
|
||||
DisplayShellErr(err)
|
||||
DisplayShellErr(filterTraceError(err, addedret))
|
||||
elseif fn then
|
||||
setfenv(fn,env)
|
||||
|
||||
@@ -268,19 +273,33 @@ local function executeShellCode(tx)
|
||||
wx.wxFileName.SetCwd(projectDir)
|
||||
end
|
||||
|
||||
local ok, res = xpcall(fn,
|
||||
local ok, res = packResults(xpcall(fn,
|
||||
function(err)
|
||||
DisplayShellErr(filterTraceError(debug.traceback(err), addedret))
|
||||
end)
|
||||
end))
|
||||
|
||||
-- restore the current dir
|
||||
if projectDir then wx.wxFileName.SetCwd(cwd) end
|
||||
|
||||
if ok and (addedret or res ~= nil) then DisplayShell(res) 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] = mobdebug.line(v, {nocode = true, comment = 1})
|
||||
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((table.unpack or unpack)(res))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ShellSupportRemote(client,uid)
|
||||
function ShellSupportRemote(client)
|
||||
remotesend = client
|
||||
|
||||
local index = bottomnotebook:GetPageIndex(out)
|
||||
@@ -358,7 +377,7 @@ out:Connect(wx.wxEVT_KEY_DOWN,
|
||||
or key == wx.WXK_SHIFT or key == wx.WXK_CONTROL
|
||||
or key == wx.WXK_ALT then
|
||||
break
|
||||
elseif key == wx.WXK_RETURN or key == WXK_NUMPAD_ENTER then
|
||||
elseif key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER then
|
||||
if not caretOnPromptLine()
|
||||
or out:LineFromPosition(out:GetSelectionStart()) < getPromptLine() then
|
||||
return
|
||||
@@ -368,6 +387,7 @@ out:Connect(wx.wxEVT_KEY_DOWN,
|
||||
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:ClearAll()
|
||||
displayShellIntro()
|
||||
@@ -392,4 +412,20 @@ out:Connect(wx.wxEVT_KEY_DOWN,
|
||||
event:Skip()
|
||||
end)
|
||||
|
||||
local function inputEditable(line)
|
||||
return caretOnPromptLine(fale, line) and
|
||||
not (out:LineFromPosition(out:GetSelectionStart()) < getPromptLine())
|
||||
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)
|
||||
|
||||
displayShellIntro()
|
||||
|
||||
@@ -224,9 +224,9 @@ function ReApplySpecAndStyles()
|
||||
local errorlog = ide.frame.bottomnotebook.errorlog
|
||||
local shellbox = ide.frame.bottomnotebook.shellbox
|
||||
|
||||
SetupKeywords(shellbox,"lua",nil,ide.config.stylesoutshell,ide.ofont,ide.ofontItalic)
|
||||
SetupKeywords(shellbox,"lua",nil,ide.config.stylesoutshell,ide.font.oNormal,ide.font.oItalic)
|
||||
|
||||
StylesApplyToEditor(ide.config.stylesoutshell,errorlog,ide.ofont,ide.ofontItalic)
|
||||
StylesApplyToEditor(ide.config.stylesoutshell,errorlog,ide.font.oNormal,ide.font.oItalic)
|
||||
end
|
||||
|
||||
function LoadConfigStyle()
|
||||
@@ -234,7 +234,7 @@ function LoadConfigStyle()
|
||||
"/cfg",
|
||||
"",
|
||||
"Lua file (*.lua)|*.lua|All files (*)|*",
|
||||
wx.wxOPEN + wx.wxFILE_MUST_EXIST)
|
||||
wx.wxFD_OPEN + wx.wxFD_FILE_MUST_EXIST)
|
||||
if fileDialog:ShowModal() == wx.wxID_OK then
|
||||
local cfg = {wxstc = wxstc, path = {}, editor = {}, view ={}, acandtip = {}, outputshell = {}, debugger={},}
|
||||
local cfgfn,err = loadfile(fileDialog:GetPath())
|
||||
|
||||
87
src/main.lua
87
src/main.lua
@@ -1,9 +1,15 @@
|
||||
-- authors: Luxinia Dev (Eike Decker & Christoph Kubisch)
|
||||
---------------------------------------------------------
|
||||
|
||||
package.cpath = package.cpath..';bin/?.dll;bin/clibs/?.dll;bin/clibs/?/?.dll;bin/clibs/?/?/?.dll'
|
||||
package.cpath = package.cpath..';bin/?.so;bin/clibs/?.so;bin/clibs/?/?.so;bin/clibs/?/?/?.so'
|
||||
package.path = package.path..';lualibs/?.lua;lualibs/?/?.lua;lualibs/?/init.lua;lualibs/?/?/?.lua;lualibs/?/?/init.lua'
|
||||
-- put bin/ and lualibs/ first to avoid conflicts with included modules
|
||||
-- that may have other versions present somewhere else in path/cpath
|
||||
local iswindows = os.getenv('WINDIR') or (os.getenv('OS') or ''):match('[Ww]indows')
|
||||
package.cpath = (iswindows
|
||||
and 'bin/?.dll;bin/clibs/?.dll;'
|
||||
or 'bin/clibs/?.dylib;bin/lib?.dylib;bin/?.so;bin/clibs/?.so;')
|
||||
.. package.cpath
|
||||
package.path = 'lualibs/?.lua;lualibs/?/?.lua;lualibs/?/init.lua;lualibs/?/?/?.lua;lualibs/?/?/init.lua;'
|
||||
.. package.path
|
||||
|
||||
require("wx")
|
||||
require("bit")
|
||||
@@ -31,6 +37,7 @@ ide = {
|
||||
verbose = false,
|
||||
},
|
||||
outputshell = {},
|
||||
filetree = {},
|
||||
|
||||
styles = StylesGetDefault(),
|
||||
stylesoutshell = StylesGetDefault(),
|
||||
@@ -44,6 +51,8 @@ ide = {
|
||||
},
|
||||
|
||||
activateoutput = false, -- activate output/console on Run/Debug/Compile
|
||||
unhidewxwindow = false, -- try to unhide a wx window
|
||||
allowinteractivescript = false, -- allow interaction in the output window
|
||||
filehistorylength = 20,
|
||||
projecthistorylength = 15,
|
||||
savebak = false,
|
||||
@@ -83,10 +92,13 @@ ide = {
|
||||
-- modTime = wxDateTime of disk file or nil,
|
||||
-- isModified = bool is the document modified? }
|
||||
ignoredFilesList = {},
|
||||
font = nil,
|
||||
fontItalic = nil,
|
||||
ofont = nil,
|
||||
ofontItalic = nil,
|
||||
font = {
|
||||
eNormal = nil,
|
||||
eItalic = nil,
|
||||
oNormal = nil,
|
||||
oItalic = nil,
|
||||
fNormal = nil,
|
||||
}
|
||||
}
|
||||
|
||||
---------------
|
||||
@@ -95,11 +107,18 @@ local filenames = {}
|
||||
local configs = {}
|
||||
do
|
||||
local arg = {...}
|
||||
local fullPath = arg[1] -- first argument must be the application name
|
||||
assert(type(fullPath) == "string", "first argument must be application name")
|
||||
|
||||
if not wx.wxIsAbsolutePath(fullPath) then
|
||||
fullPath = wx.wxGetCwd().."/"..fullPath
|
||||
if wx.__WXMSW__ then fullPath = wx.wxUnix2DosFilename(fullPath) end
|
||||
end
|
||||
|
||||
ide.arg = arg
|
||||
-- first argument must be the application name
|
||||
assert(type(arg[1]) == "string","first argument must be application name")
|
||||
ide.editorFilename = arg[1]
|
||||
ide.config.path.app = arg[1]:match("([%w_-]+)%.?[^%.]*$")
|
||||
ide.editorFilename = fullPath
|
||||
ide.osname = wx.wxPlatformInfo.Get():GetOperatingSystemFamilyName()
|
||||
ide.config.path.app = fullPath:match("([%w_-%.]+)$"):gsub("%.[^%.]*$","")
|
||||
assert(ide.config.path.app, "no application path defined")
|
||||
for index = 2, #arg do
|
||||
if (arg[index] == "-cfg" and index+1 <= #arg) then
|
||||
@@ -128,7 +147,7 @@ local function addConfig(filename,showerror,isstring)
|
||||
ide.config.os = os
|
||||
ide.config.wxstc = wxstc
|
||||
setfenv(cfgfn,ide.config)
|
||||
xpcall(function()cfgfn(assert(_G))end,
|
||||
xpcall(function()cfgfn(assert(_G or _ENV))end,
|
||||
function(err)
|
||||
print("Error while executing configuration file: \n",
|
||||
debug.traceback(err))
|
||||
@@ -138,11 +157,6 @@ end
|
||||
|
||||
do
|
||||
addConfig(ide.config.path.app.."/config.lua",true)
|
||||
addConfig("cfg/user.lua",false)
|
||||
for i,v in ipairs(configs) do
|
||||
addConfig(v,true,true)
|
||||
end
|
||||
configs = nil
|
||||
end
|
||||
|
||||
----------------------
|
||||
@@ -171,7 +185,7 @@ local function addToTab(tab,file)
|
||||
|
||||
local success,result
|
||||
success, result = xpcall(
|
||||
function()return cfgfn(_G)end,
|
||||
function()return cfgfn(_G or _ENV)end,
|
||||
function(err)
|
||||
print(("Error while executing configuration file (%s): \n%s"):
|
||||
format(file,debug.traceback(err)))
|
||||
@@ -191,8 +205,7 @@ end
|
||||
|
||||
-- load interpreters
|
||||
local function loadInterpreters()
|
||||
|
||||
local files = FileSysGet(".\\interpreters\\*.*",wx.wxFILE)
|
||||
local files = FileSysGet("./interpreters/*.*",wx.wxFILE)
|
||||
for i,file in ipairs(files) do
|
||||
if file:match "%.lua$" and app.loadfilters.interpreters(file) then
|
||||
addToTab(ide.interpreters,file)
|
||||
@@ -203,8 +216,7 @@ loadInterpreters()
|
||||
|
||||
-- load specs
|
||||
local function loadSpecs()
|
||||
|
||||
local files = FileSysGet(".\\spec\\*.*",wx.wxFILE)
|
||||
local files = FileSysGet("./spec/*.*",wx.wxFILE)
|
||||
for i,file in ipairs(files) do
|
||||
if file:match "%.lua$" and app.loadfilters.specs(file) then
|
||||
addToTab(ide.specs,file)
|
||||
@@ -215,6 +227,7 @@ local function loadSpecs()
|
||||
spec.sep = spec.sep or ""
|
||||
spec.iscomment = {}
|
||||
spec.iskeyword0 = {}
|
||||
spec.isstring = {}
|
||||
if (spec.lexerstyleconvert) then
|
||||
if (spec.lexerstyleconvert.comment) then
|
||||
for i,s in pairs(spec.lexerstyleconvert.comment) do
|
||||
@@ -226,6 +239,11 @@ local function loadSpecs()
|
||||
spec.iskeyword0[s] = true
|
||||
end
|
||||
end
|
||||
if (spec.lexerstyleconvert.stringtxt) then
|
||||
for i,s in pairs(spec.lexerstyleconvert.stringtxt) do
|
||||
spec.isstring[s] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -233,8 +251,7 @@ loadSpecs()
|
||||
|
||||
-- load tools
|
||||
local function loadTools()
|
||||
|
||||
local files = FileSysGet(".\\tools\\*.*",wx.wxFILE)
|
||||
local files = FileSysGet("./tools/*.*",wx.wxFILE)
|
||||
for i,file in ipairs(files) do
|
||||
if file:match "%.lua$" and app.loadfilters.tools(file) then
|
||||
addToTab(ide.tools,file)
|
||||
@@ -245,6 +262,14 @@ loadTools()
|
||||
|
||||
if app.preinit then app.preinit() end
|
||||
|
||||
do
|
||||
addConfig("cfg/user.lua",false)
|
||||
for i,v in ipairs(configs) do
|
||||
addConfig(v,true,true)
|
||||
end
|
||||
configs = nil
|
||||
end
|
||||
|
||||
---------------
|
||||
-- Load App
|
||||
|
||||
@@ -302,10 +327,16 @@ end
|
||||
|
||||
if app.postinit then app.postinit() end
|
||||
|
||||
-- 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)
|
||||
if ide.osname == 'Macintosh' then -- force refresh to fix the filetree
|
||||
pcall(function() ide.frame:ShowFullScreen(true) ide.frame:ShowFullScreen(false) end)
|
||||
end
|
||||
ide.frame:Show(true)
|
||||
|
||||
-- Call wx.wxGetApp():MainLoop() last to start the wxWidgets event loop,
|
||||
-- otherwise the wxLua program will exit immediately.
|
||||
-- Does nothing if running from wxLua, wxLuaFreeze, or wxLuaEdit since the
|
||||
-- MainLoop is already running or will be started by the C++ program.
|
||||
-- call wx.wxGetApp():MainLoop() last to start the wxWidgets event loop,
|
||||
-- otherwise the program will exit immediately.
|
||||
-- Does nothing if the MainLoop is already running.
|
||||
wx.wxGetApp():MainLoop()
|
||||
|
||||
@@ -168,7 +168,7 @@ function FileSysGet(dir,spec)
|
||||
end
|
||||
local f = browse:FindFirst(dir,spec)
|
||||
while #f>0 do
|
||||
table.insert(content,f)
|
||||
table.insert(content,(f:gsub("^file:",""))) -- remove file: protocol (wx2.9+)
|
||||
f = browse:FindNext()
|
||||
end
|
||||
return content
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
LICENSE
|
||||
README
|
||||
README.md
|
||||
api/lua/baselib.lua
|
||||
api/lua/love2d.lua
|
||||
api/readme.txt
|
||||
bin/clibs/lfs.dll
|
||||
bin/clibs/mojoshader.dll
|
||||
bin/clibs/mime/core.dll
|
||||
bin/clibs/ssl.dll
|
||||
bin/clibs/socket/core.dll
|
||||
bin/clibs/wx/wxlua_msw28_wxbindadv.dll
|
||||
bin/clibs/wx/wxlua_msw28_wxbindaui.dll
|
||||
@@ -38,17 +41,25 @@ bin/wxmsw28_qa_vc_custom.dll
|
||||
bin/wxmsw28_richtext_vc_custom.dll
|
||||
bin/wxmsw28_stc_vc_custom.dll
|
||||
bin/wxmsw28_xrc_vc_custom.dll
|
||||
cfg/user.lua_for_custom_settings.txt
|
||||
cfg/user-sample.lua
|
||||
interpreters/love2d.lua
|
||||
interpreters/luadeb.lua
|
||||
lualibs/copas/copas.lua
|
||||
lualibs/coxpcall/coxpcall.lua
|
||||
lualibs/luainspect/ast.lua
|
||||
lualibs/luainspect/compat_env.lua
|
||||
lualibs/luainspect/dump.lua
|
||||
lualibs/luainspect/globals.lua
|
||||
lualibs/luainspect/init.lua
|
||||
lualibs/luainspect/signatures.lua
|
||||
lualibs/luainspect/typecheck.lua
|
||||
lualibs/luainspect/types.lua
|
||||
lualibs/metalua/compile.lua
|
||||
lualibs/metalua/gg.lua
|
||||
lualibs/metalua/lcode.lua
|
||||
lualibs/metalua/ldump.lua
|
||||
lualibs/metalua/lexer.lua
|
||||
lualibs/metalua/lopcodes.lua
|
||||
lualibs/metalua/metalua
|
||||
lualibs/metalua/metalua.lua
|
||||
lualibs/metalua/mlp_expr.lua
|
||||
lualibs/metalua/mlp_ext.lua
|
||||
@@ -62,14 +73,16 @@ lualibs/metalua/metalua/runtime.lua
|
||||
lualibs/metalua/metalua/string2.lua
|
||||
lualibs/metalua/metalua/table2.lua
|
||||
lualibs/mobdebug/mobdebug.lua
|
||||
lualibs/socket/ltn12.lua
|
||||
lualibs/socket/mime.lua
|
||||
lualibs/socket/socket.lua
|
||||
lualibs/socket/socket/ftp.lua
|
||||
lualibs/socket/socket/http.lua
|
||||
lualibs/socket/socket/smtp.lua
|
||||
lualibs/socket/socket/tp.lua
|
||||
lualibs/socket/socket/url.lua
|
||||
lualibs/ltn12.lua
|
||||
lualibs/mime.lua
|
||||
lualibs/socket.lua
|
||||
lualibs/socket/ftp.lua
|
||||
lualibs/socket/http.lua
|
||||
lualibs/socket/smtp.lua
|
||||
lualibs/socket/tp.lua
|
||||
lualibs/socket/url.lua
|
||||
lualibs/ssl.lua
|
||||
lualibs/ssl/https.lua
|
||||
spec/lua.lua
|
||||
src/defs.lua
|
||||
src/editor/autocomplete.lua
|
||||
@@ -80,6 +93,7 @@ src/editor/filetree.lua
|
||||
src/editor/findreplace.lua
|
||||
src/editor/gui.lua
|
||||
src/editor/ids.lua
|
||||
src/editor/inspect.lua
|
||||
src/editor/iofilters.lua
|
||||
src/editor/markup.lua
|
||||
src/editor/menu.lua
|
||||
@@ -114,12 +128,15 @@ zbstudio/res/16/wxART_FIND.png
|
||||
zbstudio/res/16/wxART_FIND_AND_REPLACE.png
|
||||
zbstudio/res/16/wxART_FOLDER.png
|
||||
zbstudio/res/16/wxART_GO_DIR_UP.png
|
||||
zbstudio/res/16/wxART_GO_FORWARD-wxART_OTHER_C.png
|
||||
zbstudio/res/16/wxART_HELP_PAGE.png
|
||||
zbstudio/res/16/wxART_LIST_VIEW-wxART_OTHER_C.png
|
||||
zbstudio/res/16/wxART_NEW_DIR.png
|
||||
zbstudio/res/16/wxART_NORMAL_FILE-wxART_OTHER_C.png
|
||||
zbstudio/res/16/wxART_NORMAL_FILE.png
|
||||
zbstudio/res/16/wxART_PASTE.png
|
||||
zbstudio/res/16/wxART_REDO.png
|
||||
zbstudio/res/16/wxART_REPORT_VIEW-wxART_OTHER_C.png
|
||||
zbstudio/res/16/wxART_UNDO.png
|
||||
zbstudio/res/32.ico
|
||||
zbstudio/res/zerobrane.png
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
local icons = {}
|
||||
local CreateBitmap = function(id, client, size)
|
||||
local width = size:GetWidth()
|
||||
local key = width .. "/" .. id
|
||||
local fileClient = "zbstudio/res/" .. key .. "-" .. client .. ".png"
|
||||
local fileKey = "zbstudio/res/" .. key .. ".png"
|
||||
local file
|
||||
if wx.wxFileName(fileClient):FileExists() then file = fileClient
|
||||
elseif wx.wxFileName(fileKey):FileExists() then file = fileKey
|
||||
else return wx.wxNullBitmap end
|
||||
local icon = icons[file] or wx.wxBitmap(file)
|
||||
icons[file] = icon
|
||||
return icon
|
||||
end
|
||||
local ide = ide
|
||||
local app = {
|
||||
|
||||
createbitmap = CreateBitmap,
|
||||
loadfilters = {
|
||||
tools = function(file) return false end,
|
||||
specs = function(file) return true end,
|
||||
@@ -9,23 +23,12 @@ local app = {
|
||||
|
||||
preinit = function ()
|
||||
local artProvider = wx.wxLuaArtProvider()
|
||||
local icons = {}
|
||||
artProvider.CreateBitmap = function(self, id, client, size)
|
||||
local width = size:GetWidth()
|
||||
local key = width .. "/" .. id
|
||||
local fileClient = "zbstudio/res/" .. key .. "-" .. client .. ".png"
|
||||
local fileKey = "zbstudio/res/" .. key .. ".png"
|
||||
local file
|
||||
if wx.wxFileName(fileClient):FileExists() then file = fileClient
|
||||
elseif wx.wxFileName(fileKey):FileExists() then file = fileKey
|
||||
else return wx.wxNullBitmap end
|
||||
local icon = icons[file] or wx.wxBitmap(file)
|
||||
icons[file] = icon
|
||||
return icon
|
||||
end
|
||||
artProvider.CreateBitmap = function(self, ...) return CreateBitmap(...) end
|
||||
wx.wxArtProvider.Push(artProvider)
|
||||
|
||||
ide.config.interpreter = "luadeb";
|
||||
ide.config.interpreter = "luadeb"
|
||||
ide.config.unhidewxwindow = true -- allow unhiding of wx windows
|
||||
ide.config.allowinteractivescript = true -- allow interaction in the output window
|
||||
|
||||
-- this needs to be in pre-init to load the styles
|
||||
dofile("src/editor/markup.lua")
|
||||
@@ -33,6 +36,7 @@ local app = {
|
||||
|
||||
postinit = function ()
|
||||
dofile("zbstudio/menu_help.lua")
|
||||
dofile("src/editor/inspect.lua")
|
||||
|
||||
local bundle = wx.wxIconBundle()
|
||||
local files = FileSysGet("zbstudio/res/", wx.wxFILE)
|
||||
@@ -47,9 +51,7 @@ local app = {
|
||||
|
||||
local menuBar = ide.frame.menuBar
|
||||
local menu = menuBar:GetMenu(menuBar:FindMenu("&Project"))
|
||||
local itemid = menu:FindItem("Lua &interpreter")
|
||||
if itemid ~= wx.wxNOT_FOUND then menu:Destroy(itemid) end
|
||||
itemid = menu:FindItem("Project directory")
|
||||
local itemid = menu:FindItem("Project Directory")
|
||||
if itemid ~= wx.wxNOT_FOUND then menu:Destroy(itemid) end
|
||||
|
||||
menu = menuBar:GetMenu(menuBar:FindMenu("&View"))
|
||||
@@ -58,14 +60,20 @@ local app = {
|
||||
|
||||
menuBar:Check(ID_CLEAROUTPUT, true)
|
||||
|
||||
-- load welcome.lua from myprograms/ if exists
|
||||
local fn = wx.wxFileName("myprograms/welcome.lua")
|
||||
if fn:FileExists() and
|
||||
(not ide.config.path.projectdir
|
||||
or string.len(ide.config.path.projectdir) == 0) then
|
||||
fn:Normalize() -- make absolute path
|
||||
LoadFile(fn:GetFullPath(),nil,true)
|
||||
ProjectUpdateProjectDir(fn:GetPath(wx.wxPATH_GET_VOLUME))
|
||||
-- load myprograms/welcome.lua if exists and no projectdir
|
||||
local projectdir = ide.config.path.projectdir
|
||||
if (not projectdir or string.len(projectdir) == 0
|
||||
or not wx.wxFileName(projectdir):DirExists()) then
|
||||
local home = wx.wxGetHomeDir():gsub("[\\/]$","")
|
||||
for _,dir in pairs({home, home.."/Desktop", ""}) do
|
||||
local fn = wx.wxFileName("myprograms/welcome.lua")
|
||||
-- normalize to absolute path
|
||||
if fn:Normalize(wx.wxPATH_NORM_ALL, dir) and fn:FileExists() then
|
||||
LoadFile(fn:GetFullPath(),nil,true)
|
||||
ProjectUpdateProjectDir(fn:GetPath(wx.wxPATH_GET_VOLUME))
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
@@ -77,7 +85,6 @@ local app = {
|
||||
settingsapp = "ZeroBraneStudio",
|
||||
settingsvendor = "ZeroBraneLLC",
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
editor.fontname = "Courier New"
|
||||
editor.caretline = true
|
||||
editor.showfncall = true
|
||||
editor.autotabs = false
|
||||
@@ -6,12 +5,15 @@ editor.usetabs = false
|
||||
editor.tabwidth = 2
|
||||
editor.usewrap = true
|
||||
|
||||
local G = ... -- this now points to the global environment
|
||||
if G.ide.osname == 'Macintosh' then filetree.fontsize = 11 end
|
||||
|
||||
filehistorylength = 20
|
||||
|
||||
singleinstance = true
|
||||
singleinstanceport = 0xe493
|
||||
|
||||
acandtip.shorttip = true
|
||||
acandtip.shorttip = false
|
||||
acandtip.nodynwords = true
|
||||
|
||||
activateoutput = true
|
||||
|
||||
@@ -6,6 +6,7 @@ local ide = ide
|
||||
|
||||
local frame = ide.frame
|
||||
local menuBar = frame.menuBar
|
||||
local mobdebug = require "mobdebug"
|
||||
|
||||
local helpMenu = wx.wxMenu{
|
||||
{ ID_ABOUT, "&About\tF1", "About ZeroBrane Studio" },
|
||||
@@ -54,7 +55,7 @@ local function DisplayAbout(event)
|
||||
</body>
|
||||
</html>]]
|
||||
|
||||
local dlg = wx.wxDialog(frame, wx.wxID_ANY, "About ZeroBane Studio")
|
||||
local dlg = wx.wxDialog(frame, wx.wxID_ANY, "About ZeroBrane Studio")
|
||||
local html = wx.wxLuaHtmlWindow(dlg, wx.wxID_ANY,
|
||||
wx.wxDefaultPosition, wx.wxSize(440, 270),
|
||||
wx.wxHW_SCROLLBAR_NEVER)
|
||||
@@ -72,11 +73,10 @@ local function DisplayAbout(event)
|
||||
topsizer:Add(html, 1, wx.wxALL, 10)
|
||||
topsizer:Add(line, 0, wx.wxEXPAND + wx.wxLEFT + wx.wxRIGHT, 10)
|
||||
topsizer:Add(button, 0, wx.wxALL + wx.wxALIGN_RIGHT, 10)
|
||||
topsizer:Fit(dlg)
|
||||
|
||||
dlg:SetAutoLayout(true)
|
||||
dlg:SetSizer(topsizer)
|
||||
topsizer:Fit(dlg)
|
||||
|
||||
dlg:ShowModal()
|
||||
dlg:Destroy()
|
||||
end
|
||||
|
||||
BIN
zbstudio/res/16/wxART_GO_FORWARD-wxART_OTHER_C.png
Normal file
BIN
zbstudio/res/16/wxART_GO_FORWARD-wxART_OTHER_C.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 758 B |
BIN
zbstudio/res/16/wxART_LIST_VIEW-wxART_OTHER_C.png
Normal file
BIN
zbstudio/res/16/wxART_LIST_VIEW-wxART_OTHER_C.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 783 B |
BIN
zbstudio/res/16/wxART_REPORT_VIEW-wxART_OTHER_C.png
Normal file
BIN
zbstudio/res/16/wxART_REPORT_VIEW-wxART_OTHER_C.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 792 B |
Reference in New Issue
Block a user