plugins
Plugins register commands and keybindings in F#. They live in ~/.config/fedit/plugins/, build lazily on first launch, and run out-of-process in a plugin host against a read-only
snapshot of the workspace. A small required plugin.json identifies the entry point;
plugin authors write a registration function while fedit owns the JSON-RPC boundary.
Trust model. Plugins are full .NET code with no sandbox. They can read any file, open any socket, and run any process. Treat installation like installing a shell tool — only run plugins from sources you trust.
quick start
From zero to running plugin in three commands and three keystrokes.
- Drop a plugin into the plugins directory.
$ mkdir -p ~/.config/fedit/plugins $ cp -R examples/wordcount ~/.config/fedit/plugins/ $ ./fedit .
On launch the host scans this directory, compiles anything stale via
dotnet build -c Release, and caches the resulting DLL alongside the source. - Confirm it loaded.
The plugin already built and loaded on launch. To check: Ctrl+P → type
plugin list. The dock shows each plugin's status and surfaces any build failures with the exact compiler output. Edited a plugin since launch?plugin reloadrebuilds anything stale and reloads. - Run your plugin's command.
Type
wcand hit Enter. The active buffer's word count appears in the notification dock. That's it.
anatomy of a plugin
The whole authoring surface fits in one short file. Click a gutter marker — the note for the highlighted line shows in the panel at the bottom of the window.
namespace Wordcountopen Fedit.PluginApimodule Plugin = let register (host: IPluginHost) = host.RegisterCommand { Name = "wc" Usage = "wc" Summary = "Count words..." Run = fun ctx -> let n = ctx.ActiveBuffer.Text.Split(...).Length [ Notify(Info, $"{n} words") ] }
lifecycle
What happens between ./fedit . and your register function getting called.
Runs automatically on startup, and on demand via :plugin reload.
-
01 SCANPlugins.discoverList every <name>/ under ~/.config/fedit/plugins/ and parse its plugin.json.
-
02 BUILDPlugins.buildIf the DLL is missing or older than any .fs, run dotnet build -c Release.
-
03 LOADAssemblyLoadContextEach plugin gets its own ALC so it can't poison the host's type identity.
-
04 REGISTERregister(collector)Call the plugin's entry point with a collector implementing IPluginHost.
actions
A plugin's Run function returns a list of these. The host applies them in order —
pick the right one for the effect you want.
The current contract assembly is Fedit.PluginApi 1.3.0. Manifests still use
"apiVersion": "1": these actions extend the append-only v1 union without
breaking plugins compiled against an older v1 assembly. For line moves, a selection ending
at column 1 excludes that final line because only the preceding newline is selected.
-
NotifySeverity * string -
Report a result; no buffer change.
[ Notify(Info, $"{n} words") ] -
InsertTextstring -
Add text at the cursor — timestamps, snippets, UUIDs.
[ InsertText "[\(stamp)\] " ]
-
ReplaceSelectionstring -
Replace selected text (inserts if no selection).
[ ReplaceSelection "kebab-cased" ]
-
MoveCursor{ Line; Column } -
Jump the cursor to a 1-based position.
[ MoveCursor { Line = 42; Column = 7 } ] -
OpenFilestring -
Open a file relative to the workspace root.
[ OpenFile "src/Main.fs" ]
-
SaveActiveBuffer— -
Trigger the same save path as :write.
[ SaveActiveBuffer ]
-
RunCommandstring -
Chain into a built-in command by name.
[ RunCommand "open foo.fs" ]
-
SetClipboardstring -
Copy text to the system clipboard.
[ SetClipboard buffer.Text ]
-
SelectRangeCursorPosition * CursorPosition -
Select between two positions — the anchor pins one end, the caret lands on the cursor, like a shift+motion selection.
[ SelectRange(anchor, cursor) ]
-
OpenFilePreviewstring -
Open a file into the preview slot — the sidebar's Space behavior. Already-open files are activated instead.
[ OpenFilePreview "docs/plan.md" ]
-
RevealPathstring -
Expand and select a path in the sidebar without stealing focus. Paths outside the workspace are a no-op.
[ RevealPath "src/Main.fs" ]
-
ReplaceRangeCursorPosition * CursorPosition * string -
Replace the span between two 1-based positions as one undo entry. Ends swap if reversed; coordinates clamp.
[ ReplaceRange(from, to_, "text") ]
-
ClearSelection— -
Collapse the selection to a caret. No-op without a selection.
[ ClearSelection ]
-
DeleteSelection— -
Delete the selected text as one undo entry. No-op without a selection.
[ DeleteSelection ]
-
SwitchBufferint -
Activate a buffer by its BufferView.Id. Unknown ids raise an error notification.
[ SwitchBuffer buffer.Id ]
-
NewBufferstring * string -
Create a scratch buffer holding text and make it active. Later actions in the list target it.
[ NewBuffer("todos", report) ] -
SetBufferActivationstring -
Run a registered command when a line of the active buffer is activated (Enter or left-click). Place it after the NewBuffer it targets.
[ SetBufferActivation "todo-jump" ]
-
OpenFileAtstring * { Line; Column } * bool -
Open a file and move the cursor to a 1-based position once it loads; the target survives the async open and applies if the file is already open. preview picks the preview slot.
[ OpenFileAt(path, { Line = 42; Column = 7 }, false) ] -
MoveLinesUpint -
Move the current line or every line containing selected text up by count. Clamps at the top, creates one undo entry, and ignores non-positive counts.
[ MoveLinesUp 3 ]
-
MoveLinesDownint -
Move the current line or every line containing selected text down by count. Clamps at the bottom, creates one undo entry, and ignores non-positive counts.
[ MoveLinesDown 2 ]
six reference plugins
Each demonstrates a different combination of actions. Source under examples/ — copy
any folder to ~/.config/fedit/plugins/ and it'll build on the next launch.
-
:wcwordcount -
Count words in the active buffer.
uses Notify · source →
-
:journaljournal -
Insert a [YYYY-MM-DD HH:MM] stamp at the cursor; the sidebar follows the stamped file.
uses InsertText + RevealPath + Notify · source →
-
:todocounttodo-count -
Walk the workspace, count lines containing TODO:.
uses Notify · source →
-
:todolisttodo-list -
List every TODO: as path:line in a clickable todos buffer; Enter or click jumps to the source (cap 50).
uses NewBuffer + SetBufferActivation + OpenFileAt + Notify · source →
-
:todonexttodo-next -
Jump cursor to the next TODO:; wraps, then continues into other open buffers.
uses MoveCursor + SwitchBuffer + RegisterKeybinding · source →
-
:jotjot -
Session scratchpad: jot code locations, check them off, jump back.
uses NewBuffer + SwitchBuffer + ReplaceRange + RevealPath + OpenFilePreview · source →
the :plugin command
One built-in command with verb dispatch. Tab completion suggests verbs first, then arguments.
-
:plugin list -
Show plugins with status (ok / disabled / FAIL).
-
:plugin enable <name> -
Re-enable a disabled plugin; persists to config.
-
:plugin disable <name> -
Disable a plugin without removing it; persists to config.
-
:plugin install <url-or-path> -
Folder, git URL, or .zip — auto-detected.
-
:plugin remove <name> -
Delete the plugin folder and rescan.
-
:plugin reload -
Rescan disk; rebuilds anything stale.
-
:plugin validate <path> -
Dry-run: parse the manifest, report what would register.
further reading
- docs/plugins.md — the author guide: manifest reference, every type, conflict policy, debugging, distribution.
- examples/ — six plugins from 16 to ~100 lines. Copy any one as your starting skeleton; all MIT-licensed.
- src/Fedit.PluginApi/ — the contract itself. Two short files; read it in five minutes.