local Object = require "core.object" local repl_loaded, repl = pcall(require, "repl") ---@class core.repl.command ---Name of the command that user can inkove. ---@field name string ---List of params displayed on help, eg: " " ---@field params? string ---Short description of the command. ---@field description string ---Function executed when the command is requested. ---@field execute fun(cmd:string,args:table) ---@class core.repl.completion ---A lua pattern to match against the current user input. ---@field pattern string ---The function to execute if the completion pattern matches. ---@field execute fun(completions:repl.completion,str:string) ---An extensible REPL with multi-line and expressions evaluation. ---@class core.repl:core.object ---List of built-in commands. ---@field commands core.repl.command[] ---Set of rules to provide input completion. ---@field completions core.repl.completion[] ---Path to the file that will hold the input history. ---@field history_file string ---Maximum amount of entries on the history file. ---@field max_history integer ---@overload fun():core.repl local REPL = Object:extend() function REPL:new() self.commands = {} self.completions = {} self.history_file = USERDIR .. PATHSEP .. "repl_history" self.max_history = 1000 self:register_default_commands() self:register_default_completions() end ---Register a new command provider. ---@param command core.repl.command ---@return boolean? registered ---@return string? errmsg function REPL:register_command(command) command.name = "." .. command.name:gsub("^%.", "") for _, cmd in ipairs(self.commands) do if cmd.name == command.name then return nil, "command with same name already registered" end end table.insert(self.commands, command) return true end ---Register a new completion provider. ---@param completion core.repl.completion ---@return boolean? registered ---@return string? errmsg function REPL:register_completion(completion) for _, c in ipairs(self.completions) do if c.pattern == completion.pattern then return nil, "completion with same pattern already registered" end end table.insert(self.completions, completion) return true end local function repl_pcall(chunk) local out = {pcall(chunk)} local ok = out[1] local result = {} for i=2, #out do table.insert(result, out[i]) end return ok, result end ---A basic REPL with multi-line and expression evaluation for the repl command. function REPL:start() local global_mt = getmetatable(_G) setmetatable(_G, nil) -- disable strict global print("Pragtical REPL. Type 'exit' or Ctrl+D to quit.") print("Enter \".help\" for usage hints.") local buffer = "" -- Custom REPL commands local function handle_command(cmd) local args = {} for word in cmd:gmatch("%S+") do table.insert(args, word) end local command = args[1] for _, c in ipairs(self.commands) do if c.name == command then c.execute(cmd, args) return true end end return false end if repl_loaded then -- Completions handler repl.set_completion(function(completion, str) for _, comp in ipairs(self.completions) do if str:ufind(comp.pattern) then comp.execute(completion, str) return end end end) repl.load_history(self.history_file) repl.set_history_max_len(self.max_history) end while true do local prompt = buffer == "" and "> " or ">> " local line if repl_loaded then line = repl.input(prompt) else io.write(prompt) line = io.read() end if not line then print("\nBye!") break end if repl_loaded then repl.add_history(line) repl.save_history(self.history_file) end if line:match("^%.") and buffer == "" then if handle_command(line) then buffer = "" goto continue end elseif line == "exit" and buffer == "" then break end buffer = buffer .. line .. "\n" local expr_chunk = load("return " .. buffer, "repl") if expr_chunk then local ok, result = repl_pcall(expr_chunk) if ok and #result > 0 then print(table.unpack(result)) elseif not ok then print("Error:", result[1]) end buffer = "" else local chunk, err = load(buffer, "repl") if chunk then local ok, result = repl_pcall(chunk) if ok and #result > 0 then print(table.unpack(result)) elseif not ok then print("Error:", result[1]) end buffer = "" elseif err and err:match("") then -- Waiting for more input else print("Syntax error:", err) buffer = "" end end ::continue:: end setmetatable(_G, global_mt) end ---Register a set of default commands. ---This is automatically called from the constructor, ---override to add your own commands. function REPL:register_default_commands() assert(not self.default_commands_called, "default commands already registered") self.default_commands_called = true self:register_command { name = "help", description = "Show this help message", execute = function() print "Available REPL commands:\n" for _, cmd in ipairs(self.commands) do print ( " " .. cmd.name .. (cmd.params and " " .. cmd.params or "") .. " " .. cmd.description ) end end } if repl_loaded then self:register_command { name = "clear", description = "Clean the screen", execute = function() repl.clear_screen() end } end self:register_command { name = "dump", description = "Print current global variables", execute = function() print("Current globals:") for k, v in pairs(_G) do if type(k) == "string" and not k:match("^_") and k ~= "_G" then local valtype = type(v) if valtype == "string" then print(k .. ' = "' .. v .. '"') elseif valtype == "number" or valtype == "boolean" then print(k .. " = " .. tostring(v)) elseif valtype == "function" then print(k .. " = function(...) end") else print(k .. " = [" .. valtype .. "]") end end end end } self:register_command { name = "load", params = "", description = "Run a Lua file in current REPL environment", execute = function(cmd, args) if args[2] then local path = cmd:match("^%.load%s+(.+)$") local chunk, err = loadfile(path) if chunk then local ok, result = pcall(chunk) if not ok then print("Error running file:", result) end else print("Failed to load file:", err) end return end print("Error: no file provided") end } self:register_command { name = "time", params = "", description = "Time how long it takes to run a line of code", execute = function(cmd, args) if args[2] then local code = cmd:match("^%.time%s+(.+)$") local chunk = load("return " .. code, "repl") if not chunk then chunk = load(code, "repl") end if chunk then local start = os.clock() local ok, result = pcall(chunk) local finish = os.clock() if ok and result ~= nil then print(result) elseif not ok then print("Error:", result) end print(string.format("Time: %.6f seconds", finish - start)) else print("Invalid code passed to .time") end return end print("Error: no code provided") end } self:register_command { name = "edit", description = "Open editor to write a Lua snippet interactively", execute = function() local core = require "core" local tempfile = core.temp_filename(".lua") local editor = {EXEFILE, "edit"} local answer local prompt = "Use the editor on EDITOR envar? [y/N]: " if repl_loaded then answer = repl.input(prompt) else io.write(prompt) answer = io.read() end if regex.match("(y|Y|yes)", answer or "") then local ed = os.getenv("EDITOR") if ed then editor = {ed} elseif PLATFORM == "Windows" then editor = {"notepad"} end end -- Create temporary file local file = io.open(tempfile, "w+") if file then file:write("") file:close() end -- Run editor table.insert(editor, tempfile) local p, perr = process.start(editor) if p then p:wait(process.WAIT_INFINITE) else print("Error executing editor: " .. (perr or "")) end -- Read edited contents file = io.open(tempfile, "r") if file then local code = file:read("*a") file:close() os.remove(tempfile) local chunk, err = load(code, "edited_snippet") if chunk then local ok, result = pcall(chunk) if ok and result ~= nil then print(result) elseif not ok then print("Error:", result) end else print("Syntax error in edited code:", err) end else print("Failed to read temporary file.") end end } self:register_command { name = "exit", description = "Exit the REPL", execute = function() os.exit() end } end ---Register default completion providers. ---This is automatically called from the constructor, ---override to add your own completions. function REPL:register_default_completions() assert(not self.default_completions_called, "default completions already registered") self.default_completions_called = true -- built-in command self:register_completion { pattern = "^%.[%w]*", execute = function(completion, str) local cmd = str:umatch("^%.[%w]*") local cmds = {} for _, bc in ipairs(self.commands) do table.insert(cmds, bc.name) end for _, c in ipairs(cmds) do if c:ufind("^%"..cmd) then completion:add(c) end end end } -- unnamed field or member access self:register_completion { pattern = "^[%a_][%w_]*[%.:]$", execute = function(completion, str) local symbol, sep = str:umatch("([%a_][%w_]*)([%.:])") local sym = _G[symbol] if sym and (type(sym) == "table" or type(sym) == "string") then if type(sym) == "string" then sym = string end for k, _ in pairs(sym) do completion:add(symbol..sep..k) end end end } -- named field or member access self:register_completion { pattern = "^[%a_][%w_]*[%.:][%a_][%w_]*$", execute = function (completion, str) local symbol, sep, field = str:umatch("([%a_][%w_]*)([%.:])([%a_][%w_]*)") local sym = _G[symbol] if sym and (type(sym) == "table" or type(sym) == "string") then if type(sym) == "string" then sym = string end for k, _ in pairs(sym) do if k:ufind("^"..field) then completion:add(symbol..sep..k) end end end end } -- symbol with space, (, [ or { at start self:register_completion { pattern = "[%s%(%[%{][%a_][%w_]*$", execute = function(completion, str) local symbol = str:umatch("[%s%(%[%{]([%a_][%w_]*)$") str = str:sub(1, #str - #symbol) for k, _ in pairs(_G) do if k:ufind("^"..symbol) then completion:add(str .. k) end end end } -- no input or global symbol self:register_completion { pattern = "^[%a_]*[%w_]*$", execute = function(completion, str) local symbol = str:umatch("[%a_][%w_]*") for k, _ in pairs(_G) do if symbol then if k:ufind("^"..symbol) then completion:add(k) end else completion:add(k) end end end } end return REPL