fedit

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.

  1. 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.

  2. 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 reload rebuilds anything stale and reloads.

  3. Run your plugin's command.

    Type wc and 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.

examples/wordcount/Plugin.fs
 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.

  1. 01 SCAN Plugins.discover

    List every <name>/ under ~/.config/fedit/plugins/ and parse its plugin.json.

  2. 02 BUILD Plugins.build

    If the DLL is missing or older than any .fs, run dotnet build -c Release.

  3. 03 LOAD AssemblyLoadContext

    Each plugin gets its own ALC so it can't poison the host's type identity.

  4. 04 REGISTER register(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.

Notify Severity * string

Report a result; no buffer change.

[ Notify(Info, $"{n} words") ]
InsertText string

Add text at the cursor — timestamps, snippets, UUIDs.

[ InsertText "[\(stamp)\] " ]
ReplaceSelection string

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 } ]
OpenFile string

Open a file relative to the workspace root.

[ OpenFile "src/Main.fs" ]
SaveActiveBuffer

Trigger the same save path as :write.

[ SaveActiveBuffer ]
RunCommand string

Chain into a built-in command by name.

[ RunCommand "open foo.fs" ]
SetClipboard string

Copy text to the system clipboard.

[ SetClipboard buffer.Text ]
SelectRange CursorPosition * CursorPosition

Select between two positions — the anchor pins one end, the caret lands on the cursor, like a shift+motion selection.

[ SelectRange(anchor, cursor) ]
OpenFilePreview string

Open a file into the preview slot — the sidebar's Space behavior. Already-open files are activated instead.

[ OpenFilePreview "docs/plan.md" ]
RevealPath string

Expand and select a path in the sidebar without stealing focus. Paths outside the workspace are a no-op.

[ RevealPath "src/Main.fs" ]
ReplaceRange CursorPosition * 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 ]
SwitchBuffer int

Activate a buffer by its BufferView.Id. Unknown ids raise an error notification.

[ SwitchBuffer buffer.Id ]
NewBuffer string * string

Create a scratch buffer holding text and make it active. Later actions in the list target it.

[ NewBuffer("todos", report) ]
SetBufferActivation string

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" ]
OpenFileAt string * { 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) ]
MoveLinesUp int

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 ]
MoveLinesDown int

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.

:wc wordcount

Count words in the active buffer.

uses Notify · source →

:journal journal

Insert a [YYYY-MM-DD HH:MM] stamp at the cursor; the sidebar follows the stamped file.

uses InsertText + RevealPath + Notify · source →

:todocount todo-count

Walk the workspace, count lines containing TODO:.

uses Notify · source →

:todolist todo-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 →

:todonext todo-next

Jump cursor to the next TODO:; wraps, then continues into other open buffers.

uses MoveCursor + SwitchBuffer + RegisterKeybinding · source →

:jot jot

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