diff options
author | Clement Farabet <cfarabet@twitter.com> | 2014-12-25 20:49:39 +0300 |
---|---|---|
committer | Clement Farabet <cfarabet@twitter.com> | 2014-12-25 20:49:39 +0300 |
commit | 396db0f71e96dd66983fcca8b7461f0e452dc7dd (patch) | |
tree | 1a57f8de20a9ecfa857beab94e3ad951d4751e75 | |
parent | 64f19ae831cf9690a71886e78b3976b7e8844144 (diff) |
Total revamp of readline support.
(now using plain C bindings to readline)
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | colorize.lua | 4 | ||||
-rw-r--r-- | completer.lua | 225 | ||||
-rw-r--r-- | init.lua | 398 | ||||
-rw-r--r-- | readline.c | 294 | ||||
-rw-r--r-- | readline.lua | 116 | ||||
-rw-r--r-- | th | 16 | ||||
-rw-r--r-- | trepl-scm-1.rockspec | 8 |
8 files changed, 421 insertions, 642 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d22eb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +*.so diff --git a/colorize.lua b/colorize.lua index 78b0620..0192397 100644 --- a/colorize.lua +++ b/colorize.lua @@ -2,9 +2,9 @@ local colors = require 'trepl.colors' local f = {} -for name,color in pairs(colors) do +for name in pairs(colors) do f[name] = function(txt) - return color .. txt .. colors.none + return colors[name] .. txt .. colors.none end end diff --git a/completer.lua b/completer.lua deleted file mode 100644 index 28d379a..0000000 --- a/completer.lua +++ /dev/null @@ -1,225 +0,0 @@ ---- Completion engine --- Compute possible matches for input text. --- Based on [lua-rlcompleter](https://github.com/rrthomas/lua-rlcompleter) by --- Patrick Rapin and Reuben Thomas. --- @alias M - -local lfs = pcall(require, "lfs") and require"lfs" -local cowrap, coyield = coroutine.wrap, coroutine.yield - -local M = { } - ---- The list of Lua keywords -M.keywords = { - 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', - 'function', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', - 'return', 'then', 'true', 'until', 'while' -} - ---- Callback function to set final character. --- Called to set the final chracter if a single match occur. Typically, it is used --- to close a string if completion occurs inside an open string. It is intended --- to be redefined as default function does nothing. --- @param[type=string] char Either an empty string or a string of length 1. -M.final_char_setter = function(char) end - ---- References all completion generators (completers). --- Completion is context sensitive and there is 2 *contexts* : values --- (@{completers.value}) and strings (@{completers.string}). Some arguments are --- given to the completer the first time it's called. --- --- Each context has its corresponding table (sequence) with *completers* inside. --- These tables works a bit like `package.loaders` : each loader is called as a --- coroutine and can generate possible matches (by yielding strings). But, unlike --- `package.loaders`, all completers are always called; even if matches has been --- generated by previous ones. --- --- It it not the completer job to filter matches with current text (this is done --- by the complettion engine), but just generate all possible matches. -M.completers = { } - ---- Completers for Lua values. --- Completers are called with two arguments : --- --- * `value` to complete (not necessarily a table) --- * `separator` used to index value (`.`, `:` or `[`) --- --- Default completers for values are : --- --- 1. Table field completer: searches for fields inside `value` if it's a table. --- 2. Metatable completer: if `value` has a metatable, calls first completer --- with that table. --- --- @table completers.value -M.completers.value = { } - ---- Completers for strings. --- Completers are called with the string to complete. --- If `lfs` is can be loaded, a completer for files and folders is provided, it --- search for items starting with given string in current directory. --- @table completers.string -M.completers.string = { } - --- Table fields completer -table.insert(M.completers.value, function(t, sep) - if type(t) ~= "table" then return end - for k, v in pairs(t) do - if type(k) == "number" and sep == "[" then - coyield(k.."]") - elseif type(k) == "string" and (sep ~= ":" or type(v) == "function") then - coyield(k) - end - end -end) - --- Metamethod completer -table.insert(M.completers.value, function(t, sep) - local mt = getmetatable(t) - if mt and type(mt.__index) == "table" then - return M.completers.value[1](mt.__index, sep) -- use regular table completer on metatable - end -end) - --- tensor/storage/torch-classes completer -table.insert(M.completers.value, function(t, sep) - local function enumerate_metatable(typename) - if typename == nil then return end - local metatable = torch.getmetatable(typename) - for k, v in pairs(metatable) do - if type(k) == "number" and sep == "[" then - coyield(k.."]") - elseif type(k) == "string" and (sep ~= ":" or type(v) == "function") then - coyield(k) - end - end - if torch.typename(metatable) ~= typename then - enumerate_metatable(torch.typename(metatable)) - end - end - enumerate_metatable(torch.typename(t)) -end) - - --- This function does the same job as the default completion of readline, --- completing paths and filenames. Rewritten because --- rl_basic_word_break_characters is different. --- Uses LuaFileSystem (lfs) module for this task (if present). -if lfs then - table.insert(M.completers.string, function(str) - local path, name = str:match("(.*)[\\/]+(.*)") - path = (path or ".") .. "/" - name = name or str - -- avoid to trigger an error if folder does not exists - if not lfs.attributes(path) then return end - for f in lfs.dir(path) do - if (lfs.attributes(path .. f) or {}).mode == 'directory' then - coyield(f .. "/") - else - coyield(f) - end - end - end) -end - --- This function is called back by C function do_completion, itself called --- back by readline library, in order to complete the current input line. -function M.complete(word, line, startpos, endpos) - -- Helper function registering possible completion words, verifying matches. - local matches = {} - local function add(value) - value = tostring(value) - if value:match("^" .. word) then - matches[#matches + 1] = value - end - end - - local function call_completors(completers, ...) - for _, completer in ipairs(completers) do - local coro = cowrap(completer) - local match = coro(...) -- first call => give parameters - if match then - add(match) - -- continue calling to get next matches - for match in coro do add(match) end - end - end - end - - -- This function is called in a context where a keyword or a global - -- variable can be inserted. Local variables cannot be listed! - local function add_globals() - for _, k in ipairs(M.keywords) do - add(k) - end - call_completors(M.completers.value, _G) - end - - -- Main completion function. It evaluates the current sub-expression - -- to determine its type. Currently supports tables fields, global - -- variables and function prototype completion. - local function contextual_list(expr, sep, str) - if str then - M.final_char_setter('"') - return call_completors(M.completers.string, str) - end - M.final_char_setter("") - if expr and expr ~= "" then - local v = loadstring("return " .. expr) - if v then - call_completors(M.completers.value, v(), sep) - end - end - if #matches == 0 then - add_globals() - end - end - - -- This complex function tries to simplify the input line, by removing - -- literal strings, full table constructors and balanced groups of - -- parentheses. Returns the sub-expression preceding the word, the - -- separator item ( '.', ':', '[', '(' ) and the current string in case - -- of an unfinished string literal. - local function simplify_expression(expr) - -- Replace annoying sequences \' and \" inside literal strings - expr = expr:gsub("\\(['\"])", function (c) - return string.format("\\%03d", string.byte(c)) - end) - local curstring - -- Remove (finished and unfinished) literal strings - while true do - local idx1, _, equals = expr:find("%[(=*)%[") - local idx2, _, sign = expr:find("(['\"])") - if idx1 == nil and idx2 == nil then - break - end - local idx, startpat, endpat - if (idx1 or math.huge) < (idx2 or math.huge) then - idx, startpat, endpat = idx1, "%[" .. equals .. "%[", "%]" .. equals .. "%]" - else - idx, startpat, endpat = idx2, sign, sign - end - if expr:sub(idx):find("^" .. startpat .. ".-" .. endpat) then - expr = expr:gsub(startpat .. "(.-)" .. endpat, " STRING ") - else - expr = expr:gsub(startpat .. "(.*)", function (str) - curstring = str - return "(CURSTRING " - end) - end - end - expr = expr:gsub("%b()"," PAREN ") -- Remove groups of parentheses - expr = expr:gsub("%b{}"," TABLE ") -- Remove table constructors - -- Avoid two consecutive words without operator - expr = expr:gsub("(%w)%s+(%w)","%1|%2") - expr = expr:gsub("%s+", "") -- Remove now useless spaces - -- This main regular expression looks for table indexes and function calls. - return curstring, expr:match("([%.%w%[%]_]-)([:%.%[%(])" .. word .. "$") - end - - -- Now call the processing functions and return the list of results. - local str, expr, sep = simplify_expression(line:sub(1, endpos)) - contextual_list(expr, sep, str) - return matches -end - -return M @@ -20,21 +20,30 @@ -- Require Torch pcall(require,'torch') pcall(require,'paths') +pcall(require,'sys') +pcall(require,'xlua') -- Colors: local colors = require 'trepl.colors' local col = require 'trepl.colorize' +-- Kill colors: +function noColors() + for k,v in pairs(colors) do + colors[k] = '' + end +end + -- Help string: local selfhelp = [[ - ______ __ - /_ __/__ ________/ / - / / / _ \/ __/ __/ _ \ - /_/ \___/_/ \__/_//_/ - -]]..col.red('th')..[[ is an enhanced interpreter (repl) for Torch7/LuaJIT. + ______ __ + /_ __/__ ________/ / + / / / _ \/ __/ __/ _ \ + /_/ \___/_/ \__/_//_/ + +]]..col.red('th')..[[ is an enhanced interpreter (repl) for Torch7/Lua. -]]..col.blue('Features:')..[[ +]]..col.blue('Features:')..[[ Tab-completion on nested namespaces Tab-completion on disk files (when opening a string) @@ -43,20 +52,20 @@ local selfhelp = [[ Auto-print after eval (can be stopped with ;) Each command is profiled, timing is reported No need for '=' to print - Easy help on functions/packages: - ]]..col.magenta("? torch.randn")..[[ - Shell commands with: - ]]..col.magenta("$ ls -l")..[[ + Easy help on functions/packages: + ]]..col.magenta("? torch.randn")..[[ + Shell commands with: + ]]..col.magenta("$ ls -l")..[[ Print all user globals with: - ]]..col.magenta("who()")..[[ + ]]..col.magenta("who()")..[[ Import a package's symbols globally with: - ]]..col.magenta("import 'torch' ")..[[ + ]]..col.magenta("import 'torch' ")..[[ Require is overloaded to provide relative (form within a file) search paths: - ]]..col.magenta("require './local/lib' ")..[[ + ]]..col.magenta("require './local/lib' ")..[[ Optional strict global namespace monitoring: - ]]..col.magenta('th -g')..[[ + ]]..col.magenta('th -g')..[[ Optional async repl (based on https://github.com/clementfarabet/async): - ]]..col.magenta('th -a')..[[ + ]]..col.magenta('th -a')..[[ ]] -- If no Torch: @@ -193,7 +202,7 @@ function print_new(...) for k,v in pairs(obj) do if type(v) == 'table' then if depth >= (ndepth-1) or next(v) == nil then - line(tostring(k) .. ' : {}') + line(tostring(k) .. ' : {...}') else line(tostring(k) .. ' : ') printrecursive(v,depth+1) end @@ -267,24 +276,50 @@ function require(name) if name:find('^%.') then local file = debug.getinfo(2).source:gsub('^@','') local dir = '.' - if path.exists(file) then - dir = path.dirname(file) + if paths.filep(file) then + dir = paths.dirname(file) end - local pkgpath = path.join(dir,name) - if path.isfile(pkgpath..'.lua') then + local pkgpath = paths.concat(dir,name) + if paths.filep(pkgpath..'.lua') then return dofile(pkgpath..'.lua') - elseif path.isfile(pkgpath) then + elseif pkgpath:find('%.th$') and paths.filep(pkgpath) then + return torch.load(pkgpath) + elseif pkgpath:find('%.net$') and paths.filep(pkgpath) then + require 'nn' + return torch.load(pkgpath) + elseif pkgpath:find('%.json$') and paths.filep(pkgpath) then + return require('cjson').decode(io.open(pkgpath):read('*all')) + elseif pkgpath:find('%.csv$') and paths.filep(pkgpath) then + return require('csv').load(pkgpath) + elseif paths.filep(pkgpath) then return dofile(pkgpath) - elseif path.isfile(pkgpath..'.so') then + elseif paths.filep(pkgpath..'.th') then + return torch.load(pkgpath..'.th') + elseif paths.filep(pkgpath..'.net') then + require 'nn' + return torch.load(pkgpath..'.net') + elseif paths.filep(pkgpath..'.json') then + return require('cjson').decode(io.open(pkgpath..'.json'):read('*all')) + elseif paths.filep(pkgpath..'.csv') then + return require('csv').load(pkgpath..'.csv') + elseif paths.filep(pkgpath..'.so') then return package.loadlib(pkgpath..'.so', 'luaopen_'..path.basename(name))() - elseif path.isfile(pkgpath..'.dylib') then + elseif paths.filep(pkgpath..'.dylib') then return package.loadlib(pkgpath..'.dylib', 'luaopen_'..path.basename(name))() else - local initpath = path.join(pkgpath,'init.lua') + local initpath = paths.concat(pkgpath,'init.lua') return dofile(initpath) end else - return drequire(name) + local ok,res = pcall(drequire,name) + if not ok then + local ok2,res2 = pcall(require,'./'..name) + if not ok2 then + error(res) + end + return res2 + end + return res end end @@ -422,274 +457,41 @@ local aliases = [[ -- Penlight pcall(require,'pl') +-- Useful globally, from penlight +if text then + text.format_operator() +end + -- Reults: _RESULTS = {} _LAST = '' -- Readline: -local readline_ok,readline = pcall(require,"trepl.readline") -if not readline_ok then - print(col.red('WARNING: ') .. 'could not find/load readline, defaulting to linenoise') -end - --- REPL: -function repl_readline() - -- Completer: - local completer = require 'trepl.completer' - completer.final_char_setter = readline.completion_append_character - - local inputrc = paths.concat(os.getenv('HOME'),'.inputrc') - if not paths.filep(inputrc) then - local finputrc = io.open(inputrc,'w') - local trepl = -[[ -$if lua - # filter up and down arrows using characters typed so far - "\e[A":history-search-backward - "\e[B":history-search-forward -$endif -]] - finputrc:write(trepl) - finputrc:close() +local readline_ok,readline = pcall(require,'readline') +local nextline,saveline +if readline_ok then + -- Readline found: + local history = os.getenv('HOME') .. '/.luahistory' + readline.setup() + readline.read_history(history) + nextline = function(aux) + return readline.readline(prompt(aux)) end - - -- Timer - local timer_start, timer_stop - if torch and torch.Timer then - local t = torch.Timer() - local start = 0 - timer_start = function() - start = t:time().real - end - timer_stop = function() - local step = t:time().real - start - for i = 1,70 do io.write(' ') end - print(col.Black(string.format('[%0.04fs]', step))) - end - else - timer_start = function() end - timer_stop = function() end + saveline = function(line) + readline.add_history(line) + readline.write_history(history) end - - -- History: - local history = os.getenv('HOME') .. '/.luahistory' - - -- Readline callback: - readline.shell{ - -- History: - history = history, - - -- Completer: - complete = completer.complete, - - -- Chars: - word_break_characters = " \t\n\"\\'><=;:+-*/%^~#{}()[].,", - - -- Get command: - getcommand = function() - -- get the first line - local line = coroutine.yield(prompt()) - local cmd = line .. '\n' - - -- = (lua supports that) - if cmd:sub(1,1) == "=" then - cmd = "return "..cmd:sub(2) - end - - -- Interupt? - if line == 'exit' then - io.stdout:write('Do you really want to exit ([y]/n)? ') io.flush() - local line = io.read('*l') - if line == '' or line:lower() == 'y' then - os.exit() - end - end - - -- OS Commands: - if line and line:find('^%s-%$') then - local cline = line:gsub('^%s-%$','') - if io.popen then - local f = io.popen(aliases .. ' ' .. cline) - local res = f:read('*a') - f:close() - io.write(col.none(res)) io.flush() - table.insert(_RESULTS, res) - _LAST = _RESULTS[#_RESULTS] - else - os.execute(aliases .. ' ' .. cline) - end - timer_stop() - return line - end - - -- Shortcut to get help: - if line and line:find('^%s-?') then - local ok = pcall(require,'dok') - if ok then - local pkg = line:gsub('^%s-?','') - if pkg:gsub('%s*','') == '' then - print(selfhelp) - return - else - line = 'help(' .. pkg .. ')' - end - else - print('error: could not load help backend') - return line - end - end - - -- try to return first: - timer_start() - local pok,ok,err - if line:find(';%s-$') or line:find('^%s-print') then - ok = false - elseif line:match('^%s*$') then - return nil - else - local func, perr = loadstring('local f = function() return '..line..' end local res = {f()} print(unpack(res)) table.insert(_RESULTS,res[1])') - if func then - pok = true - ok,err = xpcall(func, traceback) - end - end - - -- run ok: - if ok then - _LAST = _RESULTS[#_RESULTS] - timer_stop() - return line - end - - -- parsed ok, but failed to run (code error): - if pok then - print(err) - return cmd:sub(1, -2) - end - - -- continue to get lines until get a complete chunk - local func, err - while true do - -- if not go ahead: - func, err = loadstring(cmd) - if func or err:sub(-7) ~= "'<eof>'" then break end - - -- concat: - cmd = cmd .. coroutine.yield(prompt(true)) .. '\n' - end - - -- exec chunk: - if not cmd:match("^%s*$") then - local ff,err=loadstring(cmd) - if not ff then - print(err) - return cmd:sub(1, -2) - end - local res = {xpcall(ff, traceback)} - local ok,err = res[1], res[2] - if not ok then - print(err) - else - if err ~= nil then - table.remove(res,1) - print(unpack(res)) - end - end - timer_stop() - return cmd:sub(1, -2) -- remove last \n for history - end - end, - } - io.stderr:write"\n" -end - --- No readline -> LineNoise? -local nextline -if not readline_ok then - -- Load linenoise: - local ok,L = pcall(require,'linenoise') - ok = false - if not ok then - -- No readline, no linenoise... default to plain io: - nextline = function() - io.write(prompt()) io.flush() - return io.read('*line') - end - - -- Really poor: - print(col.red('WARNING: ') .. 'could not find/load linenoise, defaulting to raw repl') - else - -- History: - local history = os.getenv('HOME') .. '/.luahistory' - L.historyload(history) - - -- Completion: - L.setcompletion(function(c,s) - -- Check if we're in a string - local ignore,str = s:gfind('(.-)"([a-zA-Z%._]*)$')() - local quote = '"' - if not str then - ignore,str = s:gfind('(.-)\'([a-zA-Z%._]*)$')() - quote = "'" - end - - -- String? - if str then - -- Complete from disk: - local f = io.popen('ls ' .. str..'* 2> /dev/null') - local res = f:read('*all') - f:close() - res = res:gsub('(%s*)$','') - local elts = stringx.split(res,'\n') - for _,elt in ipairs(elts) do - L.addcompletion(c,ignore .. quote .. elt) - end - return - end - - -- Get symbol of interest - local ignore,str = s:gfind('(.-)([a-zA-Z%._]*)$')() - - -- Lookup globals: - if not str:find('%.') then - for k,v in pairs(_G) do - if k:find('^'..str) then - L.addcompletion(c,ignore .. k) - end - end - end - - -- Lookup packages: - local base,sub = str:gfind('(.*)%.(.*)')() - if base then - local ok,res = pcall(loadstring('return ' .. base)) - for k,v in pairs(res) do - if k:find('^'..sub) then - L.addcompletion(c,ignore .. base .. '.' .. k) - end - end - end - end) - - -- read line: - nextline = function() - -- Get line: - local line = L.linenoise(prompt()) - - -- Save: - if line and not line:find('^%s-$') then - L.historyadd(line) - L.historysave(history) - end - - -- Return line: - return line - end +else + -- No readline... default to plain io: + nextline = function(aux) + io.write(prompt(aux)) io.flush() + return io.read('*line') end + saveline = function() end end --- The default repl -function repl_linenoise() +-- The repl +function repl() -- Timer local timer_start, timer_stop if torch and torch.Timer then @@ -758,6 +560,18 @@ function repl_linenoise() -- EVAL: if line then + -- Try to load line first, for multiline support: + local valid = loadstring('return ' .. line) or loadstring(line) + while not valid do + local nline = nextline(true) + if nline == '' or not nline then + break + end + line = line .. '\n' .. nline + valid = loadstring(line) + end + + -- Execute: timer_start() local ok,err if line:find(';%s-$') or line:find('^%s-print') then @@ -766,9 +580,14 @@ function repl_linenoise() ok,err = xpcall(loadstring('local f = function() return '..line..' end local res = {f()} print(unpack(res)) table.insert(_RESULTS,res[1])'), traceback) end if not ok then - local ok,err = xpcall(loadstring(line), traceback) - if not ok then - print(err) + local parsed,perr = loadstring(line) + if not parsed then + print('syntax error: ' .. perr) + else + local ok,err = xpcall(parsed, traceback) + if not ok then + print(err) + end end end timer_stop() @@ -776,6 +595,9 @@ function repl_linenoise() -- Last result: _LAST = _RESULTS[#_RESULTS] + + -- Save: + saveline(line) end end @@ -786,4 +608,4 @@ for k,v in pairs(_G) do end -- return repl, just call it to start it! -return (readline_ok and repl_readline) or repl_linenoise +return repl diff --git a/readline.c b/readline.c new file mode 100644 index 0000000..9edbef2 --- /dev/null +++ b/readline.c @@ -0,0 +1,294 @@ +// Bindings to readline +#include <stdlib.h> +#include <string.h> +#include "lua.h" +#include "lauxlib.h" +#include "lualib.h" +#include <readline/readline.h> +#include <readline/history.h> + +/* +** Lua 5.1.4 advanced readline support for the GNU readline and history +** libraries or compatible replacements. +** +** Author: Mike Pall. +** Maintainer: Sean Bolton (sean at smbolton dot com). +** +** Copyright (C) 2004-2006, 2011 Mike Pall. Same license as Lua. See lua.h. +** +** Advanced features: +** - Completion of keywords and global variable names. +** - Recursive and metatable-aware completion of variable names. +** - Context sensitive delimiter completion. +** - Save/restore of the history to/from a file (LUA_HISTORY env variable). +** - Setting a limit for the size of the history (LUA_HISTSIZE env variable). +** - Setting the app name to allow for $if lua ... $endif in ~/.inputrc. +** +** Start lua and try these (replace ~ with the TAB key): +** +** ~~ +** fu~foo() ret~fa~end<CR> +** io~~~s~~~o~~~w~"foo\n")<CR> +** +** The ~~ are just for demonstration purposes (io~s~o~w~ suffices, of course). +** +** If you are used to zsh/tcsh-style completion support, try adding +** 'TAB: menu-complete' and 'C-d: possible-completions' to your ~/.inputrc. +** +** The patch has been successfully tested with: +** +** GNU readline 2.2.1 (1998-07-17) +** GNU readline 4.0 (1999-02-18) [harmless compiler warning] +** GNU readline 4.3 (2002-07-16) +** GNU readline 5.0 (2004-07-27) +** GNU readline 5.1 (2005-12-07) +** GNU readline 5.2 (2006-10-11) +** GNU readline 6.0 (2009-02-20) +** GNU readline 6.2 (2011-02-13) +** MacOSX libedit 2.11 (2008-07-12) +** NETBSD libedit 2.6.5 (2002-03-25) +** NETBSD libedit 2.6.9 (2004-05-01) +** +** Change Log: +** 2004-2006 Mike Pall - original patch +** 2009/08/24 Sean Bolton - updated for GNU readline version 6 +** 2011/12/14 Sean Bolton - fixed segfault when using Mac OS X libedit 2.11 +*/ + +static char *lua_rl_hist; +static int lua_rl_histsize; + +static lua_State *lua_rl_L; /* User data is not passed to rl callbacks. */ + +/* Reserved keywords. */ +static const char *const lua_rl_keywords[] = { + "and", "break", "do", "else", "elseif", "end", "false", + "for", "function", "if", "in", "local", "nil", "not", "or", + "repeat", "return", "then", "true", "until", "while", NULL +}; + +static int valididentifier(const char *s) +{ + if (!(isalpha(*s) || *s == '_')) return 0; + for (s++; *s; s++) if (!(isalpha(*s) || isdigit(*s) || *s == '_')) return 0; + return 1; +} + +/* Dynamically resizable match list. */ +typedef struct { + char **list; + size_t idx, allocated, matchlen; +} dmlist; + +/* Add prefix + string + suffix to list and compute common prefix. */ +static int lua_rl_dmadd(dmlist *ml, const char *p, size_t pn, const char *s, + int suf) +{ + char *t = NULL; + + if (ml->idx+1 >= ml->allocated && + !(ml->list = realloc(ml->list, sizeof(char *)*(ml->allocated += 32)))) + return -1; + + if (s) { + size_t n = strlen(s); + if (!(t = (char *)malloc(sizeof(char)*(pn+n+(suf?2:1))))) return 1; + memcpy(t, p, pn); + memcpy(t+pn, s, n); + n += pn; + t[n] = suf; + if (suf) t[++n] = '\0'; + + if (ml->idx == 0) { + ml->matchlen = n; + } else { + size_t i; + for (i = 0; i < ml->matchlen && i < n && ml->list[1][i] == t[i]; i++) ; + ml->matchlen = i; /* Set matchlen to common prefix. */ + } + } + + ml->list[++ml->idx] = t; + return 0; +} + +/* Get __index field of metatable of object on top of stack. */ +static int lua_rl_getmetaindex(lua_State *L) +{ + if (!lua_getmetatable(L, -1)) { lua_pop(L, 1); return 0; } + + /* prefer __metatable if it exists */ + lua_pushstring(L, "__metatable"); + lua_rawget(L, -2); + if(lua_istable(L, -1)) + { + lua_remove(L, -2); + return 1; + } + else + lua_pop(L, 1); + + lua_pushstring(L, "__index"); + lua_rawget(L, -2); + lua_replace(L, -2); + if (lua_isnil(L, -1) || lua_rawequal(L, -1, -2)) { lua_pop(L, 2); return 0; } + lua_replace(L, -2); + return 1; +} /* 1: obj -- val, 0: obj -- */ + +/* Get field from object on top of stack. Avoid calling metamethods. */ +static int lua_rl_getfield(lua_State *L, const char *s, size_t n) +{ + int i = 20; /* Avoid infinite metatable loops. */ + do { + if (lua_istable(L, -1)) { + lua_pushlstring(L, s, n); + lua_rawget(L, -2); + if (!lua_isnil(L, -1)) { lua_replace(L, -2); return 1; } + lua_pop(L, 1); + } + } while (--i > 0 && lua_rl_getmetaindex(L)); + lua_pop(L, 1); + return 0; +} /* 1: obj -- val, 0: obj -- */ + +/* Completion callback. */ +static char **lua_rl_complete(const char *text, int start, int end) +{ + lua_State *L = lua_rl_L; + dmlist ml; + const char *s; + size_t i, n, dot, loop; + int savetop; + + if (!(text[0] == '\0' || isalpha(text[0]) || text[0] == '_')) return NULL; + + ml.list = NULL; + ml.idx = ml.allocated = ml.matchlen = 0; + + savetop = lua_gettop(L); + lua_pushvalue(L, LUA_GLOBALSINDEX); + for (n = (size_t)(end-start), i = dot = 0; i < n; i++) + if (text[i] == '.' || text[i] == ':') { + if (!lua_rl_getfield(L, text+dot, i-dot)) + goto error; /* Invalid prefix. */ + dot = i+1; /* Points to first char after dot/colon. */ + } + + /* Add all matches against keywords if there is no dot/colon. */ + if (dot == 0) + for (i = 0; (s = lua_rl_keywords[i]) != NULL; i++) + if (!strncmp(s, text, n) && lua_rl_dmadd(&ml, NULL, 0, s, ' ')) + goto error; + + /* Add all valid matches from all tables/metatables. */ + loop = 0; /* Avoid infinite metatable loops. */ + do { + if (lua_istable(L, -1) && + (loop == 0 || !lua_rawequal(L, -1, LUA_GLOBALSINDEX))) + for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) + if (lua_type(L, -2) == LUA_TSTRING) { + s = lua_tostring(L, -2); + /* Only match names starting with '_' if explicitly requested. */ + if (!strncmp(s, text+dot, n-dot) && valididentifier(s) && + (*s != '_' || text[dot] == '_')) { + int suf = ' '; /* Default suffix is a space. */ + switch (lua_type(L, -1)) { + case LUA_TTABLE: suf = '.'; break; /* No way to guess ':'. */ + case LUA_TFUNCTION: suf = '('; break; + case LUA_TUSERDATA: + if (lua_getmetatable(L, -1)) { lua_pop(L, 1); suf = ':'; } + break; + } + if (lua_rl_dmadd(&ml, text, dot, s, suf)) goto error; + } + } + } while (++loop < 20 && lua_rl_getmetaindex(L)); + + if (ml.idx == 0) { +error: + lua_settop(L, savetop); + return NULL; + } else { + /* list[0] holds the common prefix of all matches (may be ""). */ + /* If there is only one match, list[0] and list[1] will be the same. */ + if (!(ml.list[0] = (char *)malloc(sizeof(char)*(ml.matchlen+1)))) + goto error; + memcpy(ml.list[0], ml.list[1], ml.matchlen); + ml.list[0][ml.matchlen] = '\0'; + /* Add the NULL list terminator. */ + if (lua_rl_dmadd(&ml, NULL, 0, NULL, 0)) goto error; + } + + lua_settop(L, savetop); +#if RL_READLINE_VERSION >= 0x0600 + rl_completion_suppress_append = 1; +#endif + return ml.list; +} + +/* Initialize readline library. */ +static int f_setup(lua_State *L) +{ + char *s; + + lua_rl_L = L; + + /* This allows for $if lua ... $endif in ~/.inputrc. */ + rl_readline_name = "lua"; + /* Break words at every non-identifier character except '.' and ':'. */ + rl_completer_word_break_characters = + "\t\r\n !\"#$%&'()*+,-/;<=>?@[\\]^`{|}~"; + rl_completer_quote_characters = "\"'"; +/* #if RL_READLINE_VERSION < 0x0600 */ + rl_completion_append_character = '\0'; +/* #endif */ + rl_attempted_completion_function = lua_rl_complete; + rl_initialize(); + + return 0; +} + +static int f_readline(lua_State* L) +{ + const char* prompt = lua_tostring(L,1); + const char* line = readline(prompt); + lua_pushstring(L,line); + free((void *)line); // Lua makes a copy... + return 1; +} + +static int f_add_history(lua_State* L) +{ + if (lua_strlen(L,1) > 0) + add_history(lua_tostring(L, 1)); + return 0; +} + +static int f_write_history(lua_State* L) +{ + if (lua_strlen(L,1) > 0) + write_history(lua_tostring(L, 1)); + return 0; +} + +static int f_read_history(lua_State* L) +{ + if (lua_strlen(L,1) > 0) + read_history(lua_tostring(L, 1)); + return 0; +} + +static const struct luaL_reg lib[] = { + {"readline", f_readline}, + {"add_history",f_add_history}, + {"write_history",f_write_history}, + {"read_history",f_read_history}, + {"setup",f_setup}, + {NULL, NULL}, +}; + +int luaopen_readline (lua_State *L) { + luaL_openlib (L, "readline", lib, 0); + return 1; +} diff --git a/readline.lua b/readline.lua deleted file mode 100644 index 2c343c8..0000000 --- a/readline.lua +++ /dev/null @@ -1,116 +0,0 @@ --- Very basic FFI interface to readline, --- with history saving/restoring --- -local ffi = require 'ffi' -local assert = assert -local cocreate, coresume, costatus = coroutine.create, coroutine.resume, coroutine.status -local readline = {} - -ffi.cdef[[ -/* libc definitions */ -void* malloc(size_t bytes); -void free(void *); - -/* basic history handling */ -char *readline (const char *prompt); -void add_history(const char *line); -int read_history(const char *filename); -int write_history(const char *filename); - -void rl_set_signals(void); - -/* completion */ -typedef char **rl_completion_func_t (const char *, int, int); -typedef char *rl_compentry_func_t (const char *, int); - -char **rl_completion_matches (const char *, rl_compentry_func_t *); - -const char *rl_basic_word_break_characters; -const char *rl_completer_quote_characters; -rl_completion_func_t *rl_attempted_completion_function; -rl_completion_func_t *rl_completion_entry_function; -char *rl_line_buffer; -int rl_completion_append_character; -int rl_completion_suppress_append; -int rl_attempted_completion_over; -const char *rl_readline_name; - -int rl_initialize(); -]] - -local libreadline = ffi.load("readline") - --- enable application specific parsing with inputrc -libreadline.rl_readline_name = 'lua' - -function readline.completion_append_character(char) - libreadline.rl_completion_append_character = #char > 0 and char:byte(1,1) or 0 -end - -if jit.os ~= 'OSX' then - --libreadline.rl_set_signals() -end - -function readline.shell(config) - -- restore history - libreadline.read_history(config.history) - - -- configure completion, if any - if config.complete then - if config.word_break_characters then - libreadline.rl_basic_word_break_characters = config.word_break_characters - end - libreadline.rl_completer_quote_characters = '\'"' - - local matches - libreadline.rl_completion_entry_function = function(word, i) - libreadline.rl_attempted_completion_over = 1 - local strword = ffi.string(word) - local buffer = ffi.string(libreadline.rl_line_buffer) - if i == 0 then - matches = config.complete(strword, buffer, startpos, endpos) - end - local match = matches[i+1] - if match then - -- readline will free the C string by itself, so create copies of them - local buf = ffi.C.malloc(#match + 1) - ffi.copy(buf, match, #match+1) - return buf - end - end - end - - -- main loop - local running = true - while running do - local userfunc = cocreate(config.getcommand) - local _, prompt = assert(coresume(userfunc)) - while costatus(userfunc) ~= "dead" do - -- get next line - local s = libreadline.readline(prompt) - if s == nil then -- end of file - running = false - break - end - - local line = ffi.string(s) - ffi.C.free(s) - _, prompt = assert(coresume(userfunc, line)) - end - - if not running then - io.stdout:write('\nDo you really want to exit ([y]/n)? ') io.flush() - local line = io.read('*l') - if line == '' or line:lower() == 'y' then - os.exit() - else - readline.shell(config) - end - elseif prompt then -- final return value is the value to add to history - libreadline.add_history(prompt) - libreadline.write_history(config.history) - end - end -end - -return readline @@ -95,7 +95,9 @@ if lgfx then local ok = pcall(require, 'gfx.js') if ok then gfx.startserver() + gfx.clear() gfx.show() + sys.sleep(1) else print('could not load gfx.js, please install with: luarocks install gfx.js') end @@ -136,20 +138,20 @@ if asyncrepl then -- verbose print( [[ - + ______ __ ]]..col.Black[[| Torch7 ]]..[[ - /_ __/__ ________/ / ]]..col.Black[[| ]]..col.magenta[[Scientific computing for LuaJIT. ]]..[[ + /_ __/__ ________/ / ]]..col.Black[[| ]]..col.magenta[[Scientific computing for Lua. ]]..[[ / / / _ \/ __/ __/ _ \ ]]..col.Black[[| ]]..[[ /_/ \___/_/ \__/_//_/ ]]..col.Black[[| ]]..col.blue[[https://github.com/torch ]]..[[ ]]..col.Black[[| ]]..col.blue[[http://torch.ch ]]..[[ - + ]] .. col.red('WARNING: ') .. col.Black('you are running an experimental asynchronous interpreter for Torch.') .. [[ ]] .. col.Black('Note 1: It has no support for readline/completion yet.') .. [[ ]] .. col.Black('Note 2: The event-loop has been started in the background: ') .. col.none('async.go()') .. [[ ]] .. col.Black(' Statements like ') .. col.none('async.setInterval(1000, function() print("test") end)') .. [[ -]] .. col.Black(' are run asynchronously to the interpreter. ') .. [[ -]] .. col.Black('Note 3: See ') .. col.blue('http://github.com/clementfarabet/async') .. col.Black(' for help' ) .. [[ - +]] .. col.Black(' are run asynchronously to the interpreter. ') .. [[ +]] .. col.Black('Note 3: See ') .. col.blue('http://github.com/clementfarabet/async') .. col.Black(' for help' ) .. [[ + ]] ) @@ -164,7 +166,7 @@ else [[ ______ __ ]]..col.Black[[| Torch7 ]]..[[ - /_ __/__ ________/ / ]]..col.Black[[| ]]..col.magenta[[Scientific computing for LuaJIT. ]]..[[ + /_ __/__ ________/ / ]]..col.Black[[| ]]..col.magenta[[Scientific computing for Lua. ]]..[[ / / / _ \/ __/ __/ _ \ ]]..col.Black[[| ]]..[[ /_/ \___/_/ \__/_//_/ ]]..col.Black[[| ]]..col.blue[[https://github.com/torch ]]..[[ ]]..col.Black[[| ]]..col.blue[[http://torch.ch ]]..[[ diff --git a/trepl-scm-1.rockspec b/trepl-scm-1.rockspec index 8f3ae8c..49d29f8 100644 --- a/trepl-scm-1.rockspec +++ b/trepl-scm-1.rockspec @@ -17,9 +17,7 @@ An embedabble, Lua-only REPL for Torch. dependencies = { "torch >= 7.0", - "linenoise >= 0.4", "penlight >= 1.1.0", - "luafilesystem >= 1.6.2" } build = { @@ -28,8 +26,10 @@ build = { ['trepl.init'] = 'init.lua', ['trepl.colors'] = 'colors.lua', ['trepl.colorize'] = 'colorize.lua', - ['trepl.readline'] = 'readline.lua', - ['trepl.completer'] = 'completer.lua', + ['readline'] = { + sources = {'readline.c'}, + libraries = {'readline'} + } }, install = { bin = { |