Skip to content

Automation (Macros & Lua)

Macros

Record a sequence of edits and movements and replay it.

  • Record (Ctrl+Shift+U), Play (Ctrl+U), and play N times.
  • Save named macros, play them from a list, and bind them to F-keys.
  • Macros are saved in a documented text format you can edit.

Lua scripting

Run Lua scripts against the current document (Script → Run Lua File, or Run Last Script). Scripts run in a sandbox (no file, OS or network access) and every edit a script makes is a single undoable step.

Scripts work in text, large-file and hex tabs. Sample scripts ship with the app: sort_lines, remove_duplicate_lines, trim_trailing_whitespace, count_words, list_todos, number_lines, uppercase_selection, fullwidth_to_ascii, csv_to_markdown_table and more.

The zume API

Scripts talk to the document through a global zume table. Byte offsets are 0-based; a range is [begin, end).

Function Description
zume.text() The whole document as a string
zume.size() Document size in bytes
zume.caret() The caret's byte offset
zume.set_caret(pos) Move the caret
zume.selection() Two values: begin, end of the selection
zume.select(begin, end) Set the selection
zume.selected_text() The selected text
zume.insert(pos, text) Insert text at an offset
zume.replace(begin, end, text) Replace a byte range
zume.line_count() Number of lines
zume.line(i) Line i (0-based), without its line ending
zume.read(offset, len) A bounded read (for large-file tabs)
zume.log(msg) Write a line to the script log
zume.api_version The API version (2)

Example scripts

Uppercase the selection (or the whole document when nothing is selected):

local b, e = zume.selection()
if e <= b then b, e = 0, zume.size() end
local text = zume.text():sub(b + 1, e)
zume.replace(b, e, text:upper())
zume.log(("uppercased %d bytes"):format(e - b))

Sort the selected lines (whole document when nothing is selected), as one undo step:

local size = zume.size()
local b, e = zume.selection()
if e <= b then b, e = 0, size end
local text = zume.text()
while b > 0 and text:sub(b, b) ~= "\n" do b = b - 1 end   -- widen to line starts
while e < size and text:sub(e, e) ~= "\n" do e = e + 1 end
local block = text:sub(b + 1, e)
local trailing = block:sub(-1) == "\n"
if trailing then block = block:sub(1, -2) end
local lines = {}
for line in (block .. "\n"):gmatch("(.-)\r?\n") do lines[#lines + 1] = line end
table.sort(lines)
local out = table.concat(lines, "\n")
if trailing then out = out .. "\n" end
zume.replace(b, e, out)
zume.log(("sorted %d lines"):format(#lines))

Remove duplicate lines, keeping the first occurrence and the original order:

local text = zume.text()
local seen, out, removed = {}, {}, 0
local trailing = text:sub(-1) == "\n"
if trailing then text = text:sub(1, -2) end
for line in (text .. "\n"):gmatch("(.-)\r?\n") do
  if seen[line] then removed = removed + 1
  else seen[line] = true; out[#out + 1] = line end
end
local result = table.concat(out, "\n")
if trailing then result = result .. "\n" end
zume.replace(0, zume.size(), result)
zume.log(("removed %d duplicate line(s)"):format(removed))

Number every line (" 12: text"), using line_count / line:

local out, count = {}, zume.line_count()
for i = 0, count - 1 do
  out[#out + 1] = ("%4d: %s"):format(i + 1, zume.line(i))
end
zume.replace(0, zume.size(), table.concat(out, "\n"))
zume.log(("numbered %d lines"):format(count))

Tip

An AI assistant can write these scripts for you. See MCP → Let the AI write a Lua script.

Project hooks

A project can run a script on open and before save (on_open / on_save in the project settings), and keep project-local macros and scripts.