You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
416 lines
11 KiB
416 lines
11 KiB
-- Copyright (c) 2011-2015 Rob Hoelz <rob@hoelz.ro>
|
|
--
|
|
-- 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, "'<eof>'$") or smatch(err, "<eof>$")
|
|
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
|
|
|