diff options
Diffstat (limited to 'lib/lua-repl')
42 files changed, 3755 insertions, 0 deletions
diff --git a/lib/lua-repl/.editorconfig b/lib/lua-repl/.editorconfig new file mode 100644 index 00000000..ca4cf17b --- /dev/null +++ b/lib/lua-repl/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[{Makefile,*.makefile}] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false + diff --git a/lib/lua-repl/.gitignore b/lib/lua-repl/.gitignore new file mode 100644 index 00000000..08b58b64 --- /dev/null +++ b/lib/lua-repl/.gitignore @@ -0,0 +1,3 @@ +doc/ +*.tar.gz +*.rock diff --git a/lib/lua-repl/.proverc b/lib/lua-repl/.proverc new file mode 100644 index 00000000..a5f42ae6 --- /dev/null +++ b/lib/lua-repl/.proverc @@ -0,0 +1,2 @@ +--ext .lua +--exec lua diff --git a/lib/lua-repl/.travis.yml b/lib/lua-repl/.travis.yml new file mode 100644 index 00000000..d6390739 --- /dev/null +++ b/lib/lua-repl/.travis.yml @@ -0,0 +1,31 @@ +language: generic +sudo: false + +env: + - LUA=5.1 LUAROCKS=2.3.0 + - LUA=5.1.5 LUAROCKS=2.3.0 + - LUA=5.2.0 LUAROCKS=2.3.0 + - LUA=5.2.4 LUAROCKS=2.3.0 + - LUA=5.3.0 LUAROCKS=2.3.0 + - LUA=5.3.3 LUAROCKS=2.3.0 + +cache: + apt: true + directories: + - $HOME/luas/ + +addons: + apt: + packages: + - luarocks + +before_install: + - luarocks --local install vert + +install: + - ~/.luarocks/bin/vert init --lua-version=$LUA --luarocks-version=$LUAROCKS $HOME/luas/$LUA + - source $HOME/luas/$LUA/bin/activate + - luarocks install lua-testmore + +script: + - make test diff --git a/lib/lua-repl/COPYING b/lib/lua-repl/COPYING new file mode 100644 index 00000000..959dbc98 --- /dev/null +++ b/lib/lua-repl/COPYING @@ -0,0 +1,17 @@ +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. diff --git a/lib/lua-repl/Changes b/lib/lua-repl/Changes new file mode 100644 index 00000000..1ccb438b --- /dev/null +++ b/lib/lua-repl/Changes @@ -0,0 +1,71 @@ +NEXT + +0.10 Jan 14 2022 + --- Bug Fixes --- + - Fixed issue where current buffer is stuck after certain syntax errors (GH #61, thanks Justin Blanchard!) + +0.9 Oct 30 2018 + --- Bug Fixes --- + - Fixed rlwrap plugin under Lua 5.2 and greater (GH #57) + + --- Features/Enhancements --- + - Improve support for Lua 5.3 (thanks, Iqbal Ansari!) + +0.8 + Aug 23 2015 + --- Bug Fixes --- + - Use package.searchers if package.loaders is not available (thanks, Simon Cozens!) + + --- Backwards Incompatible Changes --- + - The default plugin bundle is no longer loaded if an rcfile is present. See + https://github.com/hoelzro/lua-repl/blob/0.8/README.md#removal-of-default-plugins-in-08 + for details. + +0.7 May 22 2015 + --- Features/Enhancements --- + - iffeature is implemented, thanks to Henry Kielmann! + - loadplugin now returns any values returned by the plugin function. + + --- Deprecations --- + - 0.8 will break backwards compatability due a change in rcfile handling; + please consult the README for details. + + --- Plugins --- + - we now have a filename completion plugin, thanks to Henry Kielmann! + - rlwrap functionality has been moved into a plugin. + +0.6 Oct 18 2014 + --- Bug Fixes --- + - Fix rcfile plugin for Lua 5.2 + +0.5 Oct 17 2014 + --- Features/Enhancements --- + - Lua 5.2 support (Thanks to Simon Cozens!) + +0.4 Mar 29 2013 + --- API Additions --- + - The repl object now has a VERSION property. + + --- Bug Fixes --- + - Overriding/augmenting getcontext will actually have an effect. + - Fix pretty_print with 'unsupported' types + + --- Features/Enhancements --- + - Change the example REPL's prompt to resemble the standalone interpreter's + - The example REPL now prints a welcome message + - If linenoise is not available and rlwrap is, the example REPL falls back to rlwrap + + --- Plugins --- + - Added some new plugins + - semicolon_suppress_output + +0.3 Oct 17 2012 + - Add plugins API. + - Add a bunch of plugins. + +0.2 Sep 28 2012 + - Add linenoise REPL. + - Add completion support. + +0.1 Oct 20 2011 + Initial release. Basic REPL functionality. diff --git a/lib/lua-repl/IDEAS.md b/lib/lua-repl/IDEAS.md new file mode 100644 index 00000000..8b7eba24 --- /dev/null +++ b/lib/lua-repl/IDEAS.md @@ -0,0 +1,43 @@ + * plugins + * replace /^=/ with 'return' + * handle locals + * override debug.debug + * supple (http://cgit.gitano.org.uk/supple.git) + * handle locals (debug.sethook?) + * debug.sethook, catch return of our chunk and grab its locals + * rewrite source code/bytecode before evaluation + * custom interpreter patch to "pcall and get bindings" + * custom module that dips into internals to "pcall and get bindings" + * some sort of debugger? + * don't contaminate globals + * tab completion (\_\_complete metamethod) + * "safe" evaluation (don't allow calling of C functions, except for those in a whitelist?) + * displaystack instead of displayerror(err)? (should xpcall return false, stack\_table?) + * visual REPL (like Factor; being able to print multi-colored/multi-sized text, images, etc) + * syntax highlighting + * paren/brace matching? + * snippets? + * code navigation (go to definition?) + * repls that "attach" to different objects (ie. inspect a single object; self is that object. completions happen against that object?) + * browsable/searchable REPL history + * not entirely sure what I mean here... + * safe termination of evaluated code (if I Control-C during an evaluation) + * store stdout/stderr output in a variable somewhere? + * persistence (pluto-based image) + +hooks +===== + + * what to do when we encounter an incomplete Lua fragment + * processing a line + * something for debug.debug... + +Implementations +=============== + + * Console + * GUI + * Web + * IRC + * safety hooks + * Awesome diff --git a/lib/lua-repl/Makefile b/lib/lua-repl/Makefile new file mode 100644 index 00000000..a092d939 --- /dev/null +++ b/lib/lua-repl/Makefile @@ -0,0 +1,14 @@ +LUA_FILES=$(shell find repl -type f -name '*.lua') +.PHONY: doc install + +doc: + luadoc -d doc $(LUA_FILES) + +install: + # TODO + +test: + LUA_INIT='' LUA_PATH=';;$(LUA_PATH);?.lua;?/init.lua;t/lib/?.lua' prove + +clean: + rm -rf doc/ diff --git a/lib/lua-repl/README.md b/lib/lua-repl/README.md new file mode 100644 index 00000000..58f128b0 --- /dev/null +++ b/lib/lua-repl/README.md @@ -0,0 +1,102 @@ +# REPL.lua - a reusable Lua REPL written in Lua, and an alternative to /usr/bin/lua + +This project has two uses: + + - An alternative to the standalone interpreter included with Lua, one that supports + things like plugins, tab completion, and automatic insertion of `return` in front + of expressions. + + - A REPL library you may embed in your application, to provide all of the niceties + of the standalone interpreter included with Lua and then some. + +Many software projects have made the choice to embed Lua in their projects to +allow their users some extra flexibility. Some of these projects would also +like to provide a Lua REPL in their programs for debugging or rapid development. +Most Lua programmers are familiar with the standalone Lua interpreter as a Lua REPL; +however, it is bound to the command line. Until now, Lua programmers would have to +implement their own REPL from scratch if they wanted to include one in their programs. +This project aims to provide a REPL implemented in pure Lua that almost any project can +make use of. + +This library also includes an example application (rep.lua), which serves as an alternative +to the standalone interpreter included with Lua. If the lua-linenoise library is installed, +it uses linenoise for history and tab completion; otherwise, it tries to use rlwrap for +basic line editing. If you would like the arrow keys to work as expected rather than printing +things like `^[[A`, please install the lua-linenoise library or the rlwrap program. + +# Project Goals + + * Provide REPL logic to Lua programs that include this module. + + * Be extensible through polymorphism and plugins. + + * Abstract away I/O, so you can run this REPL on the command line or in your own event loop and expect the same behavior. + +# Building + + * You need Luadoc (http://keplerproject.github.com/luadoc/) installed to build the documentation. + + * You need Test.More (http://fperrad.github.com/lua-TestMore/testmore.html) installed to run the tests. + +# Compatibility + +The current version of the software runs on Lua 5.1, LuaJIT ?.? etc. +A port to Lua 5.2 is envisaged, but is not at this stage a priority. +Since it is written purely in Lua, it should work on any platform that +has one of those versions of Lua installed. + +XXX Check which version of LuaJIT this works with +XXX Check that it works with other Lua interpreters + +# Installation + +You can install lua-repl via LuaRocks: + + luarocks install luarepl + +You can also install it by hand by copying the `repl` +directory to a location in your `package.path`, and +copying rep.lua to somewhere in your `PATH`. + +# Recommended packages + +`rep.lua` works best if you also have `linenoise` installed, +available from https://github.com/hoelzro/lua-linenoise. +`rep.lua` will fallback to using rlwrap if you have that as well; +without either of these, you will have command editing, history, +or other features generally provided by `readline`. + +# Features + +`rep.lua` prints the results of simple expressions without requiring +a `return ` or a `= ` in front of it. If `linenoise` is installed, +it also offers persistent history and tab completion. It also offers +a number of plugins; see plugins.md for a list of plugins that come +with lua-repl. + +# Backwards Compatibility Changes + +## Removal of default plugins in 0.8 + +Lua REPL 0.8 breaks backwards compatability by disabling the loading of the +default plugins (currently `linenoise`, `rlwrap`, `history`, `completion`, and +`autoreturn`) if an rcfile is found for a user. This is so that plugins may +not be forced onto a user if they don't want them, or play tricks with their +setup (see issue #47). If you would like to continue using these plugins, please +put the following code into your `~/.rep.lua`: + +```lua +if repl.VERSION >= 0.8 then + -- default plugins + repl:loadplugin 'linenoise' + repl:loadplugin 'history' + repl:loadplugin 'completion' + repl:loadplugin 'autoreturn' +end + +-- suppress warning message +repl.quiet_default_plugins = true +``` + +As mentioned in the code snippet, `repl.quiet_default_plugins` suppresses the warning. +You can remove this after upgrading to Lua REPL 0.8. diff --git a/lib/lua-repl/RELEASE-GUIDE.md b/lib/lua-repl/RELEASE-GUIDE.md new file mode 100644 index 00000000..1efcb728 --- /dev/null +++ b/lib/lua-repl/RELEASE-GUIDE.md @@ -0,0 +1,34 @@ +# lua-repl release guide + + - [ ] Bump VERSION (in repl/init.lua, look for references to current version) + - [ ] Update Changelog + - [ ] Rename and update rockspec + - [ ] Make sure tests pass + - [ ] Push & tag latest release + - [ ] Submit rockspec to luarocks + - [ ] E-mail lua-l + - [ ] Submit lua-l gmane link to reddit + +## E-mail template for lua-l + +Hi Lua users, + +I have just released version {{VERSION}} of lua-repl, an alternative to the +standalone REPL included with Lua and a library for embedding a Lua +REPL within a Lua application. + +lua-repl provides a library for adding a Lua REPL to your Lua-powered +application. It also provides an example REPL in the form of rep.lua, +which can take the place of the standalone interpreter provided with +Lua. It has a plugin facility; plugins for things like history and tab +completion of symbols are included in the lua-repl distribution. + +{{CHANGES}} + +You can install lua-repl via Luarocks (called luarepl there), or +manually from the source [{{REFERENCE}}]. + +-Rob + +[{{REFERENCE}}] https://github.com/hoelzro/lua-repl/archive/0.8.tar.gz + diff --git a/lib/lua-repl/Roadmap.md b/lib/lua-repl/Roadmap.md new file mode 100644 index 00000000..baaa3870 --- /dev/null +++ b/lib/lua-repl/Roadmap.md @@ -0,0 +1,44 @@ +0.10 +=== + + * Process Lua command line options with rep.lua + * Verify that it works with LuaJIT, Lua 5.0, Lua 5.2, LuaJ or something + * __pretty support for pretty print plugin + * __complete support for completion plugin + * Documentation improvements + * More clearly reference PLUGINS.md from README.md + * More clearly reference rep.lua from README.md + * Make sure that autocompletion is talked up in plugins.md (and mention in readme that many default/optional behaviors are present there) + * Make sure documentation on ~/.rep.lua is clear + * Move docs into doc/ + +Future +====== + + * Plugins + * where do plugins store values (self, storage object, etc?) + * configuration + * global assignments in plugins + * we need a way to do method advice in REPL "subclasses" + * test: using advice from within ifplugin/iffeature + * luaish plugin + * moonscript plugin - compile Moonscript instead of Lua + * Steal ideas from ilua + * Variables in ilua must be declared before use + * -L is like -l, except it automatically brings it into the global NS + * require() wrapper that does this ↑ + * table display logic control, float precision control + * print\_handler (custom print logic for types) + * \_\_pretty + * global\_handler (custom lookup logic to complement strict mode) + * easily done via a plugin + * line\_handler (custom handling of lines before being processed) + * Steal ideas from luaish + * Shell commands (lines beginning with ., filename completion) + * Steal ideas from http://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print\_loop + * Steal ideas from pry, ipython, bpython, Devel::REPL, Factor REPL + * Steal ideas from https://github.com/tpope/vim-foreplay + * Async implementation + * GTK implementation + * IRC bot implementation + * Awesome library diff --git a/lib/lua-repl/dev/RELEASE-MESSAGE b/lib/lua-repl/dev/RELEASE-MESSAGE new file mode 100644 index 00000000..0fdbdf34 --- /dev/null +++ b/lib/lua-repl/dev/RELEASE-MESSAGE @@ -0,0 +1,21 @@ +To: <lua-l@lists.lua.org> +Subject: [ANN] lua-repl $VERSION + +Hi everyone, + +I'm happy to announce a new version of lua-repl! + +For those of you who aren't aware of it, lua-repl is a library to make +it easy to embed a REPL similar to the standalone interpreter in your +Lua application. It also comes with a suite of plugins for enhancing +itself, as well as a "demo" console REPL that may be used as an +alternative to the standalone REPL (named rep.lua). + +$CHANGE_SUMMARY + +The source for lua-repl is located on Github: +https://github.com/hoelzro/lua-repl. The release tarball is here: +https://github.com/hoelzro/lua-repl/archive/$VERSION.tar.gz + +Thanks, +Rob diff --git a/lib/lua-repl/luarepl-0.10-1.rockspec b/lib/lua-repl/luarepl-0.10-1.rockspec new file mode 100644 index 00000000..4b3401a5 --- /dev/null +++ b/lib/lua-repl/luarepl-0.10-1.rockspec @@ -0,0 +1,38 @@ +package = 'luarepl' +version = '0.10-1' +source = { + url = 'https://github.com/hoelzro/lua-repl/archive/0.10.tar.gz', + dir = 'lua-repl-0.10', +} +description = { + summary = 'A reusable REPL component for Lua, written in Lua', + homepage = 'https://github.com/hoelzro/lua-repl', + license = 'MIT/X11', +} +dependencies = { + 'lua >= 5.1' +} +build = { + type = 'builtin', + modules = { + ['repl'] = 'repl/init.lua', + ['repl.utils'] = 'repl/utils.lua', + ['repl.sync'] = 'repl/sync.lua', + ['repl.compat'] = 'repl/compat.lua', + ['repl.console'] = 'repl/console.lua', + ['repl.plugins.autoreturn'] = 'repl/plugins/autoreturn.lua', + ['repl.plugins.completion'] = 'repl/plugins/completion.lua', + ['repl.plugins.example'] = 'repl/plugins/example.lua', + ['repl.plugins.history'] = 'repl/plugins/history.lua', + ['repl.plugins.keep_last_eval'] = 'repl/plugins/keep_last_eval.lua', + ['repl.plugins.linenoise'] = 'repl/plugins/linenoise.lua', + ['repl.plugins.pretty_print'] = 'repl/plugins/pretty_print.lua', + ['repl.plugins.rcfile'] = 'repl/plugins/rcfile.lua', + ['repl.plugins.semicolon_suppress_output'] = 'repl/plugins/semicolon_suppress_output.lua', + ['repl.plugins.filename_completion'] = 'repl/plugins/filename_completion.lua', + ['repl.plugins.rlwrap'] = 'repl/plugins/rlwrap.lua', + }, + install = { + bin = { 'rep.lua' }, + } +} diff --git a/lib/lua-repl/plugins.md b/lib/lua-repl/plugins.md new file mode 100644 index 00000000..2a3591b4 --- /dev/null +++ b/lib/lua-repl/plugins.md @@ -0,0 +1,285 @@ +# Plugins + +As of version 0.3, lua-repl supports a plugin mechanism. For an example, please download the source code for +lua-repl and look at `repl/plugins/example.lua`. + +# Available Plugins + +lua-repl 0.3 ships with several plugins. If you use the example script `rep.lua`, some of them are loaded automatically; +these are marked with an asterisk. + +## autoreturn (\*) + +Automatically returns the the results of the evaluated expression. So instead of having to type 'return 3', you may +simply type '3'. + +## completion (\*) + +Provides completion facilities via the 'completion' feature. Other plugins may hook into this to provide tab completion. + +## example + +Simply an example plugin. + +## history (\*) + +Provides history facilities (and the 'history' feature), storing each line entered as an individual history entry, +and persisting the history in `$HOME/.rep.lua.history`. Other plugins may hook into this. + +## keep\_last\_eval + +Retains the results of the previously evaluated expression in global variables. The first result is stored in `_1`, the +second in `_2`, etc. For brevity's sake, the first result is also stored in `_`. + +## linenoise (\*) + +Hooks into the linenoise library. Allows the use of tab completion and history. + +## pretty\_print + +'Pretty prints' return values. Tables are printed in expanded form. Colors are provided if lua-term is installed. + +## rcfile (\*) + +Loads Lua code in `$HOME/.rep.lua` if the file exists. The repl object is provided to the file in a variable named `repl`, so +users may load plugins of their choosing. + +## semicolon_suppress_output + +Suppresses automatic printing of an expression's result if the expression ends in a semicolon. + +# Creating a Plugin + +If you would like to create your own plugin, it must be in a file under `repl/plugins/` in your `package.path`. + +# Plugin Objects + +When a plugin is loaded, it is provided with five special objects that are used to affect the behavior of the REPL +object loading the plugin. + +## repl + +The `repl` object is a proxy object for the REPL object loading the plugin; you can invoke methods on it, create methods on +it, set properties on it, etc. However, if you try to create a method on the `repl` object that already exists, an error +will occur. This is to keep plugin authors from stepping on each others' toes. + +## before + +The `before` object is another proxy object from the plugin environment. If you add methods to the `before` object, the original +method remains intact; however, the method you added will be called before the original. For example, if you wanted to print +"I got some results!" before you display them on the command line, your plugin could do this: + +```lua +function before:displayresults(results) + print 'I got some results!' +end +``` + +This is called *advice*, and is stolen from Moose, an object system for the Perl programming language. + +When you apply multiple pieces of advice via `before`, they are called in last-in-first-out order: + +```lua +function before:method() + print 'Second!' +end + +function before:method() + print 'First!' +end +``` + +`before` also receives all of the parameters to the original method. If they are tables, userdata, etc, you may alter them, +which can alter the behavior of the original method, for better or for worse. + +If the method you are applying advice to does not exist on the current REPL object, an error will occur. This way, developers +can find out about API changes quickly, albeit noisily. + +## after + +The `after` object is another proxy object that attaches advice to the loading REPL object. As you can likely tell from its name, +advice applied via the `after` object occurs after the original method. Advice applied via after is called in first-in-first-out order: + +```lua +function after:method() + print 'First!' +end + +function after:method() + print 'Second!' +end +``` + +Like `before`, if you try applying advice to a method that doesn't exist, an error will occur. Also like `before`, after advice receives +all of the parameters passed to the original method. + +## around + +The `around` object is another advice object, but it works a little differently than `before` or `after`. `around` replaces the current +method will the advice, and like `before` and `after`, receives all of the parameters that would be passed to the original. However, +`around` also receives an additional parameter immediately before the parameters: the original method. This way, you can invoke +the method's original functionality if needed. For example: + +```lua +function around:displayresults(orig, results) + print "I'm displaying some results!" + orig(self, results) -- don't forget self! + print "Now I'm done!" +end +``` + +Like the other advice objects, you can't apply advice to a method that doesn't exist. Also, be warned: the `around` advice does nothing +to make sure that the parameters are passed to the original function, and it doesn't make sure that the return values from the original +function are returned. You need to do that yourself. + +## override + +The `override` object isn't really an advice object; adding methods to it will replace the methods in the REPL object itself. However, +it will fail if that method does not already exist. The rule of thumb is if you want to add new methods to the REPL object, use +`repl`; if you want to completely override an existing method, use `override`. Keep in mind this will blow away all advice applied to +a method from other plugins; use with caution! + +# Features + +Sometimes, different plugins will want to provide a method, but implemented in a different way. For example, the completion plugin +included with lua-repl implements a tab completion method; however, if you are embedding lua-repl into your own environment, you may +have a more sophisicated way to provide completions. Other plugins (like the linenoise plugin) may want to hook into the completion +feature itself, without being tied to a particular implementation. So plugins may advertise a list of *features* that they provide, +so that they can develop loose relationships between one another. To advertise features for your plugin, simply set the features variable: + +```lua +features = 'completion' -- make sure you're not setting a local! +``` + +If you wish you provide multiple features, simply use a table: + +```lua +features = { 'completion', 'something_else' } +``` + +Obviously, plugins providing a feature need to agree on a standard interface of methods that they provide. No framework is in place for this as +of yet. + +REPL objects may provide features as well; for example, `repl.console` provides the 'console' feature. You can use this to make sure your +plugins are only loaded in certain environments. + +# REPL Methods + +Now that you know how to affect the behavior of lua-repl with plugins, let's go over the methods you may advise/override, or call yourself from +within your advised/overridden methods. Please keep in mind that since lua-repl is still a young project, this API is subject to change. + +## repl:getprompt(level) + +Returns the prompt string displayed for the given prompt level, which is either 1 or 2. +1 signifies that the REPL is not in a multi-line expression (like a for loop); 2 signifies +otherwise. + +## repl:prompt(level) + +Actually displays the prompt for the given level. You more likely want to deal with +getprompt or showprompt. + +## repl:name() + +Returns the name of the REPL, used when compiling the chunks for evaluation. + +## repl:traceback(err) + +Returns a stack trace, prefixed by the given error message. + +## repl:detectcontinue(err) + +Detects whether or not the given error message means that more input is needed for a complete +chunk. You probably shouldn't touch this. + +## repl:compilechunk(code) + +Compiles the given chunk of code, returning a function, or a falsy value and an error message. + +## repl:getcontext() + +Returns the function environment that the REPL evaluates code in. + +## repl:handleline(line) + +Handles a line of input, returning the prompt level (1 or 2). Note that if this method is +called, an evaluation does not necessarily occur. + +## repl:showprompt(prompt) + +Displays the given prompt. + +## repl:displayresults(results) + +Displays the results from an evaluation. `results` is a table with the individual values in +the integer indices of the table, with the `n` key containing the number of values in the table. + +## repl:displayerror(err) + +Displays an error from an evaluation. + +## repl:hasplugin(plugin) + +Returns `true` if the given plugin has been loaded, `false` otherwise. + +## repl:hasfeature(feature) + +Returns `true` if the given feature has been loaded, `false` otherwise. + +## repl:requirefeature(feature) + +If the given feature has been loaded, do nothing. Otherwise, raise an error. + +## repl:ifplugin(plugin, action) + +If the given plugin has been loaded, call `action`. Otherwise, if the plugin +is ever loaded in the future, call `action` after that loading occurs. + +## repl:iffeature(feature, action) + +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. + +## repl:loadplugin(plugin) + +Loads the given plugin. If the plugin returns a value, that value is returned. + +## repl:shutdown() + +Called when the REPL is exited. Don't call this yourself! + +## repl:lines() -- repl.sync only + +Returns an iterator that yields a line of input per invocation. + +# The Future + +This is the first release of lua-repl with plugins. The future will bring various +refinements to the plugin interface, along with the following planned features: + +## Feature Interfaces + +Earlier I mentioned that features have a sort of "gentlemen's aggreement" on what +methods they will provide. It would be nice if the plugin system had a way of +enforcing that. + +## Attribute Storage + +Currently, if a plugin wants to store some information between method calls, it needs +to store it on the REPL object (`self`) and hope no other plugins (or REPL clone) will +use the same name. Plugin-specific storage is a high priority. + +## Configuration + +Currently, plugins don't have any sort of configuration mechanism. I plan to change that. + +## Library Plugins + +Some plugins may want to leverage functionality of others without loading those others into +the REPL itself. I call these *library plugins*. + +## Better Diagnostics + +If you try to add a method that has already been added, or provide a feature that has already been +provided, you receive no information on which plugin provided the method or feature in question. +It would be nice to know. diff --git a/lib/lua-repl/rep.lua b/lib/lua-repl/rep.lua new file mode 100755 index 00000000..029c3192 --- /dev/null +++ b/lib/lua-repl/rep.lua @@ -0,0 +1,41 @@ +#!/usr/bin/env lua + +-- 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. + +-- Not as cool a name as re.pl, but I tried. + +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/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 <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. + +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 <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.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 <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 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 <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. + +-- 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 <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. + +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 <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. + +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 <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. + +-- 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 <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. + +-- 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 <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. + +-- 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 = '<topvalue>', + 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 <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. + +-- 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 <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. + +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 <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. + +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 <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. + +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 <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. + +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 +} diff --git a/lib/lua-repl/t/abstract-repl-tests.lua b/lib/lua-repl/t/abstract-repl-tests.lua new file mode 100644 index 00000000..50d4b07c --- /dev/null +++ b/lib/lua-repl/t/abstract-repl-tests.lua @@ -0,0 +1,33 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(8) + +do -- getprompt tests {{{ + is(repl:getprompt(1), '>') + is(repl:getprompt(2), '>>') +end -- }}} + +do -- prompt abstract tests {{{ + error_like(function() + repl:prompt(1) + end, 'You must implement the showprompt method') + + error_like(function() + repl:prompt(2) + end, 'You must implement the showprompt method') +end -- }}} + +do -- name tests {{{ + is(repl:name(), 'REPL') +end -- }}} + +do -- handleline abstract tests {{{ + is(_G.testresult, nil) + error_like(function() + repl:handleline '_G.testresult = 17' + end, 'You must implement the displayresults method') + is(_G.testresult, 17) +end -- }}} diff --git a/lib/lua-repl/t/clone-repl-tests.lua b/lib/lua-repl/t/clone-repl-tests.lua new file mode 100644 index 00000000..3ec82f92 --- /dev/null +++ b/lib/lua-repl/t/clone-repl-tests.lua @@ -0,0 +1,119 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(45) + +local clone = repl:clone() +local prompt +local results +local errmsg + +isnt(type(clone), 'nil') + +function clone:showprompt(p) + prompt = p +end + +function clone:displayresults(r) + results = r +end + +function clone:displayerror(err) + errmsg = err +end + +do -- prompt tests {{{ + lives_ok(function() + clone:prompt(1) + end) + + is(prompt, '>') + + lives_ok(function() + clone:prompt(2) + end) + + is(prompt, '>>') +end -- }}} + +do -- handleline tests {{{ + is(_G.testresult, nil) + is(results, nil) + + lives_ok(function() + clone:handleline '_G.testresult = 18' + end) + + is(_G.testresult, 18) + + is(type(results), 'table') + is(results.n, 0) + is(#results, 0) + + lives_ok(function() + clone:handleline 'return 19' + end) + + is(type(results), 'table') + is(results.n, 1) + is(#results, 1) + is(results[1], 19) + + lives_ok(function() + clone:handleline 'return 20, 21, 22' + end) + + is(type(results), 'table') + is(results.n, 3) + is(#results, 3) + is(results[1], 20) + is(results[2], 21) + is(results[3], 22) + + lives_ok(function() + clone:handleline 'return 1, nil, nil, nil, nil, nil, nil, 2' + end) + + is(type(results), 'table') + is(results.n, 8) + is(results[1], 1) + for i = 2, 7 do + is(results[i], nil) + end + is(results[8], 2) +end -- }}} + +do -- error handling tests {{{ + lives_ok(function() + clone:handleline '3 4' + end) + + isnt(type(errmsg), 'nil') + + errmsg = nil + + lives_ok(function() + clone:handleline 'error "foo"' + end) + + like(errmsg, 'foo') +end -- }}} + +do -- multi-line input tests {{{ + errmsg = nil + _G.t = {} + + lives_ok(function() + clone:handleline 'for i = 1, 3 do' + clone:handleline ' table.insert(_G.t, i)' + clone:handleline 'end' + end) + + is(errmsg, nil) + is(#_G.t, 3) + is(_G.t[1], 1) + is(_G.t[2], 2) + is(_G.t[3], 3) +end -- }}} diff --git a/lib/lua-repl/t/lib/test-utils.lua b/lib/lua-repl/t/lib/test-utils.lua new file mode 100644 index 00000000..a0bb16ac --- /dev/null +++ b/lib/lua-repl/t/lib/test-utils.lua @@ -0,0 +1,56 @@ +local _M = {} + +function _M.next_line_number() + local info = debug.getinfo(2, 'l') + return info.currentline + 1 -- doesn't work with whitespace +end + +function _M.cmp_tables(lhs, rhs) + local ok = true + local got + local expected + local failing_k + + for k, v in pairs(lhs) do + local rv = rhs[k] + + if v ~= rv then + ok = false + failing_k = k + got = v + expected = rv + break + end + end + + if ok then + for k, v in pairs(rhs) do + local lv = lhs[k] + + if v ~= lv then + ok = false + failing_k = k + got = lv + expected = v + break + end + end + end + + if ok then + pass() + else + fail 'value mismatch' + diag(string.format(' got[%q]: %s', tostring(failing_k), tostring(got))) + diag(string.format('expected[%q]: %s', tostring(failing_k), tostring(expected))) + end +end + +function _M.gather_results(...) + return { + n = select('#', ...), + ..., + } +end + +return _M diff --git a/lib/lua-repl/t/plugin-after-tests.lua b/lib/lua-repl/t/plugin-after-tests.lua new file mode 100644 index 00000000..15a24c11 --- /dev/null +++ b/lib/lua-repl/t/plugin-after-tests.lua @@ -0,0 +1,202 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(26) + +local clone = repl:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + local has_called_normal + local has_called_after + + function with_plugin:foo() + has_called_normal = true + end + + with_plugin:loadplugin(function() + function after:foo() + has_called_after = true + ok(has_called_normal) + end + end) + + with_plugin:foo() + ok(has_called_normal) + ok(has_called_after) + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function after:nonexistent() + end + end) + end) + + like(err, string.format("%d: The 'nonexistent' method does not exist", line_no)) + + _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + after.foo = 17 + end) + end) + + like(err, string.format('%d: 17 is not a function', line_no)) +end -- }}} + +do -- arguments tests {{{ + local with_plugin = clone:clone() + local orig_args + local got_args + + function with_plugin:foo(...) + orig_args = { + n = select('#', ...), + ..., + } + end + + with_plugin:loadplugin(function() + function after:foo(...) + got_args = { + n = select('#', ...), + ..., + } + end + end) + + with_plugin:foo() + is(got_args.n, 0) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, 2, 3) + is(got_args.n, 3) + is(got_args[1], 1) + is(got_args[2], 2) + is(got_args[3], 3) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, nil, nil, nil, 5) + is(got_args.n, 5) + is(got_args[1], 1) + is(got_args[2], nil) + is(got_args[3], nil) + is(got_args[4], nil) + is(got_args[5], 5) + utils.cmp_tables(orig_args, got_args) +end -- }}} + +do -- exception tests {{{ + local with_plugin = clone:clone() + + local has_called_original + + function with_plugin:foo() + has_called_original = true + end + + with_plugin:loadplugin(function() + function after:foo() + error 'uh-oh' + end + end) + + local _, err = pcall(with_plugin.foo, with_plugin) + + like(err, 'uh%-oh') + ok(has_called_original) +end -- }}} + +do -- return value tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + return 17 + end + + function with_plugin:bar() + return 18, 19, 20 + end + + function with_plugin:baz() + return 1, nil, nil, nil, 5 + end + + with_plugin:loadplugin(function() + function after:foo() + return 18 + end + + function after:bar() + return 18 + end + + function after:baz() + return 18 + end + end) + + local result = with_plugin:foo() + is(result, 17) + + local results = utils.gather_results(with_plugin:bar()) + utils.cmp_tables(results, { n = 3, 18, 19, 20 }) + + results = utils.gather_results(with_plugin:baz()) + utils.cmp_tables(results, { n = 5, 1, nil, nil, nil, 5 }) +end -- }}} + +do -- multiple advice, multiple plugins {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function after:foo() + calls[#calls + 1] = 'first' + end + end) + + with_plugin:loadplugin(function() + function after:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'original', 'first', 'second' }) +end -- }}} + +do -- multiple advice, single plugin {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function after:foo() + calls[#calls + 1] = 'first' + end + + function after:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'original', 'first', 'second' }) +end -- }}} diff --git a/lib/lua-repl/t/plugin-around-tests.lua b/lib/lua-repl/t/plugin-around-tests.lua new file mode 100644 index 00000000..df0ea073 --- /dev/null +++ b/lib/lua-repl/t/plugin-around-tests.lua @@ -0,0 +1,218 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +local compat = require 'repl.compat' +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(27) + +local clone = repl:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + local has_called_normal + local has_called_around + + function with_plugin:foo() + has_called_normal = true + end + + with_plugin:loadplugin(function() + function around:foo(orig, ...) + has_called_around = true + ok(not has_called_normal) + local return_values = utils.gather_results(orig(self, ...)) + ok(has_called_normal) + return compat.unpack(return_values, 1, return_values.n) + end + end) + + with_plugin:foo() + ok(has_called_normal) + ok(has_called_around) + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function around:nonexistent() + end + end) + end) + + like(err, string.format("%d: The 'nonexistent' method does not exist", line_no)) + + _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + around.foo = 17 + end) + end) + + like(err, string.format('%d: 17 is not a function', line_no)) +end -- }}} + +do -- arguments tests {{{ + local with_plugin = clone:clone() + local orig_args + local got_args + + function with_plugin:foo(...) + orig_args = { + n = select('#', ...), + ..., + } + end + + with_plugin:loadplugin(function() + function around:foo(orig, ...) + got_args = { + n = select('#', ...), + ..., + } + + return orig(self, ...) + end + end) + + with_plugin:foo() + is(got_args.n, 0) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, 2, 3) + is(got_args.n, 3) + is(got_args[1], 1) + is(got_args[2], 2) + is(got_args[3], 3) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, nil, nil, nil, 5) + is(got_args.n, 5) + is(got_args[1], 1) + is(got_args[2], nil) + is(got_args[3], nil) + is(got_args[4], nil) + is(got_args[5], 5) + utils.cmp_tables(orig_args, got_args) +end -- }}} + +do -- exception tests {{{ + local with_plugin = clone:clone() + + local has_called_original + + function with_plugin:foo() + has_called_original = true + end + + with_plugin:loadplugin(function() + function around:foo() + error 'uh-oh' + end + end) + + local _, err = pcall(with_plugin.foo, with_plugin) + + like(err, 'uh%-oh') + ok(not has_called_original) +end -- }}} + +do -- return value tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + return 17 + end + + function with_plugin:bar() + return 18, 19, 20 + end + + function with_plugin:baz() + return 1, nil, nil, nil, 5 + end + + with_plugin:loadplugin(function() + function around:foo() + return 18 + end + + function around:bar() + return 19, 20, 21 + end + + function around:baz() + return 1, nil, nil, 4 + end + end) + + local result = with_plugin:foo() + is(result, 18) + + local results = utils.gather_results(with_plugin:bar()) + utils.cmp_tables(results, { n = 3, 19, 20, 21 }) + + results = utils.gather_results(with_plugin:baz()) + utils.cmp_tables(results, { n = 4, 1, nil, nil, 4 }) +end -- }}} + +do -- multiple advice, multiple plugins {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function around:foo(orig) + calls[#calls + 1] = 'before_one' + orig() + calls[#calls + 1] = 'after_one' + end + end) + + with_plugin:loadplugin(function() + function around:foo(orig) + calls[#calls + 1] = 'before_two' + orig() + calls[#calls + 1] = 'after_two' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'before_two', 'before_one', 'original', + 'after_one', 'after_two' }) +end -- }}} + +do -- multiple advice, single plugin {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function around:foo(orig) + calls[#calls + 1] = 'before_one' + orig() + calls[#calls + 1] = 'after_one' + end + + function around:foo(orig) + calls[#calls + 1] = 'before_two' + orig() + calls[#calls + 1] = 'after_two' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'before_two', 'before_one', 'original', + 'after_one', 'after_two' }) +end -- }}} diff --git a/lib/lua-repl/t/plugin-basic-tests.lua b/lib/lua-repl/t/plugin-basic-tests.lua new file mode 100644 index 00000000..21283ef8 --- /dev/null +++ b/lib/lua-repl/t/plugin-basic-tests.lua @@ -0,0 +1,206 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +local utils = require 'test-utils' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(28) + +local clone = repl:clone() + +do -- basic tests {{{ + local loaded + + clone:loadplugin(function() + loaded = true + end) + + ok(loaded) + + error_like(function() + clone:loadplugin(function() + error 'uh-oh' + end) + end, 'uh%-oh') + +end -- }}} + +do -- loading the same plugin twice {{{ + local function plugin() + end + + local line_no + + clone:loadplugin(plugin) + local _, err = pcall(function() + line_no = utils.next_line_number() + clone:loadplugin(plugin) + end) + like(err, tostring(line_no) .. ': plugin "function:%s+%S+" has already been loaded') + + _, err = pcall(function() + line_no = utils.next_line_number() + clone:clone():loadplugin(plugin) + end) + like(err, tostring(line_no) .. ': plugin "function:%s+%S+" has already been loaded') + + repl:clone():loadplugin(plugin) + repl:clone():loadplugin(plugin) +end -- }}} + +do -- loading plugins by name {{{ + local loaded + + package.preload['repl.plugins.test'] = function() + loaded = true + end + + clone:clone():loadplugin 'test' + + ok(loaded) + loaded = false + + clone:clone():loadplugin 'test' + + ok(loaded, 'loading a plugin twice should initialize it twice') + + package.preload['repl.plugins.test'] = function() + error 'uh-oh' + end + + error_like(function() + clone:clone():loadplugin 'test' + end, 'uh%-oh') + + package.preload['repl.plugins.test'] = nil + + local line_no + + local _, err = pcall(function() + line_no = utils.next_line_number() + clone:clone():loadplugin 'test' + end) + like(err, tostring(line_no) .. ': unable to locate plugin') +end -- }}} + +do -- hasplugin tests {{{ + local child = repl:clone() + + local plugin = function() + end + + child:loadplugin(plugin) + + local grandchild = child:clone() + + ok(not repl:hasplugin(plugin)) + ok(child:hasplugin(plugin)) + ok(grandchild:hasplugin(plugin)) + + plugin = function() + end + + child:loadplugin(plugin) + + ok(not repl:hasplugin(plugin)) + ok(child:hasplugin(plugin)) + ok(not grandchild:hasplugin(plugin)) +end -- }}} + +do -- global tests {{{ + local clone = repl:clone() + local line_no + + local _, err = pcall(function() + clone:loadplugin(function() + line_no = utils.next_line_number() + foo = 17 + end) + end) + + like(err, tostring(line_no) .. ': global environment is read%-only %(key = "foo"%)') + + _, err = pcall(function() + clone:loadplugin(function() + line_no = utils.next_line_number() + _G.foo = 17 + end) + end) + + like(err, tostring(line_no) .. ': global environment is read%-only %(key = "foo"%)') +end -- }}} + +do -- ifplugin tests {{{ + local clone = repl:clone() + local has_run + + package.preload['repl.plugins.test'] = function() + end + + clone:ifplugin('test', function() + has_run = true + end) + + ok(not has_run) + + clone:loadplugin 'test' + + ok(has_run) + + has_run = false + + clone:ifplugin('test', function() + has_run = true + end) + + ok(has_run) +end -- }}} + +do -- ifplugin multiple times {{{ + local clone = repl:clone() + local has_run + local has_run2 + + package.preload['repl.plugins.test'] = function() + end + + clone:ifplugin('test', function() + has_run = true + end) + + clone:ifplugin('test', function() + has_run2 = true + end) + + clone:loadplugin 'test' + + ok(has_run) + ok(has_run2) +end -- }}} + +do -- plugin return value {{{ + local clone = repl:clone() + + local result = clone:loadplugin(function() + return 17 + end) + + local result2 = clone:loadplugin(function() + end) + + local result3, result4 = clone:loadplugin(function() + return 18, 19 + end) + + local result5, result6, result7 = clone:loadplugin(function() + return 20, nil, 21 + end) + + is(result, 17) + is(result2, nil) + is(result3, 18) + is(result4, 19) + is(result5, 20) + is(result6, nil) + is(result7, 21) +end -- }}} diff --git a/lib/lua-repl/t/plugin-before-tests.lua b/lib/lua-repl/t/plugin-before-tests.lua new file mode 100644 index 00000000..cc23ba78 --- /dev/null +++ b/lib/lua-repl/t/plugin-before-tests.lua @@ -0,0 +1,201 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(26) + +local clone = repl:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + local has_called_normal + local has_called_before + + function with_plugin:foo() + has_called_normal = true + end + + with_plugin:loadplugin(function() + function before:foo() + has_called_before = true + ok(not has_called_normal) + end + end) + + with_plugin:foo() + ok(has_called_normal) + ok(has_called_before) + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function before:nonexistent() + end + end) + end) + + like(err, string.format("%d: The 'nonexistent' method does not exist", line_no)) + + _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + before.foo = 17 + end) + end) + + like(err, string.format('%d: 17 is not a function', line_no)) +end -- }}} + +do -- arguments tests {{{ + local with_plugin = clone:clone() + local orig_args + local got_args + + function with_plugin:foo(...) + orig_args = { + n = select('#', ...), + ..., + } + end + + with_plugin:loadplugin(function() + function before:foo(...) + got_args = { + n = select('#', ...), + ..., + } + end + end) + + with_plugin:foo() + is(got_args.n, 0) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, 2, 3) + is(got_args.n, 3) + is(got_args[1], 1) + is(got_args[2], 2) + is(got_args[3], 3) + utils.cmp_tables(orig_args, got_args) + + with_plugin:foo(1, nil, nil, nil, 5) + is(got_args.n, 5) + is(got_args[1], 1) + is(got_args[2], nil) + is(got_args[3], nil) + is(got_args[4], nil) + is(got_args[5], 5) + utils.cmp_tables(orig_args, got_args) +end -- }}} + +do -- exception tests {{{ + local with_plugin = clone:clone() + + local has_called_original + + function with_plugin:foo() + has_called_original = true + end + + with_plugin:loadplugin(function() + function before:foo() + error 'uh-oh' + end + end) + + local _, err = pcall(with_plugin.foo, with_plugin) + + like(err, 'uh%-oh') + ok(not has_called_original) +end -- }}} + +do -- return value tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + return 17 + end + + function with_plugin:bar() + return 18, 19, 20 + end + + function with_plugin:baz() + return 1, nil, nil, nil, 5 + end + + with_plugin:loadplugin(function() + function before:foo() + return 18 + end + + function before:bar() + return 18 + end + + function before:baz() + end + end) + + local result = with_plugin:foo() + is(result, 17) + + local results = utils.gather_results(with_plugin:bar()) + utils.cmp_tables(results, { n = 3, 18, 19, 20 }) + + results = utils.gather_results(with_plugin:baz()) + utils.cmp_tables(results, { n = 5, 1, nil, nil, nil, 5 }) +end -- }}} + +do -- multiple advice, multiple plugins {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function before:foo() + calls[#calls + 1] = 'first' + end + end) + + with_plugin:loadplugin(function() + function before:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'second', 'first', 'original' }) +end -- }}} + +do -- multiple advice, single plugin {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function before:foo() + calls[#calls + 1] = 'first' + end + + function before:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'second', 'first', 'original' }) +end -- }}} diff --git a/lib/lua-repl/t/plugin-feature-tests.lua b/lib/lua-repl/t/plugin-feature-tests.lua new file mode 100644 index 00000000..4c74a4c1 --- /dev/null +++ b/lib/lua-repl/t/plugin-feature-tests.lua @@ -0,0 +1,132 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +local utils = require 'test-utils' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(19) + +do -- basic tests {{{ + local clone = repl:clone() + + clone:loadplugin(function() + features = 'foo' + end) + + ok(clone:hasfeature 'foo') + ok(not clone:hasfeature 'bar') + ok(not clone:hasfeature 'baz') + + clone:loadplugin(function() + features = { 'bar', 'baz' } + end) + + ok(clone:hasfeature 'foo') + ok(clone:hasfeature 'bar') + ok(clone:hasfeature 'baz') +end -- }}} + +do -- requirefeature {{{ + local clone = repl:clone() + + clone:loadplugin(function() + features = 'foo' + end) + + clone:requirefeature 'foo' + + local line_no + local _, err = pcall(function() + line_no = utils.next_line_number() + clone:requirefeature 'bar' + end) + + like(err, tostring(line_no) .. ': required feature "bar" not present') +end -- }}} + +do -- conflicts {{{ + local clone = repl:clone() + local line_no + + clone:loadplugin(function() + features = 'foo' + end) + + local _, err = pcall(function() + line_no = utils.next_line_number() + clone:loadplugin(function() + features = 'foo' + end) + end) + like(err, tostring(line_no) .. ': feature "foo" already present') + + -- XXX what about methods injected into the object? +end -- }}} + +do -- clone:hasfeature {{{ + local child = repl:clone() + + child:loadplugin(function() + features = 'foo' + end) + + local grandchild = child:clone() + + ok(not repl:hasfeature 'foo') + ok(child:hasfeature 'foo') + ok(grandchild:hasfeature 'foo') + + child:loadplugin(function() + features = 'bar' + end) + + ok(not repl:hasfeature 'bar') + ok(child:hasfeature 'bar') + ok(not grandchild:hasfeature 'bar') +end -- }}} + +do -- iffeature tests {{{ + local clone = repl:clone() + local has_run + + clone:iffeature('foo', function() + has_run = true + end) + + ok(not has_run) + + clone:loadplugin(function() + features = 'foo' + end) + + ok(has_run) + + has_run = false + + clone:iffeature('foo', function() + has_run = true + end) + + ok(has_run) +end -- }}} + +do -- iffeature multiple times {{{ + local clone = repl:clone() + local has_run + local has_run2 + + clone:iffeature('foo', function() + has_run = true + end) + + clone:iffeature('foo', function() + has_run2 = true + end) + + clone:loadplugin(function() + features = 'foo' + end) + + ok(has_run) + ok(has_run2) +end -- }}} diff --git a/lib/lua-repl/t/plugin-override-tests.lua b/lib/lua-repl/t/plugin-override-tests.lua new file mode 100644 index 00000000..6ea11d8b --- /dev/null +++ b/lib/lua-repl/t/plugin-override-tests.lua @@ -0,0 +1,201 @@ +-- vim:foldmethod=marker +local repl = require 'repl' +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(25) + +local clone = repl:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + local has_called_normal + local has_called_override + + function with_plugin:foo() + has_called_normal = true + end + + with_plugin:loadplugin(function() + function override:foo() + has_called_override = true + end + end) + + with_plugin:foo() + ok(not has_called_normal) + ok(has_called_override) + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function override:nonexistent() + end + end) + end) + + like(err, string.format("%d: The 'nonexistent' method does not exist", line_no)) + + _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + override.foo = 17 + end) + end) + + like(err, string.format('%d: 17 is not a function', line_no)) +end -- }}} + +do -- arguments tests {{{ + local with_plugin = clone:clone() + local orig_args + local got_args + + function with_plugin:foo(...) + orig_args = { + n = select('#', ...), + ..., + } + end + + with_plugin:loadplugin(function() + function override:foo(...) + got_args = { + n = select('#', ...), + ..., + } + end + end) + + with_plugin:foo() + is(got_args.n, 0) + ok(not orig_args) + + with_plugin:foo(1, 2, 3) + is(got_args.n, 3) + is(got_args[1], 1) + is(got_args[2], 2) + is(got_args[3], 3) + ok(not orig_args) + + with_plugin:foo(1, nil, nil, nil, 5) + is(got_args.n, 5) + is(got_args[1], 1) + is(got_args[2], nil) + is(got_args[3], nil) + is(got_args[4], nil) + is(got_args[5], 5) + ok(not orig_args) +end -- }}} + +do -- exception tests {{{ + local with_plugin = clone:clone() + + local has_called_original + + function with_plugin:foo() + has_called_original = true + end + + with_plugin:loadplugin(function() + function override:foo() + error 'uh-oh' + end + end) + + local _, err = pcall(with_plugin.foo, with_plugin) + + like(err, 'uh%-oh') + ok(not has_called_original) +end -- }}} + +do -- return value tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + return 17 + end + + function with_plugin:bar() + return 18, 19, 20 + end + + function with_plugin:baz() + return 1, nil, nil, nil, 5 + end + + with_plugin:loadplugin(function() + function override:foo() + return 18 + end + + function override:bar() + return 19, 20, 21 + end + + function override:baz() + return 1, nil, nil, 4 + end + end) + + local result = with_plugin:foo() + is(result, 18) + + local results = utils.gather_results(with_plugin:bar()) + utils.cmp_tables(results, { n = 3, 19, 20, 21 }) + + results = utils.gather_results(with_plugin:baz()) + utils.cmp_tables(results, { n = 4, 1, nil, nil, 4 }) +end -- }}} + +do -- multiple advice, multiple plugins {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function override:foo() + calls[#calls + 1] = 'first' + end + end) + + with_plugin:loadplugin(function() + function override:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'second' }) +end + +do -- multiple advice, single plugin {{{ + local with_plugin = clone:clone() + local calls = {} + + function with_plugin:foo() + calls[#calls + 1] = 'original' + end + + with_plugin:loadplugin(function() + function override:foo() + calls[#calls + 1] = 'first' + end + + function override:foo() + calls[#calls + 1] = 'second' + end + end) + + with_plugin:foo() + + utils.cmp_tables(calls, { 'second' }) +end diff --git a/lib/lua-repl/t/plugin-repl-tests.lua b/lib/lua-repl/t/plugin-repl-tests.lua new file mode 100644 index 00000000..b3aa7665 --- /dev/null +++ b/lib/lua-repl/t/plugin-repl-tests.lua @@ -0,0 +1,75 @@ +-- vim:foldmethod=marker +local r = require 'repl' -- we don't call it 'repl' so we don't shadow + -- repl in the plugin environment +pcall(require, 'luarocks.loader') +require 'Test.More' +local utils = require 'test-utils' + +plan(5) + +local clone = r:clone() + +do -- basic tests {{{ + local with_plugin = clone:clone() + + function with_plugin:foo() + end + + local line_no + + local _, err = pcall(function() + with_plugin:loadplugin(function() + line_no = utils.next_line_number() + function repl:foo() + end + end) + end) + + like(err, string.format("%d: The 'foo' method already exists", line_no)) + + with_plugin:loadplugin(function() + function repl:bar() + return 17 + end + end) + + is(with_plugin:bar(), 17) + + with_plugin:loadplugin(function() + repl.baz = 18 + end) + + is(with_plugin.baz, 18) +end -- }}} + +do -- conflict tests {{{ + local clone = r:clone() + local line_no + + clone:loadplugin(function() + function repl:foo() + end + end) + + local _, err = pcall(function() + clone:loadplugin(function() + line_no = utils.next_line_number() + function repl:foo() + end + end) + end) + + like(err, tostring(line_no) .. ": The 'foo' method already exists") +end -- }}} + +do -- proxy tests {{{ + local clone = r:clone() + + clone:loadplugin(function() + features = 'foo' + end) + + clone:loadplugin(function() + ok(repl:hasfeature 'foo') + end) +end -- }}} diff --git a/lib/lua-repl/t/sync-repl-tests.lua b/lib/lua-repl/t/sync-repl-tests.lua new file mode 100644 index 00000000..d5249ec2 --- /dev/null +++ b/lib/lua-repl/t/sync-repl-tests.lua @@ -0,0 +1,52 @@ +-- vim:foldmethod=marker +local sync = require 'repl.sync' +pcall(require, 'luarocks.loader') +require 'Test.More' + +plan(13) + +local clone = sync:clone() +local resultlist = {} + +function clone:lines() + local index = 0 + local function iterator(s) + index = index + 1 + return s[index] + end + + return iterator, { + 'return foo', + 'return 1', + 'return "bar"', + 'return {}', + 'return 1, 2, 3', + } +end + +function clone:showprompt() +end + +function clone:displayresults(results) + resultlist[#resultlist + 1] = results +end + +clone:run() + +is(#resultlist, 5) +is(resultlist[1].n, 1) +is(resultlist[1][1], nil) + +is(resultlist[2].n, 1) +is(resultlist[2][1], 1) + +is(resultlist[3].n, 1) +is(resultlist[3][1], 'bar') + +is(resultlist[4].n, 1) +is(type(resultlist[4][1]), 'table') + +is(resultlist[5].n, 3) +is(resultlist[5][1], 1) +is(resultlist[5][2], 2) +is(resultlist[5][3], 3) |
