-- mod-version:3 -- lite-xl 2.1.8 local core = require "core" local style = require "core.style" local command = require "core.command" local View = require "core.view" local process = require "process" local config = require "core.config" local TreeView = require "plugins.treeview" local ToolbarView = require "plugins.toolbarview" core.jpdebug = core.jpdebug or {} -- Global list of all the runners core.jpdebug.runners = core.jpdebug.runners or {} -- Currently used view local active_view = nil -- The selected target local selected_target = nil -- The running system local running_proc = nil local running_runner = nil -- Table containing all debug information (like breakpoints) local debug_info = {} -- Check if required plugins are installed local required_toolbar_plugins = true if TreeView == nil or ToolbarView == nil then required_toolbar_plugins = false end -- Local helper functions ---------------------------------------- -- Dump any table to a string ---@param o any ---@param force bool|nil ---@return nil ---@diagnostic disable-next-line: unused-function local function dump(o, force) force = force or false if type(o) == 'table' or force then local s = '{ ' for k,v in pairs(o) do if type(k) ~= 'number' then k = '"'..k..'"' end s = s .. '['..k..'] = ' .. dump(v) .. ',' end return s .. '} ' else return type(o)..": "..tostring(o) end end -- Get the plugin's path ---@return string|nil local function get_plugin_directory() local paths = { USERDIR .. PATHSEP .. "plugins" .. PATHSEP .. "jpdebug", DATADIR .. PATHSEP .. "plugins" .. PATHSEP .. "jpdebug" } for _, v in ipairs(paths) do if system.get_file_info(v) then return v end end return nil end -- Simple function splitting strings local function stringsplit(inputstr, sep) if sep == nil then sep = "%s" end local t = {} for str in string.gmatch(inputstr, "([^"..sep.."]+)") do table.insert(t, str) end return t end -- ---------- JPDebugView: a simple scrollable log view ---------- ---@class JPDebugView : core.view local JPDebugView = View:extend() function JPDebugView:new(title) JPDebugView.super.new(self) self.scrollable = true self.context = "session" self.caption = title or "JP Debug" self.lines = { "ready.\n" } self.max_lines = 5000 -- keep memory bounded self.font = style.code_font self.line_h = self.font:get_height() end function JPDebugView:get_name() return self.caption end function JPDebugView:get_scrollable_size() return math.max(#self.lines * self.line_h + style.padding.y * 2, self.size.y) end function JPDebugView:push(kind, s) --TODO do some things with kind here if not s or s == "" then return end local lines = stringsplit(s, "\n") for _,l in pairs(lines) do self.lines[#self.lines + 1] = l if #self.lines > self.max_lines then local drop = #self.lines - self.max_lines for _ = 1, drop do table.remove(self.lines, 1) end end end -- autoscroll to bottom self.scroll.to.y = self:get_scrollable_size() core.redraw = true end function JPDebugView:clear() self.lines = {} core.redraw = true end function JPDebugView:draw() self:draw_background(style.background) local ox, oy = self:get_content_offset() local x = ox + style.padding.x local y = oy + style.padding.y renderer.draw_rect(self.position.x, self.position.y, self.size.x, self.size.y, style.background) for i = 1, #self.lines do renderer.draw_text(self.font, self.lines[i], x, y, style.text) y = y + self.line_h end self:draw_scrollbar() end function JPDebugView:try_close(do_close) ---@diagnostic disable-next-line: undefined-field JPDebugView.super.try_close(self, do_close) active_view = nil end -- ---------- helper: pick a target from project module ---------- local function get_targets() local t = (config.plugins and config.plugins.jpdebug and config.plugins.jpdebug.targets) or {} return t end local function get_selected_target() local t = selected_target or ((config.plugins and config.plugins.jdebug and config.plugins.jpdebug.default_target) or nil) return t end -- ---------- run target & pipe stdout/stderr into the view ---------- local function run_target(target, name) -- Check if something is alredy running if running_proc then core.error("[jpdebug] Already a runner active") return end -- TODO fix this, it throws a node is locked error once in a while local view = nil if active_view then -- If there is already a view use that one view = active_view else -- Otherwhise lets make one view = JPDebugView() core.root_view:get_active_node():add_view(view) active_view = view end -- Check if we have a runner for runner_name,runner in pairs(core.jpdebug.runners) do if runner_name == target.type then -- Found a runner running_proc = runner:run(target, name, debug_info) running_runner = runner if running_proc == nil then core.error("[jpdebug] Could not run the target") view:push("stderr", "Could not run the target") return end view:clear() -- background pump (non-blocking I/O) core.add_thread(function() while true do core.redraw = true coroutine.yield(0.016) -- ~60fps if running_proc == nil then return end local out = runner:read_stdout(running_proc) if out == nil then -- stdout pipe closed: try drain stderr and break when both closed local err = runner:read_stderr(running_proc) if err ~= nil and err ~= "" then view:push("stderr", err) end break end if out ~= "" then view:push("stdout", out) end local err = runner:read_stderr(running_proc) if err ~= nil and err ~= "" then view:push("stderr", err) end end if running_proc then local code = runner:wait(running_proc, process.WAIT_INFINITE) view:push("stdout", ("\n[exit] code=%s\n"):format(tostring(code))) end running_proc = nil running_runner = nil end) return view end end -- No suitable runners found core.error("[jpdebug] No suitable runners found for target %s", name) view:push("stderr", "No suitable runners found for target "..name) end local function hot_require(name, into) -- blow away the cached module package.loaded[name] = nil local fresh = require(name) if into then -- wipe old table (keep the identity) for k in pairs(into) do into[k] = nil end -- copy new fields in for k, v in pairs(fresh) do into[k] = v end -- copy metatable too, if any local mt = getmetatable(fresh) if mt then setmetatable(into, mt) end return into else return fresh end end local function load_runners() local runner_shell = hot_require("plugins.jpdebug.runners.shell", core.jpdebug.runners.shell) local runner_luadebug = hot_require("plugins.jpdebug.runners.luadebug", core.jpdebug.runners.luadebug) core.jpdebug.runners[runner_shell.name] = runner_shell core.jpdebug.runners[runner_luadebug.name] = runner_luadebug core.log("[jpdebug] Runners loaded") end -- And call it while loading the plugin load_runners() -- ---------- Add toolbar to treeview if plugins are installed ------ if required_toolbar_plugins and ToolbarView then ---@class Toolbar: core.view local Toolbar = ToolbarView:extend() function Toolbar:new() Toolbar.super.new(self) self.toolbar_font = renderer.font.load(get_plugin_directory() .. PATHSEP .. "toolbar_commands.ttf", style.icon_big_font:get_size()) self.toolbar_commands = {} end function Toolbar:_rebuild() local t = { {symbol = "A", command = "jpdebug:set-target"}, } if running_proc == nil then table.insert(t, {symbol = "B", command = "jpdebug:run"}) else table.insert(t, {symbol = "D", command = "jpdebug:stop"}) end table.insert(t, {symbol = "E", command = "jpdebug:reload-runners"}) self.toolbar_commands = t end function Toolbar:update() ---@diagnostic disable-next-line: undefined-field Toolbar.super.update(self) self:_rebuild() end local toolbar_view = Toolbar() ---@diagnostic disable-next-line: unused-local local toolbar_node = TreeView.node.b:split("up", toolbar_view, {y = true}) end ---@diagnostic disable-next-line: param-type-mismatch command.add(nil, { -- The run command ["jpdebug:run"] = function() local targets = get_targets() local target = get_selected_target() if target then if targets[target] then core.log('[jpdebug] Starting target %s', target) run_target(targets[target], target) else core.error("[jpdebug] Selected target not existing in targets list") return end else core.error("[jpdebug] No target selected in project module") return end end, ["jpdebug:stop"] = function() core.log(dump(running_proc)) if running_runner then running_runner:kill(running_proc) end core.log("[jpdebug] Stopped runner") if active_view then active_view:push("stdout", " ... Stopped") end running_proc = nil running_runner = nil end, -- The set target command ["jpdebug:set-target"] = function() core.command_view:enter("Select target", { show_suggestions = true, submit = function(selection) if get_targets()[selection] then selected_target = selection else core.error("[jpdebug] '%s' not a target", selection) end end, suggest = function(_) local l = {} for name, _ in pairs(get_targets()) do table.insert(l, name) end return l end }) end, ["jpdebug:reload-runners"] = function() load_runners() end, })