Writing a FileType Plugin
A FileType plugin teaches the explorer to recognise and preview a new kind of file. It is the smallest, most common extension you can write — a package, one abstract-type subtype, and two methods. No Rust changes, no registration call: using YourPackage and the dispatch tables grow.
This page is the step-by-step build. For the conceptual contract see The Dispatch ABI; for a complete external package see the HDF5 worked example; for turning the plugin on in a project see Discovery & Configuration.
What a FileType plugin does
Two things, both dispatched on your subtype:
| Method | Returns | Role |
|---|---|---|
matches(::Type{T}, path) | Bool | claim a path (usually by extension) |
preview(::Type{T}, path) | PreviewPayload | render it for the preview pane |
parse_entities(::Type{T}, path) is optional — it yields the ConceptEntity values a file declares, for the concept layer. Nothing calls it today (NOT BUILT), and the phase-1 plugins return an empty vector; see below.
At runtime the kernel finds every loaded subtype with file_types (a subtypes(FileType) scan) and picks the first one whose matches returns true with file_type_for. Your job is to supply the subtype and its methods.
Step 1 — Create the package
A plugin is an ordinary Julia package that depends on ConceptExplorerCore. Generate one and add the dependency:
using Pkg
Pkg.generate("MyPreview")
Pkg.activate("MyPreview")
Pkg.develop(path="/path/to/ship-of-tools/core")ConceptExplorerCore is unregistered, so use Pkg.develop(path=...) to point at the local Ship of Tools core/ checkout. Your Project.toml should carry ConceptExplorerCore under [deps]. The built-in Markdown plugin needs nothing else; only depend on what your preview actually uses (the HDF5 plugin adds HDF5, the Julia-source plugin adds JuliaSyntax and JSON3):
name = "MyPreview"
uuid = "..." # filled in by Pkg.generate
version = "0.1.0"
[deps]
ConceptExplorerCore = "5a28ea34-b669-4214-86b0-825ecd8fbc7c"
[compat]
julia = "1.12"Step 2 — Declare the FileType subtype
In src/MyPreview.jl, using ConceptExplorerCore and define a singleton subtype. The type carries no fields — it is purely a dispatch tag — so an empty struct ... end is the whole declaration:
module MyPreview
using ConceptExplorerCore
export MyKind
struct MyKind <: ConceptExplorerCore.FileType end
end # moduleThat is enough for file_types() to see MyKind once the package is loaded.
Step 3 — Claim paths with matches
Extend ConceptExplorerCore.matches for your subtype. Return true for paths your plugin owns. Extension matching on a lowercased path is the standard idiom — every built-in plugin does exactly this:
const MY_EXTENSIONS = (".myext", ".my")
ConceptExplorerCore.matches(::Type{MyKind}, path::AbstractString) =
any(endswith(lowercase(path), ext) for ext in MY_EXTENSIONS)file_type_for walks file_types() in order and returns the first subtype whose matches is true. Keep your predicate narrow — claim only paths you can actually render — so you don't shadow another plugin.
Step 4 — Render with preview
Extend ConceptExplorerCore.preview to return a PreviewPayload. A payload is a MIME string plus the bytes the frontend should render; the two-argument constructor PreviewPayload(mime, data) covers the common case (a third extras::Dict argument is available for renderer hints):
function ConceptExplorerCore.preview(::Type{MyKind}, path::AbstractString)
bytes = read(path)
ConceptExplorerCore.PreviewPayload("text/markdown", bytes)
endThe MIME you choose decides which frontend renderer draws the preview — this is the only contract the frontend cares about, and it is why a new FileType needs no Rust change — unless its output must be bounded, in which case the daemon's preview gates (is_bounded_output_plugin in the backend) also name its extensions.
Choosing a MIME
Pick a MIME the frontend already renders, or emit text/markdown and let its markdown renderer do the layout. MIMEs in use by the built-in plugins:
| MIME | Frontend rendering | Used by |
|---|---|---|
text/markdown | comrak + cosmic-text (headings, code fences, inline math) | Markdown plugin; the HDF5 example wraps its metadata tree in fenced markdown |
text/plain | plain UTF-8 text | error fallbacks |
application/json | plain UTF-8 text today (pretty-print / syntax colour planned) | JSON plugin |
image/png | native image quad | the PNG example in the ABI page |
application/vnd.sot.tokens+json | pre-tokenised span renderer ({spans:[{text,kind},…]}) | Julia-source plugin |
If none fits, the pragmatic choice is to format your content as markdown (text/markdown) — that is what the HDF5 plugin does, emitting a header plus a fenced text tree. Returning text/plain is the right move for an error path (open failed, parse failed): surface the failure as visible text rather than faking a successful preview.
Failing visibly
Do not let a bad file produce a fake-success preview. The HDF5 plugin catches an open or read failure and returns a text/plain payload describing the error, so the gap is on screen instead of a silent blank. Match that: render the error, don't swallow it.
Optional: parse_entities
If your file kind declares conceptual units the concept layer should track (functions, types, sections, datasets), implement parse_entities. Every phase-1 built-in returns an empty vector — the method exists so the concept layer can grow later without an ABI change:
ConceptExplorerCore.parse_entities(::Type{MyKind}, path::AbstractString) =
ConceptExplorerCore.ConceptEntity[]Nothing calls parse_entities today: the concept layer does not read it, so whatever a plugin returns is unused (NOT BUILT). A later revision could yield one ConceptEntity per logical unit (the Markdown plugin notes heading-hierarchy entities as the obvious next step), each carrying an ast_hash for staleness provenance; ast_hash has no method or caller today either.
Step 5 — Enable it
Loaded subtypes are discovered automatically, but which packages a project loads is explicit. Add your package to the project's Project.toml:
[sot]
extensions = ["MyPreview"]The kernel Base.requires each entry at startup, in order, before serving requests — so after install + this edit + a kernel restart, opening a matching file routes through your preview. See Discovery & Configuration for the full discovery rule.
Full skeleton
module MyPreview
using ConceptExplorerCore
export MyKind
struct MyKind <: ConceptExplorerCore.FileType end
const MY_EXTENSIONS = (".myext", ".my")
ConceptExplorerCore.matches(::Type{MyKind}, path::AbstractString) =
any(endswith(lowercase(path), ext) for ext in MY_EXTENSIONS)
function ConceptExplorerCore.preview(::Type{MyKind}, path::AbstractString)
bytes = read(path)
ConceptExplorerCore.PreviewPayload("text/markdown", bytes)
end
ConceptExplorerCore.parse_entities(::Type{MyKind}, path::AbstractString) =
ConceptExplorerCore.ConceptEntity[]
end # moduleNext steps
Worked Example: HDF5 — a real third-party plugin, end to end.
Discovery & Configuration — enabling plugins, project-root rules.
The Dispatch ABI — the conceptual contract behind every method here.
API — ConceptExplorerCore — symbol-by-symbol reference.