From 64b106c13e18c33be0f2b0de532054e0ed3f731d Mon Sep 17 00:00:00 2001 From: jacqueline Date: Wed, 13 Dec 2023 16:10:08 +1100 Subject: add a cool lua repl --- lib/lua-repl/repl/compat.lua | 30 ++ lib/lua-repl/repl/console.lua | 57 +++ lib/lua-repl/repl/init.lua | 416 +++++++++++++++++++++ lib/lua-repl/repl/plugins/autoreturn.lua | 29 ++ lib/lua-repl/repl/plugins/completion.lua | 192 ++++++++++ lib/lua-repl/repl/plugins/example.lua | 41 ++ lib/lua-repl/repl/plugins/filename_completion.lua | 63 ++++ lib/lua-repl/repl/plugins/history.lua | 60 +++ lib/lua-repl/repl/plugins/keep_last_eval.lua | 43 +++ lib/lua-repl/repl/plugins/linenoise.lua | 59 +++ lib/lua-repl/repl/plugins/pretty_print.lua | 262 +++++++++++++ lib/lua-repl/repl/plugins/rcfile.lua | 54 +++ lib/lua-repl/repl/plugins/rlwrap.lua | 41 ++ .../repl/plugins/semicolon_suppress_output.lua | 36 ++ lib/lua-repl/repl/sync.lua | 46 +++ lib/lua-repl/repl/utils.lua | 70 ++++ 16 files changed, 1499 insertions(+) create mode 100644 lib/lua-repl/repl/compat.lua create mode 100644 lib/lua-repl/repl/console.lua create mode 100644 lib/lua-repl/repl/init.lua create mode 100644 lib/lua-repl/repl/plugins/autoreturn.lua create mode 100644 lib/lua-repl/repl/plugins/completion.lua create mode 100644 lib/lua-repl/repl/plugins/example.lua create mode 100644 lib/lua-repl/repl/plugins/filename_completion.lua create mode 100644 lib/lua-repl/repl/plugins/history.lua create mode 100644 lib/lua-repl/repl/plugins/keep_last_eval.lua create mode 100644 lib/lua-repl/repl/plugins/linenoise.lua create mode 100644 lib/lua-repl/repl/plugins/pretty_print.lua create mode 100644 lib/lua-repl/repl/plugins/rcfile.lua create mode 100644 lib/lua-repl/repl/plugins/rlwrap.lua create mode 100644 lib/lua-repl/repl/plugins/semicolon_suppress_output.lua create mode 100644 lib/lua-repl/repl/sync.lua create mode 100644 lib/lua-repl/repl/utils.lua (limited to 'lib/lua-repl/repl') diff --git a/lib/lua-repl/repl/compat.lua b/lib/lua-repl/repl/compat.lua new file mode 100644 index 00000000..fd47748c --- /dev/null +++ b/lib/lua-repl/repl/compat.lua @@ -0,0 +1,30 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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. + +return { + -- unpack was moved to table.unpack on Lua version 5.2 + -- See https://www.lua.org/manual/5.2/manual.html#8 + unpack = unpack or table.unpack, + -- loadstring was deprecated in favor of load, which was updated + -- to handle string arguments + loadstring = loadstring or load, + -- package.loaders was renamed package.searchers in Lua 5.2 + package = { + searchers = package.loaders or package.searchers + } +} diff --git a/lib/lua-repl/repl/console.lua b/lib/lua-repl/repl/console.lua new file mode 100644 index 00000000..154d0372 --- /dev/null +++ b/lib/lua-repl/repl/console.lua @@ -0,0 +1,57 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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. + +-- @class repl.console +--- This module implements a command line-based REPL, +--- similar to the standalone Lua interpreter. + +local sync_repl = require 'repl.sync' +local compat = require 'repl.compat' +local console_repl = sync_repl:clone() +local stdout = io.stdout +local stdin = io.stdin +local print = print +local unpack = unpack + +-- @see repl:showprompt(prompt) +function console_repl:showprompt(prompt) + stdout:write(prompt .. ' ') +end + +-- @see repl.sync:lines() +function console_repl:lines() + return stdin:lines() +end + +-- @see repl:displayresults(results) +function console_repl:displayresults(results) + if results.n == 0 then + return + end + + print(compat.unpack(results, 1, results.n)) +end + +-- @see repl:displayerror(err) +function console_repl:displayerror(err) + print(err) +end + +console_repl._features.console = true + +return console_repl diff --git a/lib/lua-repl/repl/init.lua b/lib/lua-repl/repl/init.lua new file mode 100644 index 00000000..b5499f59 --- /dev/null +++ b/lib/lua-repl/repl/init.lua @@ -0,0 +1,416 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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. + +-- @class repl +--- This module implements the core functionality of a REPL. + +local plugins_lookup_meta = { __mode = 'k' } + +local repl = { _buffer = '', _plugins = setmetatable({}, plugins_lookup_meta), _features = {}, _ifplugin = {}, _iffeature = {}, VERSION = 0.10 } +local compat = require 'repl.compat' +local select = select +local dtraceback = debug.traceback +local setmetatable = setmetatable +local sformat = string.format +local smatch = string.match +local error = error +local setfenv = require('repl.utils').setfenv + +local function gather_results(success, ...) + local n = select('#', ...) + return success, { n = n, ... } +end + +local function tcopy(t, copy) + copy = copy or {} + + for k, v in pairs(t) do + copy[k] = v + end + + return copy +end + +--- Returns the prompt for a given level. +-- @param level The prompt level. Either 1 or 2. +function repl:getprompt(level) + return level == 1 and '>' or '>>' +end + +--- Displays a prompt for the given prompt level. +-- @param level The prompt level. Either 1 or 2. +function repl:prompt(level) + self:showprompt(self:getprompt(level)) +end + +--- Returns the name of the REPL. For usage in chunk compilation. +-- @return The REPL's name. +-- @see load +function repl:name() + return 'REPL' +end + +--- Gets a traceback for an error. +-- @param ... All of the stuff that xpcall passes to error functions. +-- @see xpcall +-- @return A stack trace. The default implementation returns a simple string-based trace. +function repl:traceback(...) + return dtraceback(...) +end + +--- Uses the compilation error to determine whether or not further input +--- is pending after the last line. You shouldn't have to override this +--- unless you use an implementation of Lua that varies in its error +--- messages. +-- @param err The compilation error from Lua. +-- @return Whether or not the input should continue after this line. +function repl:detectcontinue(err) + return smatch(err, "''$") or smatch(err, "$") +end + +function repl:compilechunk(chunk) + return compat.loadstring(chunk, self:name()) +end + +--- Evaluates a line of input, and displays return value(s). +-- @param line The line to evaluate +-- @return The prompt level (1 or 2) +function repl:handleline(line) + local chunk = self._buffer .. line + local f, err = self:compilechunk(chunk) + + if f then + self._buffer = '' + + setfenv(f, self:getcontext()) + local success, results = gather_results(xpcall(f, function(...) return self:traceback(...) end)) + if success then + self:displayresults(results) + else + self:displayerror(results[1]) + end + else + if self:detectcontinue(err) then + self._buffer = chunk .. '\n' + return 2 + else + self:displayerror(err) + self._buffer = '' + end + end + + return 1 +end + +--- Creates a new REPL object, so you can override methods without fear. +-- @return A REPL clone. +function repl:clone() + local plugins_copy = tcopy(self._plugins, setmetatable({}, plugins_lookup_meta)) + local features_copy = tcopy(self._features) + local ifplugin_copy = {} + local iffeature_copy = {} + + for k, v in pairs(self._ifplugin) do + ifplugin_copy[k] = tcopy(v) + end + + for k, v in pairs(self._iffeature) do + iffeature_copy[k] = tcopy(v) + end + + return setmetatable({ + _buffer = '', + _plugins = plugins_copy, + _features = features_copy, + _ifplugin = ifplugin_copy, + _iffeature = iffeature_copy, + }, { __index = self }) +end + +--- Displays the given prompt to the user. Must be overriden. +-- @param prompt The prompt to display. +function repl:showprompt(prompt) + error 'You must implement the showprompt method' +end + +--- Displays the results from evaluate(). Must be overriden. +-- @param results The results to display. The results are a table, with the integer keys containing the results, and the 'n' key containing the highest integer key. + +function repl:displayresults(results) + error 'You must implement the displayresults method' +end + +--- Displays errors from evaluate(). Must be overriden. +-- @param err The error value returned from repl:traceback(). +-- @see repl:traceback +function repl:displayerror(err) + error 'You must implement the displayerror method' +end + +--- Checks whether this REPL object has loaded the given plugin. +-- @param plugin The plugin that the REPL may have loaded. +function repl:hasplugin(plugin) + return self._plugins[plugin] +end + +function repl:hasfeature(feature) + return self._features[feature] +end + +function repl:requirefeature(feature) + if not self:hasfeature(feature) then + error(sformat('required feature %q not present', feature), 2) + end +end + +function repl:ifplugin(plugin, action) + if self:hasplugin(plugin) then + action() + else + local pending_actions = self._ifplugin[plugin] + + if not pending_actions then + pending_actions = {} + self._ifplugin[plugin] = pending_actions + end + + pending_actions[#pending_actions + 1] = action + end +end + +--- If the given feature has been loaded, call `action`. Otherwise, if the +-- feature is ever loaded in the future, call `action` after that loading occurs. +function repl:iffeature(feature, action) + if self:hasfeature(feature) then + action() + else + local pending_features = self._iffeature[feature] + + if not pending_features then + pending_features = {} + self._iffeature[feature] = pending_features + end + + pending_features[#pending_features + 1] = action + end +end + +local function setup_before(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = function(...) + value(...) + return old_value(...) + end + end + + return setmetatable({}, mt) +end + +local function setup_after(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = function(...) + local _, results = gather_results(true, old_value(...)) + value(...) + return compat.unpack(results, 1, results.n) + end + end + + return setmetatable({}, mt) +end + +local function setup_around(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = function(self, ...) + return value(self, old_value, ...) + end + end + + return setmetatable({}, mt) +end + +local function setup_override(repl) + local mt = {} + + function mt:__newindex(key, value) + if type(value) ~= 'function' then + error(tostring(value) .. " is not a function", 2) + end + + local old_value = repl[key] + + if old_value == nil then + error(sformat("The '%s' method does not exist", key), 2) + end + + repl[key] = value + end + + return setmetatable({}, mt) +end + +local function setup_repl(repl) + local mt = {} + + function mt:__newindex(key, value) + local old_value = repl[key] + + if old_value ~= nil then + error(sformat("The '%s' method already exists", key), 2) + end + + repl[key] = value + end + + function mt:__index(key) + local value = repl[key] + + if type(value) == 'function' then + -- XXX cache this? + return function(_, ...) + return value(repl, ...) + end + end + + return value + end + + return setmetatable({}, mt) +end + +-- TODO use lua-procure for this (eventually) +local function findchunk(name) + for _, loader in pairs(compat.package.searchers) do + local chunk = loader(name) + + if type(chunk) == 'function' then + return chunk + end + end + + error('unable to locate plugin', 3) +end + +function repl:loadplugin(chunk) + if self:hasplugin(chunk) then + error(sformat('plugin %q has already been loaded', tostring(chunk)), 2) + end + self._plugins[chunk] = true + + local plugin_actions = self._ifplugin[chunk] + self._ifplugin[chunk] = nil + + if type(chunk) == 'string' then + chunk = findchunk('repl.plugins.' .. chunk) + end + + local plugin_env = { + repl = setup_repl(self), + before = setup_before(self), + after = setup_after(self), + around = setup_around(self), + override = setup_override(self), + init = function() end, + } + + local function ro_globals(_, key, _) + error(sformat('global environment is read-only (key = %q)', key), 2) + end + + plugin_env._G = plugin_env + plugin_env.features = {} + setmetatable(plugin_env, { __index = _G, __newindex = ro_globals }) + + setfenv(chunk, plugin_env) + local _, results = gather_results(nil, chunk()) + + local features = plugin_env.features or {} + + if type(features) == 'string' then + features = { features } + end + + for _, feature in ipairs(features) do + if self._features[feature] then + error(sformat('feature %q already present', feature), 2) + end + + self._features[feature] = true + + local feature_actions = self._iffeature[feature] + self._iffeature[feature] = nil + if feature_actions then + for _, action in ipairs(feature_actions) do + action() + end + end + end + + if plugin_actions then + for _, action in ipairs(plugin_actions) do + action() + end + end + + return compat.unpack(results, 1, results.n) +end + +-- XXX how to guarantee this gets called? +function repl:shutdown() +end + +function repl:getcontext() + return _G +end + +return repl diff --git a/lib/lua-repl/repl/plugins/autoreturn.lua b/lib/lua-repl/repl/plugins/autoreturn.lua new file mode 100644 index 00000000..a0e15eab --- /dev/null +++ b/lib/lua-repl/repl/plugins/autoreturn.lua @@ -0,0 +1,29 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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. + +-- A plugin that causes the REPL to automatically return evaluation results + +function around:compilechunk(orig, chunk) + local f, err = orig(self, 'return ' .. chunk) + + if not f then + f, err = orig(self, chunk) + end + + return f, err +end diff --git a/lib/lua-repl/repl/plugins/completion.lua b/lib/lua-repl/repl/plugins/completion.lua new file mode 100644 index 00000000..938c7439 --- /dev/null +++ b/lib/lua-repl/repl/plugins/completion.lua @@ -0,0 +1,192 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 utils = require 'repl.utils' +local getmetatable = getmetatable +local pairs = pairs +local sfind = string.find +local sgmatch = string.gmatch +local smatch = string.match +local ssub = string.sub +local tconcat = table.concat +local tsort = table.sort +local type = type + +local function isindexable(value) + if type(value) == 'table' then + return true + end + + local mt = getmetatable(value) + + return mt and mt.__index +end + +local function getcompletions(t) + local union = {} + + while isindexable(t) do + if type(t) == 'table' then + -- XXX what about the pairs metamethod in 5.2? + -- either we don't care, we implement a __pairs-friendly + -- pairs for 5.1, or implement a 'rawpairs' for 5.2 + for k, v in pairs(t) do + if union[k] == nil then + union[k] = v + end + end + end + + local mt = getmetatable(t) + t = mt and mt.__index or nil + end + + return pairs(union) +end + +local function split_ns(expr) + if expr == '' then + return { '' } + end + + local pieces = {} + + -- XXX method calls too (option?) + for m in sgmatch(expr, '[^.]+') do + pieces[#pieces + 1] = m + end + + -- logic for determining whether to pad the matches with the empty + -- string (ugly) + if ssub(expr, -1) == '.' then + pieces[#pieces + 1] = '' + end + + return pieces +end + +local function determine_ns(expr) + local ns = _G -- XXX what if the REPL lives in a special context? (option?) + local pieces = split_ns(expr) + + for index = 1, #pieces - 1 do + local key = pieces[index] + -- XXX rawget? or regular access? (option?) + ns = ns[key] + + if not isindexable(ns) then + return {}, '', '' + end + end + + expr = pieces[#pieces] + + local prefix = '' + + if #pieces > 1 then + prefix = tconcat(pieces, '.', 1, #pieces - 1) .. '.' + end + + local last_piece = pieces[#pieces] + + local before, after = smatch(last_piece, '(.*):(.*)') + + if before then + ns = ns[before] -- XXX rawget + prefix = prefix .. before .. ':' + expr = after + end + + return ns, prefix, expr +end + +local isidentifierchar + +do + local ident_chars_set = {} + -- XXX I think this can be done with isalpha in C... + local ident_chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.:_0123456789' + + for i = 1, #ident_chars do + local char = ssub(ident_chars, i, i) + ident_chars_set[char] = true + end + + function isidentifierchar(char) + return ident_chars_set[char] + end +end + +local function extract_innermost_expr(expr) + local index = #expr + + while index > 0 do + local char = ssub(expr, index, index) + if isidentifierchar(char) then + index = index - 1 + else + break + end + end + + index = index + 1 + + return ssub(expr, 1, index - 1), ssub(expr, index) +end + +-- XXX is this logic (namely, returning the entire line) too specific to +-- linenoise? +function repl:complete(expr, callback) + if utils.ends_in_unfinished_string(expr) then + return + end + + local ns, prefix, path + + prefix, expr = extract_innermost_expr(expr) + + ns, path, expr = determine_ns(expr) + + prefix = prefix .. path + + local completions = {} + + for k, v in getcompletions(ns) do + if sfind(k, expr, 1, true) == 1 then + local suffix = '' + local type = type(v) + + -- XXX this should be optional + if type == 'function' then + suffix = '(' + elseif type == 'table' then + suffix = '.' + end + + completions[#completions + 1] = prefix .. k .. suffix + end + end + + tsort(completions) + + for _, completion in ipairs(completions) do + callback(completion) + end +end + +features = 'completion' diff --git a/lib/lua-repl/repl/plugins/example.lua b/lib/lua-repl/repl/plugins/example.lua new file mode 100644 index 00000000..d55fd076 --- /dev/null +++ b/lib/lua-repl/repl/plugins/example.lua @@ -0,0 +1,41 @@ +-- Example plugin that demonstrates the objects available to a +-- plugin, as well as the methods that a plugin should make use +-- of + +-- Adding methods and properties to the repl object adds them to +-- the REPL object loading the plugin. If such a method or property +-- already exists, the current plugin will fail to load. +function repl:newmethod(...) +end + +-- Adding methods to the before object causes them to be called +-- before the actual method itself. If the method being added +-- (in this case displayresults) does not exist on the REPL object +-- loading this plugin, the current plugin will fail to load. +function before:displayresults(results) +end + +-- Adding methods to the after object causes them to be called +-- after the actual method itself. If the method being added +-- (in this case displayresults) does not exist on the REPL object +-- loading this plugin, the current plugin will fail to load. +function after:displayresults(results) +end + +-- Adding methods to the around object causes them to be called +-- instead of the original method of the same name. The new +-- method receives all of the arguments that the original would, +-- except it also receives the original method as the first argument. +-- This way, the new method may invoke the original as it pleases. +-- If the method being added (in this case displayresults) does not exist on +-- the REPL object loading this plugin, the current plugin will fail to load. +function around:evalute(orig, chunk) +end + +-- Adding methods to the override object causes them to be called +-- instead of the original method of the same name. If the method being added +-- (in this case displayresults) does not exist on the REPL object loading this +-- plugin, the current plugin will fail to load. +function override:name() + return 'Plugin!' +end diff --git a/lib/lua-repl/repl/plugins/filename_completion.lua b/lib/lua-repl/repl/plugins/filename_completion.lua new file mode 100644 index 00000000..c16729f3 --- /dev/null +++ b/lib/lua-repl/repl/plugins/filename_completion.lua @@ -0,0 +1,63 @@ +local utils = require 'repl.utils' +local lfs = require 'lfs' + +repl:requirefeature 'completion' + +local function guess_directory_separator(file_name) + return file_name:match('/') or + file_name:match('\\') or + '/' +end + +local function split_parent_directory(file_name) + local parent_directory, directory_entry = + file_name:match('^(.+)[\\/](.+)$') + if not parent_directory then + parent_directory = '.' + directory_entry = file_name + end + return parent_directory, directory_entry +end + +local function is_ignored_directory_entry(entry) + return entry == '.' or + entry == '..' +end + +local function replace_end_of_string(str, suffix, replacement) + assert(str:sub(-#suffix) == suffix) + return str:sub(1, -(#suffix+1)) .. replacement +end + +local function complete_file_name(file_name, expr, callback) + local directory, partial_entry = split_parent_directory(file_name) + for entry in lfs.dir(directory) do + if not is_ignored_directory_entry(entry) and + entry:find(partial_entry, 1, true) == 1 then + callback(replace_end_of_string(expr, partial_entry, entry)) + end + end +end + +local function complete_directory(directory, expr, callback) + for entry in lfs.dir(directory) do + if not is_ignored_directory_entry(entry) then + callback(expr..entry) + end + end +end + +function after:complete(expr, callback) + if utils.ends_in_unfinished_string(expr) then + local file_name = expr:match('[%w@/\\.-_+#$%%{}[%]!~ ]+$') + if file_name then + if file_name:find('[/\\]$') then + complete_directory(file_name, expr, callback) + else + complete_file_name(file_name, expr, callback) + end + else + complete_directory('.', expr, callback) + end + end +end diff --git a/lib/lua-repl/repl/plugins/history.lua b/lib/lua-repl/repl/plugins/history.lua new file mode 100644 index 00000000..6330dbd2 --- /dev/null +++ b/lib/lua-repl/repl/plugins/history.lua @@ -0,0 +1,60 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 history_file + +local function invokecallback(self, name, ...) + if not self._history_callbacks then + return + end + + local impl = self._history_callbacks[name] + return impl(...) +end + +local function init() + if os.getenv 'HOME' then + history_file = os.getenv('HOME') .. '/.rep.lua.history' + end +end + +-- XXX I don't know if this callback setup way +-- is the best way to go about this (in fact +-- I'm pretty sure it isn't), but I just need +-- something that works right now. +function repl:setuphistorycallbacks(callbacks) + self._history_callbacks = callbacks + + if history_file then + invokecallback(self, 'load', history_file) + end +end + +function after:handleline(line) + invokecallback(self, 'addline', line) +end + +function before:shutdown() + if history_file then + invokecallback(self, 'save', history_file) + end +end + +features = 'history' + +init() diff --git a/lib/lua-repl/repl/plugins/keep_last_eval.lua b/lib/lua-repl/repl/plugins/keep_last_eval.lua new file mode 100644 index 00000000..01a77946 --- /dev/null +++ b/lib/lua-repl/repl/plugins/keep_last_eval.lua @@ -0,0 +1,43 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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. + +-- A plugin that stores the results of the last evaluation in _G._ + +local tostring = tostring + +function before:displayresults(results) + local context = self:getcontext() + + if self._keep_eval_lastn then + context._ = nil + + for i = 1, self._keep_eval_lastn do + context['_' .. tostring(i)] = nil + end + end + + if results.n > 0 then + context._ = results[1] + + for i = 1, results.n do + context['_' .. tostring(i)] = results[i] + end + + self._keep_eval_lastn = results.n + end +end diff --git a/lib/lua-repl/repl/plugins/linenoise.lua b/lib/lua-repl/repl/plugins/linenoise.lua new file mode 100644 index 00000000..4407c535 --- /dev/null +++ b/lib/lua-repl/repl/plugins/linenoise.lua @@ -0,0 +1,59 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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. + +-- A plugin that uses linenoise (https://github.com/hoelzro/lua-linenoise) for prompting + +local ln = require 'linenoise' + +repl:requirefeature 'console' + +function override:showprompt(prompt) + self._prompt = prompt -- XXX how do we make sure other plugins don't step on this? +end + +function override:lines() + return function() + return ln.linenoise(self._prompt .. ' ') + end +end + +repl:iffeature('completion', function() + ln.setcompletion(function(completions, line) + repl:complete(line, function(completion) + ln.addcompletion(completions, completion) + end) + end) +end) + +repl:ifplugin('history', function() + repl:setuphistorycallbacks { + load = function(filename) + ln.historyload(filename) + end, + + addline = function(line) + ln.historyadd(line) + end, + + save = function(filename) + ln.historysave(filename) + end, + } +end) + +features = 'input' diff --git a/lib/lua-repl/repl/plugins/pretty_print.lua b/lib/lua-repl/repl/plugins/pretty_print.lua new file mode 100644 index 00000000..2318f444 --- /dev/null +++ b/lib/lua-repl/repl/plugins/pretty_print.lua @@ -0,0 +1,262 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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. + +-- Pretty prints expression results (console only) + +local format = string.format +local tconcat = table.concat +local tsort = table.sort +local tostring = tostring +local type = type +local floor = math.floor +local pairs = pairs +local ipairs = ipairs +local error = error +local stderr = io.stderr + +pcall(require, 'luarocks.require') +local ok, term = pcall(require, 'term') +if not ok then + term = nil +end + +local keywords = { + ['and'] = true, + ['break'] = true, + ['do'] = true, + ['else'] = true, + ['elseif'] = true, + ['end'] = true, + ['false'] = true, + ['for'] = true, + ['function'] = true, + ['if'] = true, + ['in'] = true, + ['local'] = true, + ['nil'] = true, + ['not'] = true, + ['or'] = true, + ['repeat'] = true, + ['return'] = true, + ['then'] = true, + ['true'] = true, + ['until'] = true, + ['while'] = true, +} + +local function compose(f, g) + return function(...) + return f(g(...)) + end +end + +local emptycolormap = setmetatable({}, { __index = function() + return function(s) + return s + end +end}) + +local colormap = emptycolormap + +if term then + colormap = { + ['nil'] = term.colors.blue, + string = term.colors.yellow, + punctuation = compose(term.colors.green, term.colors.bright), + ident = term.colors.red, + boolean = term.colors.green, + number = term.colors.cyan, + path = term.colors.white, + misc = term.colors.magenta, + } +end + +local function isinteger(n) + return type(n) == 'number' and floor(n) == n +end + +local function isident(s) + return type(s) == 'string' and not keywords[s] and s:match('^[a-zA-Z_][a-zA-Z0-9_]*$') +end + +-- most of these are arbitrary, I *do* want numbers first, though +local type_order = { + number = 0, + string = 1, + userdata = 2, + table = 3, + thread = 4, + boolean = 5, + ['function'] = 6, + cdata = 7, +} + +local function cross_type_order(a, b) + local pos_a = type_order[ type(a) ] + local pos_b = type_order[ type(b) ] + + if pos_a == pos_b then + return a < b + else + return pos_a < pos_b + end +end + +local function sortedpairs(t) + local keys = {} + + local seen_non_string + + for k in pairs(t) do + keys[#keys + 1] = k + + if not seen_non_string and type(k) ~= 'string' then + seen_non_string = true + end + end + + local sort_func = seen_non_string and cross_type_order or nil + tsort(keys, sort_func) + + local index = 1 + return function() + if keys[index] == nil then + return nil + else + local key = keys[index] + local value = t[key] + index = index + 1 + + return key, value + end + end, keys +end + +local function find_longstring_nest_level(s) + local level = 0 + + while s:find(']' .. string.rep('=', level) .. ']', 1, true) do + level = level + 1 + end + + return level +end + +local function dump(params) + local pieces = params.pieces + local seen = params.seen + local path = params.path + local v = params.value + local indent = params.indent + + local t = type(v) + + if t == 'nil' or t == 'boolean' or t == 'number' then + pieces[#pieces + 1] = colormap[t](tostring(v)) + elseif t == 'string' then + if v:match '\n' then + local level = find_longstring_nest_level(v) + pieces[#pieces + 1] = colormap.string('[' .. string.rep('=', level) .. '[' .. v .. ']' .. string.rep('=', level) .. ']') + else + pieces[#pieces + 1] = colormap.string(format('%q', v)) + end + elseif t == 'table' then + if seen[v] then + pieces[#pieces + 1] = colormap.path(seen[v]) + return + end + + seen[v] = path + + local lastintkey = 0 + + pieces[#pieces + 1] = colormap.punctuation '{\n' + for i, v in ipairs(v) do + for j = 1, indent do + pieces[#pieces + 1] = ' ' + end + dump { + pieces = pieces, + seen = seen, + path = path .. '[' .. tostring(i) .. ']', + value = v, + indent = indent + 1, + } + pieces[#pieces + 1] = colormap.punctuation ',\n' + lastintkey = i + end + + for k, v in sortedpairs(v) do + if not (isinteger(k) and k <= lastintkey and k > 0) then + for j = 1, indent do + pieces[#pieces + 1] = ' ' + end + + if isident(k) then + pieces[#pieces + 1] = colormap.ident(k) + else + pieces[#pieces + 1] = colormap.punctuation '[' + dump { + pieces = pieces, + seen = seen, + path = path .. '.' .. tostring(k), + value = k, + indent = indent + 1, + } + pieces[#pieces + 1] = colormap.punctuation ']' + end + pieces[#pieces + 1] = colormap.punctuation ' = ' + dump { + pieces = pieces, + seen = seen, + path = path .. '.' .. tostring(k), + value = v, + indent = indent + 1, + } + pieces[#pieces + 1] = colormap.punctuation ',\n' + end + end + + for j = 1, indent - 1 do + pieces[#pieces + 1] = ' ' + end + + pieces[#pieces + 1] = colormap.punctuation '}' + else + pieces[#pieces + 1] = colormap.misc(tostring(v)) + end +end + +repl:requirefeature 'console' + +function override:displayresults(results) + local pieces = {} + + for i = 1, results.n do + dump { + pieces = pieces, + seen = {}, + path = '', + value = results[i], + indent = 1, + } + pieces[#pieces + 1] = '\n' + end + + stderr:write(tconcat(pieces, '')) +end diff --git a/lib/lua-repl/repl/plugins/rcfile.lua b/lib/lua-repl/repl/plugins/rcfile.lua new file mode 100644 index 00000000..a74e81ac --- /dev/null +++ b/lib/lua-repl/repl/plugins/rcfile.lua @@ -0,0 +1,54 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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. + +-- A plugin that runs code in $HOME/.rep.lua before the REPL starts + +local setfenv = require('repl.utils').setfenv + +local function readable(filename) + local f = io.open(filename, 'r') + if not f then + return false + end + f:close() + return true +end + +local function init() + local home = os.getenv 'HOME' + + if not home then + return + end + + local rcfile = home .. '/.rep.lua' + + if not readable(rcfile) then + return + end + + local chunk = assert(loadfile(rcfile)) + local env = setmetatable({ repl = repl }, { __index = _G, __newindex = _G }) + + setfenv(chunk, env) + + chunk() + return true +end + +return init() diff --git a/lib/lua-repl/repl/plugins/rlwrap.lua b/lib/lua-repl/repl/plugins/rlwrap.lua new file mode 100644 index 00000000..9f195a3f --- /dev/null +++ b/lib/lua-repl/repl/plugins/rlwrap.lua @@ -0,0 +1,41 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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. + +if os.getenv 'LUA_REPL_RLWRAP' then + features = 'input' +else + -- XXX check that we're not receiving input from a non-tty + local has_rlwrap = os.execute('which rlwrap >/dev/null 2>/dev/null') + + if type(has_rlwrap) ~= 'boolean' then + has_rlwrap = has_rlwrap == 0 + end + + if not has_rlwrap then + error 'Please install rlwrap in order to use the rlwrap plugin' + end + + local lowest_index = -1 + + while arg[lowest_index] ~= nil do + lowest_index = lowest_index - 1 + end + lowest_index = lowest_index + 1 + os.execute(string.format('LUA_REPL_RLWRAP=1 rlwrap %q %q', arg[lowest_index], arg[0])) + os.exit(0) +end diff --git a/lib/lua-repl/repl/plugins/semicolon_suppress_output.lua b/lib/lua-repl/repl/plugins/semicolon_suppress_output.lua new file mode 100644 index 00000000..4adaecb6 --- /dev/null +++ b/lib/lua-repl/repl/plugins/semicolon_suppress_output.lua @@ -0,0 +1,36 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 smatch = string.match + +-- XXX will this affect any other plugins? +function around:compilechunk(orig, chunk) + local f, err = orig(self, chunk) + + if not f then + return f, err + end + + if smatch(chunk, ';%s*$') then + return function() + f() + end + end + + return f +end diff --git a/lib/lua-repl/repl/sync.lua b/lib/lua-repl/repl/sync.lua new file mode 100644 index 00000000..a422ed4b --- /dev/null +++ b/lib/lua-repl/repl/sync.lua @@ -0,0 +1,46 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 repl = require 'repl' +local sync_repl = repl:clone() +local error = error + +-- @class repl.sync +--- This module implements a synchronous REPL. It provides +--- a run() method for actually running the REPL, and requires +--- that implementors implement the lines() method. + +--- Run a REPL loop in a synchronous fashion. +-- @name repl.sync:run +function sync_repl:run() + self:prompt(1) + for line in self:lines() do + local level = self:handleline(line) + self:prompt(level) + end + self:shutdown() +end + +--- Returns an iterator that yields lines to be evaluated. +-- @name repl.sync:lines +-- @return An iterator. +function sync_repl:lines() + error 'You must implement the lines method' +end + +return sync_repl diff --git a/lib/lua-repl/repl/utils.lua b/lib/lua-repl/repl/utils.lua new file mode 100644 index 00000000..3e2ca18f --- /dev/null +++ b/lib/lua-repl/repl/utils.lua @@ -0,0 +1,70 @@ +-- Copyright (c) 2011-2015 Rob Hoelz +-- +-- 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 setfenv = setfenv or function(f, t) + local upvalue_index = 1 + + -- XXX we may need a utility library if debug isn't available + while true do + local name = debug.getupvalue(f, upvalue_index) + -- some functions don't have an _ENV upvalue, because + -- they never refer to globals + if not name then + return + end + if name == '_ENV' then + debug.setupvalue(f, upvalue_index, t) + return + end + upvalue_index = upvalue_index + 1 + end +end + +--- Tests wheter an expression ends in an unfinished string literal. +-- @return First position in the unfinished string literal or `nil`. +local function ends_in_unfinished_string(expr) + local position = 0 + local quote + local current_delimiter + local last_unmatched_start + while true do + -- find all quotes: + position, quote = expr:match('()([\'"])', position+1) + if not position then break end + -- if we're currently in a string: + if current_delimiter then + -- would end current string? + if current_delimiter == quote then + -- not escaped? + if expr:sub(position-1, position-1) ~= '\\' then + current_delimiter = nil + last_unmatched_start = nil + end + end + else + current_delimiter = quote + last_unmatched_start = position+1 + end + end + return last_unmatched_start +end + +return { + setfenv = setfenv, + ends_in_unfinished_string = ends_in_unfinished_string +} -- cgit v1.2.3