Complete With Readline |
|
Here is implementation of completion for the stand-alone Lua interpreter lua.
It is based on the GNU Readline library. The standard Lua distribution already
uses Readline optionally in the Makefile, by defining LUA_USE_READLINE.
But Readline is only used for editing input command and for the history
management, not the completion. To add this, lua.c has to install a callback
function in Readline library. To simplify the implementation, this C function
is just in charge to call a Lua function that will make the decisions depending
on the context, and to return the results in the format expected by Readline.
It is indeed very similar in features than Mike Pall's advanced readline patch on LuaPowerPatches. The difference is that it is written essentially in Lua, making it easier to customize for specific applications.
lua.c
#ifdef LUA_USE_READLINE
/* This function is called repeatedly by rl_completion_matches inside
do_completion, each time returning one element from the Lua table. */
static char* table_iterator (const char* text, int state)
{
size_t len;
const char* str;
char* result;
lua_State* L = globalL;
lua_rawgeti(L, -1, state+1);
if(lua_isnil(L, -1))
return NULL;
str = lua_tolstring(L, -1, &len);
result = malloc(len + 1);
strcpy(result, str);
lua_pop(L, 1);
return result;
}
/* This function is called by readline() when the user wants a completion.
It has to return an array of strings that match the word in the context.
do_completion is called with the word to complete and its position inside
the editing buffer. It then calls back Lua function "completion" to do the
hard work, which returns a table of matches. rl_completion_matches helps
in the process to find the common matching part, and to sort the array.
On any error, Lua stack is recovered and NULL is returned. */
static char** do_completion (const char* text, int istart, int iend)
{
char** matches = NULL;
lua_State* L = globalL;
int top;
if(L == NULL)
return NULL;
do {
top = lua_gettop(L);
lua_getglobal(L, "completion");
if(!lua_isfunction(L, -1))
break;
lua_pushstring(L, text);
lua_pushstring(L, rl_line_buffer);
lua_pushinteger(L, istart+1);
lua_pushinteger(L, iend+1);
if(lua_pcall(L, 4, 1, 0))
break;
if(!lua_istable(L, -1))
break;
matches = rl_completion_matches (text, table_iterator);
} while(0);
lua_settop(L, top);
return (matches);
}
/* Initialise Readline for completion. It should be called from main().*/
static void init_completion(lua_State* L)
{
rl_attempted_completion_function = do_completion;
/* This is a list of Lua operators, separating words. */
rl_basic_word_break_characters = " \t\n\"\\'><=;+-*/%^~#{}()[].,";
/* Inhibits the default added space on completed words. */
rl_completion_append_character = 0;
/* The completion Lua function is loaded here. The user might prefer
to "require" a module or incorporate it into an existing script. */
luaL_dofile(L, "complete.lua");
}
#endif
complete.lua
-- 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 completion(word, line, startpos, endpos) -- The complete list of Lua keywords local keywords = { 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while' } -- 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 -- 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. local function filename_list(str) local path, name = str:match("(.*)[\\/]+(.*)") path = (path or ".").."/" name = name or str for f in lfs.dir(path) do if (lfs.attributes(path..f) or {}).mode == 'directory' then add(f.."/") else add(f) end end end -- This function makes a guess of the next character following an identifier, -- based on the type of the value it holds. local function postfix(value) local t = type(value) if t == 'function' or (getmetatable(value) or {}).__call then return '(' elseif t == 'table' and #value > 0 then return '[' elseif t == 'table' then return '.' else return ' ' 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(keywords) do add(k..' ') end for k,v in pairs(_G) do add(k..postfix(v)) end 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 return filename_list(str) end if expr == nil or expr == "" then return add_globals() end local v = loadstring("return "..expr) if not v then return end v = v() local t = type(v) if sep == '.' then if t ~= 'table' then return end for k,v2 in pairs(v) do if type(k) == 'string' then add(k..postfix(v2)) end end elseif sep == '[' then if t ~= 'table' then return end for k,v2 in pairs(v) do if type(k) == 'number' then add(k.."]"..postfix(v2)) end end if word ~= "" then add_globals() end elseif sep == '(' then -- This is a great place to return the prototype of the function, -- in case your application has some mean to know it. -- The following is just a useless example: add("function "..expr.." (...)"..string.rep(" ", 40)) add("°") -- display trick 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. 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. -- You may have to complete it depending on your application. return curstring,expr:match("([%.%w%[%]_]-)([%.%[%(])"..word.."$") end -- Now calls the processing functions and returns the list of results. local str, expr, sep = simplify_expression(line:sub(1,endpos)) contextual_list(expr, sep, str) return matches end
/* build@ cc -shared readline.c -o readline.so
** Completely and absolutely minimal binding to the readline library
** Steve Donovan, 2007
** Merged with http://lua-users.org/wiki/CompleteWithReadline
** by Ulrik, 2010. I disclaim any copyright on my contributions.
*
* Enable completion in your lua REPL with:
* lua -lreadline -lcomplete
* where complete.lua defines a global function:
* completion (word, line, startpos, endpos) -> array of matches
*/
#include <stdlib.h>
#include <stdio.h>
#include <readline/readline.h>
#include <readline/history.h>
#define LUA_LIB
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
/* The readline library uses a lot of global variables,
* so we do too. Lua state for the completion function.
* I'm sorry for this.. */
static lua_State *stateL;
static int f_readline(lua_State* L)
{
const char* prompt = lua_tostring(L,1);
char *line = readline(prompt);
lua_pushstring(L, line);
/* readline return value must be free'd */
free(line);
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 const struct luaL_reg lib[] = {
{"readline", f_readline},
{"add_history",f_add_history},
{NULL, NULL},
};
/* This function is called repeatedly by rl_completion_matches inside
do_completion, each time returning one element from the Lua table. */
static char* table_iterator (const char* text, int state)
{
size_t len;
const char* str;
char* result;
lua_State* L = stateL;
lua_rawgeti(L, -1, state+1);
if(lua_isnil(L, -1))
return NULL;
str = lua_tolstring(L, -1, &len);
result = malloc(len + 1);
strcpy(result, str);
lua_pop(L, 1);
return result;
}
/* This function is called by readline() when the user wants a completion.
... */
static char** do_completion (const char* text, int istart, int iend)
{
char** matches = NULL;
lua_State* L = stateL;
int top;
if(L == NULL)
return NULL;
do {
top = lua_gettop(L);
lua_getglobal(L, "completion");
if(!lua_isfunction(L, -1))
break;
lua_pushstring(L, text);
lua_pushstring(L, rl_line_buffer);
lua_pushinteger(L, istart+1);
lua_pushinteger(L, iend+1);
if(lua_pcall(L, 4, 1, 0))
break;
if(!lua_istable(L, -1))
break;
matches = rl_completion_matches (text, table_iterator);
} while(0);
lua_settop(L, top);
rl_completion_suppress_append = 1;
return (matches);
}
/* Initialise Readline for completion. It should be called from main().*/
static int init_completion(lua_State* L)
{
stateL = L;
rl_attempted_completion_function = do_completion;
/* This is a list of Lua operators, separating words. */
rl_basic_word_break_characters = " \t\n\"\\'><=;+-*/%^~#{}()[].,";
/* Inhibits the default added space on completed words. */
rl_completion_append_character = '\0';
/* The completion Lua function is loaded here. The user might prefer
to "require" a module or incorporate it into an existing script. */
return 0;
}
int luaopen_readline (lua_State *L) {
luaL_openlib (L, "readline", lib, 0);
(void) init_completion(L);
return 1;
}