TextEdit Plugin API

TextEdit is the tabbed text editor introduced with AmigaOS 3.2, built on ReAction and texteditor.gadget. From version 47.37 it loads filetype plugins from disk ("Turn Filetypes into a file based plugin that is loaded on startup"). From 47.38 it also loads extension plugins ("Introduce a new kind of plugin: Extensions"). The NDK 3.2 R4 publishes the binary interface in three headers under Include_H/tools/TextEdit/:

Header Contents
plugin.h struct PluginHead, struct Plugin, the security magic, InitPlugins()/ReleasePlugins()
filetypes.h struct EditorSettings, struct Filetype (filetype plugins, ID FTYP)
extension.h struct TextEditInterface, struct Extension (extension plugins, ID EXTS)

This article describes those structures and the callbacks they declare, together with the texteditor.gadget hooks and listeners they rely on. The headers have no $VER line or autodoc, so the field comments and the TextEdit release notes are the only documentation.

How a plugin file is recognised

A plugin is a loadable file that contains a struct PluginHead. The host recognises the file from the head's magic and ID:

struct PluginHead {
    ULONG               ph_Security;    /* should be PLUGINHEAD_SECURITY */
    ULONG               ph_ID;          /* FILETYPEPLUGINHEAD_ID or EXTENSIONPLUGINHEAD_ID */
    BPTR                ph_SegList;     /* set to zero in plugins */
    struct PluginHead * ph_Next;        /* set to zero in plugins */
    UWORD               ph_Version;     /* set to PLUGINHEAD_VERSION */
    UWORD               ph_Reserved;    /* only for alignment */
    STRPTR              ph_VersString;  /* pointer to $VER: string */
    struct Plugin *     ph_FirstPlugin; /* pointer to first plugin */
};

#define PLUGINHEAD_SECURITY  0x70FF4E75 /* MOVEQ #-1,D0 and RTS */
#define PLUGINHEAD_VERSION   1
#define MASTER_VERSION       2          /* the version of this master */
Constant Value Meaning
PLUGINHEAD_SECURITY 3.2+ 0x70FF4E75 Opcodes MOVEQ #-1,D0 / RTS
PLUGINHEAD_VERSION 3.2+ 1 ph_Version
MASTER_VERSION 3.2+ 2 p_MasterVersion
FILETYPEPLUGINHEAD_ID 3.2+ 0x46545950 ('FTYP') ph_ID for filetype plugins
EXTENSIONPLUGINHEAD_ID 3.2+ 0x45585453 ('EXTS') ph_ID for extension plugins
FILETYPEPLUGIN_VERSION 3.2+ 2 p_Version of a filetype plugin
EXTENSIONPLUGIN_VERSION 3.2+ 1 p_Version of an extension plugin

The security longword is a pair of valid 68k instructions. 0x70FF is MOVEQ #-1,D0 and 0x4E75 is RTS. Put it at the very start of the code, as the header comment implies, and a user who runs the plugin from the Shell gets an immediate return with -1 instead of a crash. The header itself says: "There should be no need to modify the PluginHead structure! Only the ID field should be changed for each project, as this part must be individual."

ph_SegList and ph_Next must be zero in the file ("set to zero in plugins"). Their names suggest that the loader uses them to record the segment list and chain loaded heads.

struct Plugin

Each head points to a singly linked list of struct Plugin descriptors:

struct Plugin {
    struct Plugin * p_Next;        /* pointer to next plugin */
    UWORD p_Version;               /* FILETYPEPLUGIN_VERSION or EXTENSIONPLUGIN_VERSION */
    UWORD p_MasterVersion;         /* set to MASTER_VERSION */
    UWORD p_PluginVersion;         /* the version of this plugin - not checked by textedit */
    UWORD p_PluginRevision;        /* and the revision - not checked by textedit */
    UWORD p_Identifier;            /* an ID to replace internal plugins */
    UWORD p_Flags;                 /* some flags to define plugin type */

    /* Now the real data comes */
    void __ASM__ (*init)(__REG__(a0, APTR userData));
};
  • p_Version is the version of the plugin type interface: 2 for filetypes, 1 for extensions.
  • p_MasterVersion is set to MASTER_VERSION (2).
  • p_PluginVersion/p_PluginRevision are for your own use. "not checked by textedit".
  • p_Identifier lets a plugin "replace internal plugins". TextEdit's own highlighters cover ARexx, Shell scripts, C and 68k assembler (release notes 47.12–47.33).
  • init() is called with its argument in A0.

plugin.h also declares the loader functions that manage the list:

void InitPlugins(STRPTR plugindir, ULONG headerID,
                 struct Plugin *dummyPlugin, struct PluginHead *dummyPluginHead);
void ReleasePlugins(struct Plugin *dummyPlugin, struct PluginHead *dummyPluginHead);

From the parameter names, InitPlugins() loads the plugins found in plugindir whose head carries headerID, and links them behind the dummy list anchors. ReleasePlugins() releases them again. These are plain C prototypes on the loader side (no library base or LVO), not services offered to plugins.

Register conventions: __ASM__ and __REG__

All callbacks use register arguments through the compiler-neutral macros from <clib/compiler-specific.h>:

Compiler __ASM__ __REG__(r, p)
SAS/C __asm register __r p
Maxon / StormC (empty) register __r p
VBCC (empty) __reg("r") p
GCC (AMIGA) (empty) p __asm("r")
DICE (empty) __r p

Declare your callback implementations the same way. Otherwise the host passes the arguments in registers that your function never reads.

Filetype plugins (FTYP)

A filetype plugin describes one kind of document: a localized name, the default icon names it matches, editor settings, an optional syntax highlighter and optional pages in the settings window.

struct EditorSettings

struct EditorSettings {
    UWORD Version;
    BOOL  HasLineNumbers:1;
    BOOL  CutCopyLineWhenNoSel:1;
    BOOL  AutoIndent:1;
    UWORD TabKeyPolicy:2;
    ULONG WrapColumn;
    UBYTE WordWrapMode;
    UBYTE IndentationWidth;
    UBYTE TabWidth;
    UBYTE IndentUsing;
};

These map onto texteditor.gadget attributes such as GA_TEXTEDITOR_ShowLineNumbers, GA_TEXTEDITOR_TabKeyPolicy (GV_TEXTEDITOR_TabKey_IndentsLine / _IndentsAfter), GA_TEXTEDITOR_TabSize and GA_TEXTEDITOR_IndentWidth. TextEdit 47.41 "Expand[ed] indentation settings with several new values". 47.42 fixed a crash caused by default settings with tabwidth = 0, so give every field a sane default.

struct Filetype

Member Type Filled by Purpose
Node struct MinNode host List linkage
WorkingCopy struct EditorSettings * host Settings being edited in the GUI
menuNumber, menuitem ULONG, struct MenuItem * host Filetypes menu entry
Version UWORD plugin
name CONST_STRPTR plugin "localized GUI visible name"
typeName STRPTR plugin "name used for prefs files etc"
autoFileTypes STRPTR plugin "newline separated list of deficon names that will match"
editorSettings struct EditorSettings * plugin Active settings
installChangeListener callback host Install a struct ChangeListener on a document
executeRexxCommand callback host Run a TextEdit command on a document
pluginData ULONG plugin Private
terminate callback plugin Cleanup
highlighterHook struct Hook * plugin Syntax highlighting, passed to texteditor.gadget
settingsGadget, settingsTitle callbacks plugin Extra settings pages (index i)
loadSettings, saveSettings callbacks plugin Read/write your settings chunk with iffparse
setSettingsFromGUI, setGUIFromSettings callbacks plugin Sync GUI and settings
processGadgetUp callback plugin GADGETUP from your settings page
applySettings callback plugin Apply to open documents
disposeGadgets callback plugin Free the page gadgets
assignedToDocument, unassignedFromDocument callbacks plugin Document now uses or stops using this filetype

The "filled by" column is inferred from the comments in the header. The two host callbacks (installChangeListener, executeRexxCommand) follow the same signatures as in struct TextEditInterface below. TextEdit 47.42: "FileType plugins can now also listen to changes and execute ARexx commands against the document."

Callback prototypes from filetypes.h:

Object *settingsGadget(__REG__(a0, struct Filetype *fType), __REG__(d0, ULONG i));
STRPTR  settingsTitle (__REG__(a0, struct Filetype *fType), __REG__(d0, ULONG i));
BOOL    loadSettings  (__REG__(a0, struct IFFHandle *iff),
                       __REG__(a1, struct ContextNode *cn),
                       __REG__(a2, struct Filetype *fType));
BOOL    saveSettings  (__REG__(a0, struct IFFHandle *iff),
                       __REG__(a1, struct ContextNode *cn),
                       __REG__(a2, struct Filetype *fType));
void    setSettingsFromGUI(__REG__(a0, struct Filetype *fType));
void    setGUIFromSettings(__REG__(a0, struct Filetype *fType), __REG__(a1, struct Window *window));
void    processGadgetUp(__REG__(a0, struct Filetype *fType), __REG__(d0, UWORD gadgetID),
                        __REG__(a1, struct Window *window));
void    applySettings (__REG__(a0, struct Filetype *fType));
void    disposeGadgets(__REG__(a0, struct Filetype *fType));
void    assignedToDocument  (__REG__(a0, struct Filetype *fType), __REG__(a1, APTR document));
void    unassignedFromDocument(__REG__(a0, struct Filetype *fType), __REG__(a1, APTR document));
void    terminate(__REG__(a0, struct Filetype *fType));

Rules from the header and release notes:

  • Gadgets on your settings pages "should have a gadget ID higher than 1000 so as not to conflict with other gadgets in the preferences window".
  • Settings are stored in IFF. TextEdit 47.34 moved to "IFF based settings files", so loadSettings/saveSettings receive an open IFFHandle and the current ContextNode.
  • File type detection uses DefIcons via icon.library (47.25). autoFileTypes therefore lists default icon type names, not filename patterns.

The highlighter hook

highlighterHook is installed as GA_TEXTEDITOR_HighlighterHook ([ISG], struct Hook *). The texteditor.gadget autodoc: "The supplied hook will be called for every line in the document both after load and when the user edits the document. When calling the hook the object is a value that needs to be passed back in when calling HightLightSetFormat(), HighlightSetState()". The message is

struct HighlightMessage {
    ULONG  Version;
    STRPTR Text;
    ULONG  StatusOfPrevBlock;
};

The hook applies styles through the texteditor.gadget library call (V47, SFD/texteditor_lib.sfd, base TextFieldBase):

VOID HighlightSetFormat(APTR object, ULONG pos, ULONG end, UWORD style); /* a0,d0,d1,d2 */

style combines TBSTYLE_UNDERLINE 0x0001, TBSTYLE_BOLD 0x0002, TBSTYLE_ITALIC 0x0004, TBSTYLE_SETCOLOR 0x0008 and TBSTYLE_SPELLERROR 0x0040, with a pen in the TBSTYLE_COLORMASK (0xFF00) byte. As its name says, StatusOfPrevBlock passes in the state left by the previous block. The autodoc also names a HighlightSetState() call, which by its name records that state for the next block (for example, an open multi-line comment). Only HighlightSetFormat() appears in the SFD.

Extension plugins (EXTS)

An extension is not tied to a document type. It can add menus, gadgets around the editor, and handlers for keys, gadgets and signals.

struct Extension (filled by the plugin)

struct Extension
{
    ULONG extensionData;               /* free for the extension */
    struct TextEditInterface *tei;     /* services offered by TextEdit */
    ULONG sigMask;                     /* signals for which handleSignal is called */

    void (*setWindowPointer)(a0 struct Extension *ext, a1 struct Window *window);
    void (*onNewDocument)(a0 struct Extension *ext, a1 APTR document);
    void (*onCurrentDocumentChanged)(a0 struct Extension *ext, a1 APTR document);
    void (*onDisposeDocument)(a0 struct Extension *ext, a1 APTR document);
    BOOL (*handleGadgetUp)(a0 struct Extension *ext, d0 UWORD gadgetId);
    BOOL (*handleRawKey)(a0 struct Extension *ext, a1 struct IntuiMessage *msg);
    void (*handleSignal)(a0 struct Extension *ext, d0 ULONG signal);
    BOOL (*aboutToQuit)(a0 struct Extension *ext);
    void (*terminate)(a0 struct Extension *ext);
};

(Shown in shorthand. The header declares each member with __ASM__ and __REG__.)

Callback When
setWindowPointer TextEdit tells you its window
onNewDocument / onDisposeDocument A tab is created or closed
onCurrentDocumentChanged The user switches tabs
handleGadgetUp GADGETUP for one of your gadgets (47.42 "extension callback on gadget up")
handleRawKey IDCMP_RAWKEY. Returns BOOL
handleSignal One of the signals in sigMask arrived
aboutToQuit Added in 47.43. Returns BOOL
terminate Plugin is being unloaded

struct TextEditInterface (provided by TextEdit)

Member Arguments Purpose (header comments)
installChangeListener a0 APTR document, a1 struct ChangeListener * Listen to edits
installLeftBarHook a0 APTR document, a1 struct Hook * Draw in the left bar
executeRexxCommand a0 APTR document, a1 STRPTR rexxCmd "only commands in TextEdit, not full rexx". FreeVec() any returned string that is not TRUE or FALSE
insertMenu d0 ULONG insertPosition, a0 struct Menu *menuSnip Add menus or items
installTopGadget / installBottomGadget / installSideGadget a0 Object *gadget Add a ReAction gadget around the editor
setWindowFormatStrings a0 STRPTR titleFormat, a1 STRPTR titleModifiedFormat Window title formats
activateGadget a0 Object *gadget Activate one of your gadgets

executeRexxCommand gotcha: the result string is allocated by TextEdit. The header says "be sure to FreeVec any returned string not TRUE or FALSE". ARexx commands that appeared in 47.42–47.43 include Open and FILENAME.

texteditor.gadget hooks used by plugins

struct ChangeListener

From <gadgets/texteditor.h>, registered with GM_TEXTEDITOR_AddChangeListener or through the interface above:

struct ChangeListener
{
    void (*onCharsInserted)(struct ChangeListener *listener, ULONG startBlockNun,
                            ULONG startPosInBlock, ULONG endBlockNum, ULONG endPosInBlock);
    void (*onCharsDeleted)(struct ChangeListener *listener, ULONG startBlockNum,
                           ULONG startPosInBlock, ULONG endBlockNum, ULONG endPosInBlock);
    void (*onCharEntered)(struct ChangeListener *listener, ULONG character);
    ULONG Reserved1;
    ULONG Reserved2;   /* all ReservedX members of this struct must be NULL */
    ULONG Reserved3;
    ULONG Reserved4;
    ULONG Reserved5;
};

"You must keep the structure and members valid as long as it is registered. You may add your own data following the structure. It is safe to set function pointers to NULL." These callbacks are plain C function pointers, without __REG__. texteditor.gadget's own bookmark system is "one such (internal) user of the ChangeListener system".

Left bar hook

GA_TEXTEDITOR_LeftBarHook ([IS]) adds a left sidebar. The hook receives a struct LeftBarRenderMessage (Command = LEFTBAR_RENDERCOMMAND 0) for each line, with a RastPort whose font is set and whose position is at the top left of the area. It also receives a struct LeftBarMouseMessage (LEFTBAR_MOUSECOMMAND 1) on clicks (the autodoc calls it LeftBarInputMessage). The autodoc rules:

  • Don't draw line numbers. Use GA_TEXTEDITOR_ShowLineNumbers.
  • The area is 20 pixels × TxHeight. The RastPort "for now isn't clipped but will be in the future so keep your painting within".
  • Mouse messages are "called on the input.device context so be careful". Returning TRUE causes a repaint. Return NULL for everything else.

Skeleton of a filetype plugin

This is a structural sketch. It uses only the fields and constants from the headers.

#include <exec/types.h>
#include <clib/compiler-specific.h>
#include <tools/textedit/plugin.h>
#include <tools/textedit/filetypes.h>

static struct EditorSettings mySettings = {
    0,              /* Version */
    1, 0, 1,        /* HasLineNumbers, CutCopyLineWhenNoSel, AutoIndent */
    0,              /* TabKeyPolicy */
    0,              /* WrapColumn */
    0, 4, 8, 0      /* WordWrapMode, IndentationWidth, TabWidth, IndentUsing */
};

static struct Filetype myType;    /* filled in init() */

static void __ASM__ myInit(__REG__(a0, APTR userData))
{
    myType.Version        = FILETYPEPLUGIN_VERSION;
    myType.name           = "Makefile";
    myType.typeName       = "makefile";
    myType.autoFileTypes  = "makefile";
    myType.editorSettings = &mySettings;
    /* leave unused callbacks NULL */
}

static struct Plugin myPlugin = {
    NULL, FILETYPEPLUGIN_VERSION, MASTER_VERSION,
    1, 0,           /* p_PluginVersion, p_PluginRevision */
    0, 0,           /* p_Identifier, p_Flags */
    myInit
};

/* Must be the first thing in the first hunk */
struct PluginHead myHead = {
    PLUGINHEAD_SECURITY, FILETYPEPLUGINHEAD_ID,
    0, NULL, PLUGINHEAD_VERSION, 0,
    "$VER: makefile.plugin 1.0 (13.11.2026)",
    &myPlugin
};

The skeleton leaves open three things that the NDK headers do not define: how init() hands the struct Filetype to TextEdit (what userData points to), the p_Flags bit values, and the plugin directory TextEdit scans. Check them against TextEdit's own documentation. Make sure that myHead really ends up at the start of the first code hunk, for example by placing it in its own object file linked first.

See Also


Sources: NDK 3.2 R4 Include_H/tools/TextEdit/{plugin,filetypes,extension}.h, Include_H/gadgets/texteditor.h, Include_H/clib/compiler-specific.h, Autodocs/texteditor_gc.doc, SFD/texteditor_lib.sfd, ReleaseNotes/TextEdit-RelNotes, ReleaseNotes/texteditor-RelNotes Updated: 2026-11-13