01 · Requirements
What you need
A text editor and JavaScript. Plugins are evaluated by JavaScriptCore, the engine already in macOS, so there is nothing to install and nothing to compile. Each plugin runs in its own context with its own globals, so two plugins cannot see or overwrite each other’s state.
- A folder named
Something.linelarkplugin. - A
manifest.jsoninside it, and the script that manifest names. - Standard ECMAScript. There is no DOM, no
fetch, norequire, and no file system access — a plugin reaches the editor and nothing else.
The sandboxed Mac App Store edition reads the same plugins folder inside its own container. Install Plugin… is Studio-only, so the App Store edition only ever runs scripts you placed there yourself.
02 · Quickstart
Your first plugin
- Choose Plugins ▸ Reveal Plugins Folder in Finder.
- Create a folder there called
Uppercase.linelarkplugin. - Add a
manifest.jsonwith at least anidand aname. - Add
main.jswith the command below. - Choose Plugins ▸ Reload Plugins. Your command appears under Plugins ▸ Uppercase.
linelark.addCommand("upper", "Uppercase Selection", function () {
linelark.replaceSelection(linelark.selection().toUpperCase());
});The script runs once at load. Its job is to register what the plugin offers; the functions you hand to addCommand run later, each time the user picks that command.
03 · Metadata
The manifest
Only id and name are required. Everything else has a default or is simply omitted.
{
"id": "com.example.sortlines",
"name": "Sort Lines",
"version": "1.0.0",
"apiVersion": 1,
"main": "main.js",
"author": "Your Name",
"description": "Sorts the selected lines, and drops duplicates on request.",
"license": "MIT",
"homepage": "https://example.com/sortlines",
"pricing": "free"
}idRequiredReverse-DNS identity everything keys off. Two installed plugins claiming one id is reported as an error rather than one quietly winning.
nameRequiredShown in the Plugins menu and in Manage Plugins.
version"0"Displayed beside the name. Any string.
apiVersion1The API generation you wrote against. A plugin asking for a newer host is refused at load with a message saying so.
main"main.js"The script to evaluate, relative to the plugin folder.
author—Credited in Manage Plugins.
description—One line describing what the plugin does.
license—An SPDX identifier by convention. Shown as written.
homepage—Where the plugin lives. http and https only.
pricing"free"One of free, paid, or trial. An unknown word reads as free rather than failing the manifest.
purchaseUrl—Where to buy or licence it. http and https only.
04 · Reference
The linelark API
Everything a plugin can reach hangs off one global object. Offsets are UTF-16 code units throughout — what the editor’s storage and JavaScript’s own strings both count in — so linelark.text().substr(loc, len) and linelark.getRange(loc, len) always agree, astral-plane characters included.
addCommand(id, title, fn)Registers a menu command. Call it at load time.
apiVersion()The host API generation, currently 1.
log(message)Appends to this plugin’s log, shown in Manage Plugins. console.log is the same thing.
fileName()The front tab’s display name, such as main.swift or new 1.
filePath()Its full path, or null for an untitled tab.
language()The current language id, such as swift.
isReadOnly()Whether the document refuses edits.
text()The whole buffer.
length()Its length in UTF-16 code units.
lineCount()Number of lines. A trailing newline closes the last line.
line(n)One line, 1-based, without its line break.
getRange(loc, len)A substring.
selection()The selected text.
selectionRange()An object with location and length.
setSelection(loc, len)Selects, scrolls it into view, and focuses the editor.
replaceSelection(s)Replaces the selection.
replaceRange(loc, len, s)Replaces a range.
setText(s)Replaces the whole buffer.
insert(s)Inserts at the caret.
It is named after the command that made it and reaches every pane showing that buffer, exactly as a typed edit does. You never have to batch changes yourself for undo to behave.
05 · Craft
Best practices
- Make one edit, not many.
- Compute the whole replacement and apply it with a single
replaceRange. A loop of small edits produces a stack of undo steps the user has to unwind one at a time, and every edit invalidates offsets computed before it. - Read offsets immediately before you use them.
- Any write moves everything after it. If you need to edit in several places, work from the end of the document backwards, or recompute after each change.
- Handle an empty selection.
- Commands are always enabled while a document is open. Decide deliberately whether yours acts on the selection, the current line, or the whole buffer, and say so with
linelark.logwhen there is nothing to do. - Let errors be errors.
- A range outside the document, a write to a read-only file, or a call with no document open throws a real JavaScript error you can
try/catch. An error you do not catch is reported to the user with your plugin’s name and recorded in your log — it never takes the editor down. - Namespace nothing, but assume nothing either.
- Your globals are yours alone, so short names are safe. For the same reason you cannot rely on another plugin having loaded.
- Keep commands fast.
- They run on the main thread while the user waits. Work proportional to the document is fine; unbounded loops are not.
- Version the API you need.
- Set
apiVersionto the generation you tested against. A plugin asking for a newer host is refused with an explanation instead of failing halfway through its first command.
06 · Distribution
Free and paid plugins
Plugins may be free or commercial. Authors sell their own work, on their own terms, from their own site.
Linelark is not a store. It takes no payment, publishes no catalogue, and enforces no licence. A plugin is not bought through the editor; the manifest simply carries the sign that says what it costs and where to get it. Declare pricing as free, paid, or trial, and add a purchaseUrl. Manage Plugins shows the label and links out to you.
A plugin is plain JavaScript in a folder the user owns and can read. Any check inside one can be removed by anyone who cares to. Price on trust and convenience, as independent tools have done for years, and do not spend effort on protection the format cannot provide.
Two consequences worth knowing before you charge for one. Only http and https links are honoured, because a manifest is untrusted text that ends up behind something the user clicks. And a pricing word this version of Linelark does not recognise reads as free rather than failing the manifest, so a plugin never stops loading because a newer word appeared.
07 · Boundaries
What plugins cannot do
Stated plainly so you do not design around something that is not there.
- Only the front document. There is no API for the tab list, the folder tree, the find state, or the terminal.
- No interface of their own. A plugin contributes menu commands. It cannot add a panel, a sheet, a toolbar item, or a keyboard shortcut.
- No network, no disk, no processes. The context has no
fetch, no file access, and no way to run a command. - Commands are fixed at load. A plugin registers everything it offers when it loads, then decides what to do inside the command — it cannot vary its menu by language or file.
- No watchdog. Commands run on the main thread and cannot be interrupted, so a plugin that loops forever hangs the editor. A plugin that throws is caught and reported; only one that never returns is a problem.
- No native code. Plugins cannot link libraries, which is why capabilities needing one belong in the editor rather than in a plugin.
Notepad++ plugins are compiled libraries that drive Scintilla through its message interface. Linelark has no Scintilla and loads no binaries, so those plugins cannot run here in any form. See the migration guide.
08 · Complete
Worked example
Sort Lines registers two commands and shows the shape of a real plugin: expand the selection to whole lines, compute the result, apply it as one edit. It ships with the editor’s source and is loaded and run by its test suite, so it cannot drift out of step with the API.
// Whatever lines the selection touches, whole — selecting half of a line
// should still sort that line rather than cutting it in two.
function selectedLineRange() {
var range = linelark.selectionRange();
var text = linelark.text();
var start = range.location;
while (start > 0 && text.charAt(start - 1) !== "\n") {
start--;
}
var end = range.location + range.length;
while (end < text.length && text.charAt(end) !== "\n") {
end++;
}
return { location: start, length: end - start };
}
function sortSelectedLines(unique) {
var range = selectedLineRange();
if (range.length === 0) {
linelark.log("Select the lines to sort first.");
return;
}
var lines = linelark.getRange(range.location, range.length).split("\n");
lines.sort(function (a, b) {
return a.localeCompare(b);
});
if (unique) {
lines = lines.filter(function (line, index) {
return index === 0 || line !== lines[index - 1];
});
}
// One replacement rather than one per line: it lands as a single undoable
// edit, named after the command in the Undo menu.
linelark.replaceRange(range.location, range.length, lines.join("\n"));
}
linelark.addCommand("sort", "Sort Selected Lines", function () {
sortSelectedLines(false);
});
linelark.addCommand("sortUnique", "Sort Selected Lines and Remove Duplicates", function () {
sortSelectedLines(true);
});Pair it with the manifest above, drop the folder in the plugins directory, and choose Plugins ▸ Reload Plugins.
09 · Resolve
Troubleshooting
- My plugin is not in the menu.
- Open Plugins ▸ Manage Plugins…. A plugin that failed to load is still listed, with the reason next to it. A plugin that loaded but registered nothing says so too.
- It says the manifest could not be read.
- Check the JSON is valid and that
idandnameare both present. A trailing comma is the usual culprit. - Another plugin already uses that id.
- Two installed plugins claiming one
idis an error rather than one silently winning. Change yours and reload. - My edit was refused.
- The document is read-only, or the range you gave falls outside it. Offsets are stale after any write — recompute them.
- How do I see what my plugin printed?
- Select the plugin in Manage Plugins. Its log holds everything from
linelark.logandconsole.log, plus any error it let escape, capped at the most recent 200 lines.
