diff --git a/examples/lua_api_explorer.lua b/examples/lua_api_explorer.lua new file mode 100644 index 0000000..a20e46d --- /dev/null +++ b/examples/lua_api_explorer.lua @@ -0,0 +1,39 @@ +local lynx = require "lynx" +local raylua_funcs = require "lynx.raylua_lynx" + +local menu = lynx.menu ({ + lynx.text("Test Lynx menu in Raylib", { selectable = false }), + lynx.text("", { selectable = false }), + lynx.text("Long life to Lynx !"), + lynx.text("the best UI library ever !"), + lynx.text("which works everywhere !"), + lynx.text("", { selectable = false }), + lynx.text("@TSnake41"), +}, { + x = 0, + y = 0, + w = 500, + h = 500, + default_height = 24, + current = 3, + funcs = raylua_funcs +}) + +rl.SetConfigFlags(rl.FLAG_VSYNC_HINT) + +rl.InitWindow(800, 450, "raylib [lua] example - lynx menu") + +while not rl.WindowShouldClose() do + rl.BeginDrawing() + + rl.ClearBackground(rl.BLACK) + local pos = rl.GetMousePosition() + menu:input_mouse(pos.x, pos.y, 0) + + menu:update(rl.GetFrameTime()) + menu:draw() + + rl.EndDrawing() +end + +rl.CloseWindow() diff --git a/examples/lua_lynx_menu.lua b/examples/lua_lynx_menu.lua new file mode 100644 index 0000000..489653f --- /dev/null +++ b/examples/lua_lynx_menu.lua @@ -0,0 +1,40 @@ +local lynx = require "lynx" +local raylua_funcs = require "lynx.raylua_lynx" + +local menu = lynx.menu ({ + lynx.text("Test Lynx menu in Raylib", { selectable = false }), + lynx.text("", { selectable = false }), + lynx.text("Long life to Lynx !"), + lynx.text("the best UI library ever !"), + lynx.text("which works everywhere !"), + lynx.text("", { selectable = false }), + lynx.text("@TSnake41"), +}, { + x = 0, + y = 0, + w = 500, + h = 500, + default_height = 24, + current = 3, + funcs = raylua_funcs +}) + +rl.SetConfigFlags(rl.FLAG_VSYNC_HINT) +--rl.SetTargetFPS(60) + +rl.InitWindow(800, 450, "raylib [lua] example - lynx menu") + +while not rl.WindowShouldClose() do + rl.BeginDrawing() + + rl.ClearBackground(rl.BLACK) + local pos = rl.GetMousePosition() + menu:input_mouse(pos.x, pos.y, 0) + + menu:update(rl.GetFrameTime()) + menu:draw() + + rl.EndDrawing() +end + +rl.CloseWindow() diff --git a/examples/lynx b/examples/lynx new file mode 160000 index 0000000..088321f --- /dev/null +++ b/examples/lynx @@ -0,0 +1 @@ +Subproject commit 088321f96efc444eb99f1b35ed1718003bad3b78 diff --git a/examples/misc_32x32.lua b/examples/misc_32x32.lua new file mode 100644 index 0000000..079e6b3 --- /dev/null +++ b/examples/misc_32x32.lua @@ -0,0 +1,90 @@ +--[[ +raylib 32x32 game/demo competition + +Competition consist in developing a videogame in a 32x32 pixels screen size. + +RULES: + + 1) Use only raylib (and included libraries), no external libraries allowed + 2) The submission should consist of just one source file + 3) Render your game/demo to a 32x32 pixels render texture, + show what you could do with a 32x32 RGB LED matrix! + 4) No external resources, you CAN only create them programmatically, + 5) Game/demo can be 2D or 3D, choose wisely + 5) Shaders (if used) should be included in the source as string (char *) + and loaded with LoadShaderCode() + 6) Code must compile and execute in PLATFORM_DESKTOP (Windows, Linux, macOS) + + +LICENSE: zlib/libpng + +Copyright (c) 2020 () + +This software is provided "as-is", without any express or implied warranty. In no event +will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter it and redistribute it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not claim that you + wrote the original software. If you use this software in a product, an acknowledgment + in the product documentation would be appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be misrepresented + as being the original software. + + 3. This notice may not be removed or altered from any source distribution. +]] + +local ffi = require "ffi" + +local windowWidth = 512 +local windowHeight = 512 +local gameScreenWidth = 32 +local gameScreenHeight = 32 + +rl.SetTraceLogLevel(rl.LOG_WARNING) +rl.SetConfigFlags(rl.FLAG_VSYNC_HINT) +rl.InitWindow(windowWidth, windowHeight, "my 32x32 game/demo") + +local target = rl.LoadRenderTexture(gameScreenWidth, gameScreenHeight) +rl.SetTextureFilter(target.texture, rl.FILTER_POINT) + +rl.SetTargetFPS(60) + +while not rl.WindowShouldClose() do + local scale = math.min(rl.GetScreenWidth() / gameScreenWidth, rl.GetScreenHeight() / gameScreenHeight) + + rl.BeginDrawing() + + rl.BeginTextureMode(target) + rl.ClearBackground(rl.WHITE) + rl.DrawTextEx(rl.GetFontDefault(), "Hello", ffi.new("Vector2", 4, 0), rl.GetFontDefault().baseSize, 1, ffi.new("Color", 0, 64, 255, 255)) + rl.DrawTextEx(rl.GetFontDefault(), "it's", ffi.new("Vector2", 8, 10), rl.GetFontDefault().baseSize, 1, ffi.new("Color", 0, 64, 255, 255)) + rl.DrawTextEx(rl.GetFontDefault(), "raylua", ffi.new("Vector2", 0, 20), rl.GetFontDefault().baseSize, 1, ffi.new("Color", 0, 64, 255, 255)) + + rl.EndTextureMode() + + rl.DrawTexturePro( + target.texture, + ffi.new("Rectangle", 0, 0, target.texture.width, -target.texture.height), + ffi.new("Rectangle", + (rl.GetScreenWidth() - gameScreenWidth * scale) * 0.5, + (rl.GetScreenHeight() - gameScreenHeight * scale) * 0.5, + gameScreenWidth * scale, gameScreenHeight * scale + ), ffi.new("Vector2", 0, 0), 0, rl.WHITE + ) + + for x=0,rl.GetScreenWidth(),16 do + rl.DrawRectangle(x, 0, 4, rl.GetScreenHeight(), rl.BLACK) + end + + for y=0,rl.GetScreenHeight(),16 do + rl.DrawRectangle(0, y, rl.GetScreenWidth(), 4, rl.BLACK) + end + rl.EndDrawing() +end + +rl.UnloadRenderTexture(target) + +rl.CloseWindow() diff --git a/raylua_e b/raylua_e new file mode 100644 index 0000000..cffb33c Binary files /dev/null and b/raylua_e differ diff --git a/raylua_s b/raylua_s new file mode 100644 index 0000000..09dd6bc Binary files /dev/null and b/raylua_s differ diff --git a/repl/main.lua b/repl/main.lua new file mode 100644 index 0000000..1a4c1cf --- /dev/null +++ b/repl/main.lua @@ -0,0 +1,19 @@ +local repl = require 'repl.console' +local rcfile_loaded = repl:loadplugin 'rcfile' + +if not rcfile_loaded then + local has_linenoise = pcall(require, 'linenoise') + + if has_linenoise then + repl:loadplugin 'linenoise' + else + pcall(repl.loadplugin, repl, 'rlwrap') + end + + repl:loadplugin 'history' + repl:loadplugin 'completion' + repl:loadplugin 'autoreturn' +end + +print('Lua REPL ' .. tostring(repl.VERSION)) +repl:run() diff --git a/repl/repl/compat.lua b/repl/repl/compat.lua new file mode 100644 index 0000000..fd47748 --- /dev/null +++ b/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/repl/repl/console.lua b/repl/repl/console.lua new file mode 100644 index 0000000..154d037 --- /dev/null +++ b/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/repl/repl/init.lua b/repl/repl/init.lua new file mode 100644 index 0000000..5e5a95e --- /dev/null +++ b/repl/repl/init.lua @@ -0,0 +1,415 @@ +-- 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.9 } +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) + 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/repl/repl/plugins/autoreturn.lua b/repl/repl/plugins/autoreturn.lua new file mode 100644 index 0000000..a0e15ea --- /dev/null +++ b/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/repl/repl/plugins/completion.lua b/repl/repl/plugins/completion.lua new file mode 100644 index 0000000..938c743 --- /dev/null +++ b/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/repl/repl/plugins/example.lua b/repl/repl/plugins/example.lua new file mode 100644 index 0000000..d55fd07 --- /dev/null +++ b/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/repl/repl/plugins/filename_completion.lua b/repl/repl/plugins/filename_completion.lua new file mode 100644 index 0000000..c16729f --- /dev/null +++ b/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/repl/repl/plugins/history.lua b/repl/repl/plugins/history.lua new file mode 100644 index 0000000..6330dbd --- /dev/null +++ b/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/repl/repl/plugins/keep_last_eval.lua b/repl/repl/plugins/keep_last_eval.lua new file mode 100644 index 0000000..01a7794 --- /dev/null +++ b/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/repl/repl/plugins/linenoise.lua b/repl/repl/plugins/linenoise.lua new file mode 100644 index 0000000..4407c53 --- /dev/null +++ b/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/repl/repl/plugins/pretty_print.lua b/repl/repl/plugins/pretty_print.lua new file mode 100644 index 0000000..2318f44 --- /dev/null +++ b/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/repl/repl/plugins/rcfile.lua b/repl/repl/plugins/rcfile.lua new file mode 100644 index 0000000..a74e81a --- /dev/null +++ b/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/repl/repl/plugins/rlwrap.lua b/repl/repl/plugins/rlwrap.lua new file mode 100644 index 0000000..9f195a3 --- /dev/null +++ b/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/repl/repl/plugins/semicolon_suppress_output.lua b/repl/repl/plugins/semicolon_suppress_output.lua new file mode 100644 index 0000000..4adaecb --- /dev/null +++ b/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/repl/repl/sync.lua b/repl/repl/sync.lua new file mode 100644 index 0000000..a422ed4 --- /dev/null +++ b/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/repl/repl/utils.lua b/repl/repl/utils.lua new file mode 100644 index 0000000..3e2ca18 --- /dev/null +++ b/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 +}