Remote file editing while debugging

After ZeroBrane Studio for Vera was released, I received several questions whether it is possible to load files from the device and edit them. The product already allows to upload files to Vera devices and to download log files, but it doesn't support editing of remote files and I thought it would be useful and interesting to implement this.

What you see below is the plugin that does just that. It adds a new menu item (File | Open Remotely...) that asks for a file name and then loads that file in a new editor tab. You can edit and save that file as you'd do with any other file. The main difference is that it only works when a debugging session is in progress as it uses the debugging mechanism to retrieve and save the files.

To install this plugin, you need to save it as remoteedit.lua into packages/ directory inside ZeroBrane Studio or inside <HOME>/.zbstudio directory. If you are on OSX, use the later location (<HOME>/.zbstudio/packages/remoteedit.lua) to avoid overwriting the plugin when you upgrade ZeroBrane Studio.

local G = ...
local id = G.ID("remoteedit.openremotefile")
local lastfile, menuid, debugger = ""
local editors = {}
local function reportErr(err) return(err:gsub('.-:%d+:%s*','')) end

local mobdebug = require("mobdebug")
local copas = require("copas")

return {
  name = "Remote edit plugin",
  description = "Allows to edit files remotely while debugging is in progress.",
  author = "Paul Kulchenko",
  version = 0.11,

  onRegister = function(self)
    local menu = ide:GetMenuBar():GetMenu(ide:GetMenuBar():FindMenu(TR("&File")))
    menuid = menu:Insert(2, id, "Open Remotely...")
    debugger = ide.debugger

    ide:GetMainFrame():Connect(id, wx.wxEVT_COMMAND_MENU_SELECTED,
      function()
        local file = wx.wxGetTextFromUser("Enter name (with path) of the remote file",
          "Open remote file", lastfile)
        if file and #file > 0 then
          self:loadFile(file)
          lastfile = file
        end
      end)
    ide:GetMainFrame():Connect(id, wx.wxEVT_UPDATE_UI,
      function (event) event:Enable(debugger.server and not debugger.running) end)

    -- this is a workaround for an API call missing in v0.39
    if tonumber(ide.VERSION) and tonumber(ide.VERSION) <= 0.39 then
      local sf = SaveFile
      SaveFile = function(...)
        if PackageEventHandle("onEditorPreSave", ...) ~= false then sf(...) end
      end
    end
  end,

  onUnRegister = function(self)
    local menu = ide:GetMenuBar():GetMenu(ide:GetMenuBar():FindMenu(TR("&File")))
    ide:GetMainFrame():Disconnect(id, wx.wxID_ANY, wx.wxEVT_COMMAND_MENU_SELECTED)
    if menuid then menu:Destroy(menuid) end
  end,

  onEditorClose = function(self, editor)
    editors[editor] = nil
  end,

  onEditorPreSave = function(self, editor, filepath)
    local remote = editors[editor]
    if remote and ide:GetDocument(editor):IsModified() then
      self:saveFile(remote, editor)
      return false
    end
  end,

  loadFile = function(self, remote)
    if not debugger then return end
    if not debugger.server or debugger.running then return end
    local code = ([[(function() local f, err = io.open(%s); if not f then error(err) end; local c = f:read('*a'); f:close(); return c end)()]])
      :format(mobdebug.line(remote))
    copas.addthread(function()
      local res, _, err = debugger.evaluate(code)
      if err then
        DisplayOutputLn(("Failed to load file '%s': %s.")
          :format(remote, reportErr(err)))
        return
      end

      local ok, content = LoadSafe("return "..res)
      if ok then
        DisplayOutputLn(("Loaded file '%s'."):format(remote))
        self.onIdleOnce = function()
          local editor = NewFile("remote: "..remote)
          editor:SetText(content)
          editor:SetSavePoint()
          editors[editor] = remote
        end
      else
        DisplayOutputLn(("Failed to load file '%s': %s.")
          :format(remote, content))
      end
    end)
  end,

  saveFile = function(self, remote, editor)
    if not debugger then return end
    if not debugger.server or debugger.running then return end
    local content = editor:GetText()
    local code = ([[local f, err = io.open(%s, 'w'); if not f then error(err) end; f:write(%s); f:close()]])
      :format(mobdebug.line(remote), mobdebug.line(content))
    copas.addthread(function()
      local err = select(3, debugger.execute(code))
      if not err then
        editor:SetSavePoint()
        DisplayOutputLn(("Saved file '%s'."):format(remote))
      else
        DisplayOutputLn(("Failed to save file '%s': %s.")
          :format(remote, reportErr(err)))
      end
    end)
  end,
}

[Updated 03/08/2014] Fixed an issue with local saving of modified files.

You should get a copy of my slick ZeroBrane Studio IDE and follow me on twitter here.

Leave a comment

what will you say?
(required)
(required)

About

I am Paul Kulchenko.
I live in Kirkland, WA with my wife and three kids.
I do consulting as a software developer.
I study robotics and artificial intelligence.
I write books and open-source software.
I teach introductory computer science.
I develop a slick Lua IDE and debugger.

Recommended

Close