Complete With Readline

lua-users home
wiki

Showing revision 9

Description

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.

C code in 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

Lua Code 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
-- PatrickRapin

readline as standalone lua module

*If* your Lua interpreter is already compiled with readline, and you don't want to patch it, you can define a module that does all of the completion hooks setup itself. The following is just a very simple adaption of the previous code, then using the same 'complete.lua' for configuration. I cut down on some of the comments, since the code is partially exactly the same as above.

/* 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;
}
-- Ulrik 2010 apr
RecentChanges · preferences
edit · history · current revision
Edited September 1, 2010 12:07 pm GMT (diff)