Appearance
Scripting with Lua
A script is a few lines of Lua that act on the file you are editing: tidy a selection, rewrite a block, jump to the next match of a syntax query. You keep it under Settings › Scripts, give it a name, and run it however suits you — a hardware key, the command palette, a key on the accessory bar, or :Name on vim's command line. Vim does not have to be on.
Scripts run in a sandbox: they can read and change the open file and ask the app to do what your keys can, and nothing else — no files of their own, no network, no system.
Your first script
Settings › Scripts, tap +.
Name it
Shout. A name starts with a capital letter and has only letters and digits.Under Source, tap Edit. The editor opens with a starter that says how the script runs. Its toolbar's share and download buttons (Share, Import) are for a script kept as a file. Replace it with:
lualocal i, j = selection() if j < i then return notify('Select something first') end buf.replace(i, j, buf.text():sub(i, j):upper())Tap Done. Done is offered once the source compiles.
Tap Save on the script's screen.
Open a file, select a word, and run it: from the command palette (Run script: Shout), or with vim on, type :Shout. To give it a key, see Running a script.
The Scripts screen
Each row is a script: its key on the left, its name, and the first line of its source.
- Tap a row to open the script. Tap its key to set its mnemonic. Long-press to delete it.
- On a script's screen:
- Name is fixed once the script exists — it is how the script is run, and its shortcut and mnemonic are kept by it. To rename, make a new script and delete the old one.
- Source is shown with line numbers. Edit opens it in the editor, which checks the Lua as you type and underlines a line that will not compile; Done is offered once it compiles and brings the source back here.
- Mnemonic is the two characters on the script's key. Left empty, the key shows the first letters of the name:
OIforOrganizeImports,JoforJoin. Another script or snippet already showing the same characters is pointed out; you can keep them anyway. - Save keeps everything at once. Until you tap it, nothing changes: a key, a shortcut or
:Nameruns the script as it was last saved. - Delete removes the script — see Deleting a script.
Running a script
| From | How |
|---|---|
| A hardware key | Settings › Shortcuts › Scripts: each script is a shortcut, Run script: Name. Give it a key there as you would any shortcut. |
| The command palette | Search for Run script: Name. |
| The accessory bar | Each script is a key in the key library and the key picker, under Scripts. Put it on one of your keyboards — see Keyboards. |
| Vim's command line | :Name, with anything after the name handed to the script (below). :action script.Name runs it too. |
| Your vimrc or a command of your own | command J Join makes :J run the script Join; a mapping can run :Join<CR>. |
- A script runs on the file you are editing. Outside the editor — on Home, in Settings — its key does nothing.
- Arguments come only from the command line: what follows the name is the script's
..., so:Grep TODOhands the script'TODO'(local word = ...). From a key, the palette or the accessory bar,...is empty. - A key another shortcut already uses is pointed out when you set it; if you keep it, the other shortcut wins. Give the script a key of its own.
- One script at a time. Running a script while one is still running — a held key repeats — is refused: Script Join is already running.
With vim on, and with vim off
Scripts run either way. What differs is where they speak:
- With vim on, a script's printed lines and its error show on vim's message line, as
:lua's do. - With vim off, they show on the editor's status line — an error for longer — and
mode()answersoff. The bar shows the last line of what was printed; when there is more, a speech-bubble button beside it opens the whole output, and the button stays after the line clears.ex()andregistersare vim's, so they are refused; everything else works.
Stopping a script
Esc or Ctrl+C stops a running script with "Interrupted" — also one that is waiting on the app, such as a slow format. With vim on, the same Esc that leaves insert mode stops it.
The Lua you can use
Lua 5.3, with its string, table, math and utf8 libraries and the basics (pairs, ipairs, pcall, tostring, select, …). Nothing reaches outside the editor: no io, no os, no require; load takes Lua text only.
print(...)collects lines and shows them when the script ends — the last 20, with a count of any before them.notify(text)shows a line at once.- Strings are bytes, as in any Lua:
#'中文'is 6.utf8.len,utf8.offsetandutf8.codescount and step by characters. - An error stops the script and names it and the line:
Join:3: attempt to index a nil value. - Each run starts fresh: nothing one run sets is there for the next.
The editor, from Lua
Positions are Lua's: 1-based byte positions, ranges inclusive, as string.sub counts them — so what string.find gives you goes straight back in:
lua
local i, j = buf.text():find('TODO')
if i then buf.replace(i, j, 'DONE') end| Call | What it does |
|---|---|
buf.text() | The whole file, as a string |
buf.lines(), buf.line(n) | Every line as a table, or line n (1-based) |
buf.replace(i, j, text) | Replace bytes i to j; buf.replace(i, i - 1, text) inserts at i |
buf.format(i, j) | Format bytes i to j (Python, JavaScript, TypeScript, JSON, CSS, GraphQL) |
cursor(), cursor(pos) | The cursor's position, or move it there |
selection(), selection(i, j) | The selection as i, j — an empty one is i, i - 1 — or select bytes i to j |
action(id, arg) | Run an app action by its id — the ids are listed in Vim mode |
set(name, value) | Set an editor option: set('ts', 2), set('nu', false) |
syntax.outline(), syntax.outline(path) | The file's outline, as the structure panel shows it, or another project file's |
syntax.nodeAt(pos) | The syntax node at a position and the nodes around it |
syntax.query(pattern), syntax.query(pattern, path) | Every match of a tree-sitter query, in this file or another project file |
notify(text) | Show a line now |
mode() | Vim's mode: normal, insert, visual or replace, and line or block for a visual mode; off with vim off |
ex(line) | Run a vim command line, | and all (vim on only) |
registers.a, registers.a = 'text' | Read or set a vim register, as :let @a does; + sets the clipboard (vim on only) |
A position inside a character is refused, never moved to the nearest one:
buf.replace(3, 3, 'x')inhéllostops with "3 is inside a character" (éis bytes 2 and 3).Reading and editing stop the script when refused — a read-only file, a position outside the text, a file still opening — with the reason and the line.
ex,actionandsetanswer instead, with a table to check:ok,message, and for an actionapplied(false when there was nothing to do) anddata(what a query found):luaif not action('editor.save').ok then return endAn action that asks you —
action('project.quickOpen')opens the file picker — waits for your pick.syntax.nodeAt(pos):enclosing(...)finds the nearest node aroundposwhose kind matches any of the Lua patterns given. Grammars name things differently, so give the kinds your language uses: in Go a function isfunction_declaration,method_declarationorfunc_literal; in TypeScriptfunction_declaration,method_definition,arrow_functionorfunction_expression.lualocal f = syntax.nodeAt(cursor()):enclosing('function', 'method', 'func_literal') if f then selection(f.from, f.to) endsyntax.query(pattern)runs a tree-sitter query and answers itsmatches, each with itspattern(1-based) andcaptures: the capture'sname, the node'skind,fromandtoin bytes, itslineand itstext(cut at 200 characters,truncatedthen set). A query stops at 1,000 matches and on a file over 256 KB, and says so rather than answering part of the list.lualocal q = syntax.query('(function_declaration name: (identifier) @name)') for _, m in ipairs(q.matches) do print(m.captures[1].text) endbuf.format(i, j)formats part of the file with its formatter's own range form, and answers whether anything changed and which lines:local changed, lines = buf.format(i, j). The formatter may widen the range to whole lines or a whole statement. A range ending at the start of a line leaves that line out, as a selection does. Go has no range formatting: format the whole file withaction('editor.formatDocument').A
pathasks about another file in the project without opening it —syntax.outline('src/util.ts'). The file you are editing is answered from the editor; another open file with unsaved changes is refused (save it first); a path outside the project, a binary file and a file over 1 MB are refused.sourcein a query's answer says where the text came from:editorordisk.action,ex,setand thesyntaxcalls cannot be used inside agsubreplacement function, atable.sortcomparison or a metamethod — they wait for the app, and Lua cannot wait there.
Undo
A script's edits undo as one step, its ex and buf.format edits included — unless you typed while it waited, which keeps them separate. Formatting or a lint fix the script asks for through action (action('editor.formatDocument')) is the app's own edit, made while the script waits for it, so it is a step of its own.
Limits
Limits keep a mistake from freezing the editor. Each stops the script with its reason and line:
- Too long: a script that computes too long stops with "instruction budget spent". A long one does not freeze the editor meanwhile: it pauses every few milliseconds to let the editor draw.
- Too big: building a string over 16 MB stops with "result too large", and more than 256 MB of strings in all with "memory budget spent".
- Too slow a pattern: one that backtracks too long stops with "pattern too complex".
- Too much asked of the app: more than 100 actions (
setcounts as one, and so do:fmtand:lintthroughex) or 2,000 requests of any kind in one run stops it, naming the count and the last action. Every action a script runs is recorded in the key event log under its name.
Recipes
Surround the selection — :Surround [ or a key (a parenthesis by default):
lua
local open = ... or '('
local close = ({ ['('] = ')', ['['] = ']', ['{'] = '}', ['<'] = '>' })[open] or open
local i, j = selection()
if j < i then return notify('Select something first') end
buf.replace(i, j, open .. buf.text():sub(i, j) .. close)Trim trailing whitespace in the whole file, as one undo step:
lua
local text = buf.text()
local trimmed = text:gsub('[ \t]+\n', '\n'):gsub('[ \t]+$', '')
if trimmed ~= text then buf.replace(1, #text, trimmed) endCount the TODOs:
lua
local n = 0
for _ in buf.text():gmatch('TODO') do n = n + 1 end
notify(n .. ' TODO' .. (n == 1 and '' or 's'))Format, then save — stopping if the format was refused:
lua
if not action('editor.formatDocument').ok then return end
action('editor.save')Select the function around the cursor (TypeScript and Go kinds):
lua
local f = syntax.nodeAt(cursor()):enclosing('function_declaration', 'method', 'arrow_function', 'func_literal')
if not f then return notify('Not in a function') end
selection(f.from, f.to)One-off Lua: :lua
With vim on, :lua {chunk} runs the Lua you give it without keeping it: :lua print(6 * 7). Everything on this page holds for it — the same calls, limits and single undo step — and its errors name it :lua. The whole rest of the line is Lua, | included (it is Lua's bitwise or).
Names and commands
- A script's name is its command, and its own:
:command Join …is refused while a script holds the name, and so is a vimrccommand Join …line. A:commandor a vimrc line may run one:command J Join. - There is no rename. A new script under the new name starts with no shortcut and no mnemonic, and a vimrc line naming the old one answers No script named Join.
Deleting a script
- A script on a keyboard cannot be deleted. You are told which keyboard and slot hold its key — "Script in use" — and remove it there first.
- Otherwise you are asked, and told what goes with it: its shortcut, so a later script of the same name does not inherit your key, and any vimrc line that names it, which will then fail.
