Literate ontology engineering in Visual Studio Code. Write OWL ontologies as plain-text Org-mode files — with label display, IntelliSense, syntax checking, folding, and one-click OWL export.
ELOT (Emacs Literate Ontology Tool) is an established workflow for ontology engineering in which a single Org-mode document is simultaneously your ontology source, its documentation, and your analytical workspace. This extension brings the core ELOT experience to VS Code — no Emacs required.
| Feature | Shortcut |
|---|---|
| Org→OWL export | Right-click → Tangle to Manchester Syntax |
| Import OWL ontology | Command Palette → Elot: Import OWL Ontology |
| Label display (hover + toggle) | F5 (.org and other files) |
| Headline folding | Tab / Shift+Tab |
| Go to definition | F12 / Ctrl+Click |
| IntelliSense completion | Ctrl+Space |
| Org Indent Mode | Ctrl+Shift+I |
| Export to HTML | Right-click → Export to HTML (requires Pandoc) |
| OWL axiom syntax checking | Automatic (red squiggly underlines) |
| Bold headlines & fontified description lists | Always on |
- Install the extension — search for "Elot" in the Extensions panel, or install from the Visual Studio Marketplace.
- Open an
.orgontology file — try the example files from the repository. - Hover over a CURIE to see its label and annotations.
- Press F5 to toggle visual label display.
- Press Tab on a heading to fold/unfold.
- Press F12 on a CURIE to jump to its definition.
- Press Ctrl+Space to search and insert entities.
Have an existing ontology in RDF/XML, Turtle, or another OWL format? The extension can convert it to ELOT's Org-mode format directly from VS Code — no command line needed.
- Open the Command Palette (
Ctrl+Shift+P) and run Elot: Import OWL Ontology. - Choose whether to enter a URL or browse for a local file.
- The extension runs
elot-exporter.jar(downloaded automatically on first use) and converts the ontology to Org-mode. - After conversion you are prompted for a file location to save the result
with an
.orgextension. The default filename is derived from the top-level heading — for example, if the ontology's top-level heading is* core, the suggested filename iscore.org. - The saved
.orgfile opens immediately with all ELOT features active (label display, IntelliSense, syntax checking, etc.).
Why the save prompt? VS Code features like hover, completion, and diagnostics are tied to the file extension. An unsaved buffer doesn't have an
.orgextension, so ELOT functions won't activate until the file is saved. If you cancel the save dialog the converted text still opens as an untitled buffer — just save it as.orglater to enable ELOT.
The OWL importer runs elot-exporter.jar, which is compiled for Java 21
and requires Java 21 or newer on your system. Many enterprise laptops ship
with older Java versions (8, 11, or 17), so a check before first use is
recommended:
java -versionIf the major version is below 21, running the importer will fail with
UnsupportedClassVersionError: ... has been compiled by a more recent version
of the Java Runtime ... class file version 65.0
Install a current JDK from one of:
- Eclipse Temurin (free, open source)
- Microsoft Build of OpenJDK
- Oracle JDK
On managed machines where the system Java cannot be replaced, install a
newer JDK alongside it and point the extension at it via the
elot.javaPath setting (absolute path to the java executable in the
new JDK's bin/ directory). This avoids touching JAVA_HOME or PATH and
leaves other Java tools untouched.
The extension defaults elot.javaPath to java (first match on PATH).
You can also run the exporter from the command line:
java -jar elot-exporter.jar input-ontology.owl -o output.orgDownload it from the releases page.
Ontology files are full of identifiers like obo:BFO_0000001 that are hard to
read. ELOT can display human-readable labels instead.
Hover over any CURIE (e.g. obo:BFO_0000001) or angle-bracket URI in an
.org file to see a hover card with:
- Label (bold) — e.g. "entity"@en
URI(monospace)- Type — e.g.
owl:Class,owl:ObjectProperty - Extra annotations — e.g.
skos:definition,rdfs:comment
Always on by default. Disable via the setting elot.labelDisplay.hoverEnabled.
Press F5 (or click the 🏷 icon in the editor title bar, or the status bar indicator, or right-click → Elot: Toggle Label Display) to visually replace CURIEs with their labels throughout the editor.
When enabled:
- The underlying document text is not modified — CURIEs remain in the file
- The CURIE text is visually hidden and replaced by the label via CSS pseudo-elements
- Labels are shown in italic by default (configurable)
- Copy-paste still copies the real CURIEs
- The status bar shows 🏷 Labels (highlighted) when ON,
</>CURIEs when OFF
Toggle methods:
| Method | Location |
|---|---|
| F5 | Keyboard shortcut (when .org file is active) |
| 🏷 icon | Editor title bar (top-right) |
| Status bar | Bottom-right indicator (clickable) |
| Right-click | Context menu → Elot: Toggle Label Display |
| Command Palette | Elot: Toggle Label Display |
Org headings can be folded and unfolded, just like in Emacs Org-mode. Each heading's fold region extends to just before the next sibling or ancestor heading.
| Action | Method |
|---|---|
| Toggle fold at cursor | Press Tab on a heading line |
| Fold at cursor | Ctrl+Shift+[ (⌘+⌥+[ on Mac) |
| Unfold at cursor | Ctrl+Shift+] (⌘+⌥+] on Mac) |
| Fold all | Shift+Tab (when some headings are unfolded), or Ctrl+K Ctrl+0 |
| Unfold all | Shift+Tab (when all folded), or Ctrl+K Ctrl+J |
| Fold to level N | Ctrl+K Ctrl+N (e.g. Ctrl+K Ctrl+1 for level 1) |
| Click gutter | Click the ▸ / ▾ chevron in the left gutter next to a heading |
The Tab and Shift+Tab bindings are scoped to .org files, so they don't
interfere with normal Tab behaviour in other file types.
Note: VS Code's fold toggle is two-state (folded ↔ unfolded), unlike Emacs Org-mode's three-state TAB cycling (folded → children → subtree). Shift+Tab provides the global overview/show-all toggle.
When you see a CURIE like obo:BFO_0000001 anywhere in an Org file, you can
jump directly to the heading where that entity is declared — the VS Code
equivalent of Emacs's M-. (xref-find-definitions).
| Action | Method |
|---|---|
| Go to definition | F12, or Ctrl+Click (Cmd+Click on Mac) on a CURIE |
| Peek definition (inline preview) | Alt+F12 |
| Go back | Alt+← (like Emacs M-,) |
| Right-click | Context menu → Go to Definition / Peek Definition |
Works purely within the current Org file — no external index or language server required.
Toggle visual indentation that mimics Emacs's org-indent-mode. When enabled,
sub-headings and their body text are indented proportional to their depth, and
leading stars on sub-headings are hidden — all without modifying the file.
| Headline level | Visual indent |
|---|---|
* (level 1) |
0 (left margin) |
** (level 2) |
2 spaces |
*** (level 3) |
4 spaces |
| Body text | Same as its parent heading |
| Action | Method |
|---|---|
| Keyboard shortcut | Ctrl+Shift+I (in an .org file) |
| Command Palette | Elot: Toggle Org Indent Mode |
| Status bar | Click the Flat / Indent indicator (bottom-right) |
| Title bar | Click the indent icon in the editor title area |
| Right-click | Context menu → Elot: Toggle Org Indent Mode |
Press Ctrl+Space to get a dropdown of all OWL entities declared in the current Org file — searchable by label or CURIE.
| Field | Content |
|---|---|
| Label (main text) | Human-readable name, e.g. "entity"@en |
| Description (right-aligned) | CURIE, e.g. obo:BFO_0000001 |
| Detail (subtitle) | RDF type, e.g. owl:Class |
| Icon | Class for owl:Class, Property for properties, Value for individuals |
| Documentation panel | Full detail: label, CURIE, type, and annotations |
Selecting an item inserts the CURIE into the document. You can also
auto-trigger by typing : after a prefix name (e.g. obo:). Items are grouped
by type (Classes → Properties → Individuals), then sorted alphabetically by
label.
The extension validates OWL Manchester Syntax axioms as you type. Any
description list item whose tag is an OMN keyword (SubClassOf,
EquivalentTo, DisjointWith, Domain, Range, SubPropertyOf,
InverseOf, SubPropertyChain, Types, Facts, SameAs,
DifferentFrom, etc.) is parsed against the OWL Manchester Syntax grammar.
If the axiom value is malformed, a red squiggly underline appears with an error message. Diagnostics are updated on every save and when the file is opened.
| Keyword | Grammar rule |
|---|---|
SubClassOf, EquivalentTo, DisjointWith, Domain, Range, Types |
ClassExpressionList |
SubPropertyOf, InverseOf |
ObjectPropertyExpressionList |
SubPropertyChain |
SubPropertyChain |
Facts |
Fact |
SameAs, DifferentFrom |
IndividualIRIList |
Example:
- SubClassOf :: obo:BFO_0000139 only (obo:BFO_0000015 or obo:BFO_0000035)
Errors are caught immediately — no need to load the ontology into a reasoner first.
Two always-on decorations enhance readability:
- Bold headlines — All Org headings (
*,**,***, …) are rendered in bold. - Subdued description list tags — In description lists like
- rdfs:isDefinedBy :: lis-ont:core, the tag portion (- rdfs:isDefinedBy ::) is shown in a muted colour with italic styling, so the value stands out.
Applied automatically — no toggle or configuration needed.
| Setting | Default | Description |
|---|---|---|
elot.labelDisplay.fontStyle |
"italic" |
Font style for labels: italic, normal, or oblique |
elot.labelDisplay.hoverEnabled |
true |
Show hover cards for CURIEs in .org files |
elot.javaPath |
"java" |
Path to the Java executable (Java 21+ required for OWL import) |
DB-backed (global) label display has its own settings — see
Label Database below
(elot.dbPath, elot.activeLabelSources, elot.preferredLanguages,
elot.globalLabelDisplay.*).
Change in VS Code Settings (search for "elot").
Org files often have long lines. Press Alt+Z to toggle word wrap, or add
this to your settings.json:
"[org]": {
"editor.wordWrap": "on"
}ELOT can resolve identifiers (CURIEs and full URIs) to human-readable labels
across any file you open in VS Code — Turtle, SPARQL, CSV, source code, log
files — by consulting a persistent SQLite index. The index is populated and
managed exclusively by the elot-cli db sub-command; the VS Code extension
reads from it (read-only).
Status (2026-04): the CLI write/query path and the VS Code read path (hover + visual label replacement + status bar + source management commands) are shipping. End-to-end smoke tests against a real Extension Host are the next milestone (Step 2.3.5b of the plan). Emacs (
elot-mode) shares the same DB schema and binary file format — see README-global-label-display.org.
Once you have populated a database with elot-cli db register ... (see
below), point the extension at it via settings, mark which sources should
be consulted, and the extension provides:
- DB-backed hover for CURIEs and IRIs in non-Org files (Turtle,
SPARQL, JSON, source code, plain text, …). Shows the label, the
resolved id, the
rdf:type, definitions, comments, and a[src: NAME]provenance footer. - Visual label replacement (F5) for non-Org files. Same idea as the Org-side F5 toggle: the document text is unchanged; CURIEs and IRIs are rendered with their labels via CSS pseudo-elements.
- Status bar indicator showing active-source count and total decoratable ids. Click it to toggle global label display. Hover for a per-source breakdown.
- Command palette access to the source-management surface (Activate / Deactivate / Reorder).
The Org F5 (
elot.toggleLabels) and the new global F5 (elot.toggleGlobalLabels) coexist: F5 routes to the Org-side toggle in.orgfiles and to the DB-backed toggle elsewhere.
| Setting | Default | Description |
|---|---|---|
elot.dbPath |
"" |
Absolute path to the SQLite DB. Empty = use the per-platform VS Code globalStorage location. Workspace-level overrides user-level. |
elot.activeLabelSources |
[] |
Ordered list of active sources. Each entry is either a string (source name) or {"source": "name", "dataSource": "path"}. First match wins on lookup conflicts. |
elot.preferredLanguages |
[] |
Preferred language tags (BCP-47), in priority order. Empty = ["", "en"] (untagged first, then English). |
elot.globalLabelDisplay.hoverEnabled |
true |
Show DB-backed hovers in non-Org files. |
elot.globalLabelDisplay.maxIds |
500 |
Per-file cap on how many CURIE/IRI tokens are decorated. Status bar shows a "capped" warning when reached. |
elot.globalLabelDisplay.includeLanguages |
[] |
VS Code language IDs in which DB-backed hover / decoration apply. Empty = built-in default (plaintext, markdown, typescript, javascript, python, json, yaml, xml, turtle, sparql, …). Org is always excluded. |
settings.json accepts both the shorthand and the canonical form for
active sources:
Beware JSONC commas. VS Code's
settings.jsonparser is forgiving but a trailing comma after the last key/value can silently invalidate a setting. If a label-related setting "doesn't seem to take effect", open the file and look for a stray comma before the closing brace.
| Command | Effect |
|---|---|
Elot: Toggle Global Label Display (F5 in non-Org files) |
Show/hide DB-backed labels in the current editor. |
Elot: Activate Label Source |
Multi-select QuickPick of inactive sources; appends to elot.activeLabelSources. |
Elot: Deactivate Label Source |
Multi-select QuickPick of active sources; removes from elot.activeLabelSources. |
Elot: Reorder Active Label Sources |
Two-step QuickPick: pick a source, then move up / down / top / bottom / done. (Per-item button reorder lands in 2.3.6.) |
Elot: Label DB Info |
Diagnostic: shows the resolved DB path, bridge state, registered sources with id counts, and current settings. Useful as a first stop when something looks off. |
The settings round-trip preserves the original scope of the key (workspace
vs. user) and always writes the canonical {source, dataSource} shape so
the resulting settings.json is unambiguous after a UI edit.
Sits in the bottom-right, next to the Org-side label-display indicator. Format:
🏷 Labels (3 src, 12k ids) ← display ON
</> CURIEs (3 src, 12k ids) ← display OFF
🏷 Labels (3/5 src, 12k ids, capped) ← scan cap reached
$(database) ELOT: no DB ← bridge has no file open
When active source count differs from total registered sources, the
format collapses to (N/M src, K ids). Hover the indicator for a full
breakdown (path, per-source id counts, prefs).
By precedence (extension):
- Workspace-level
elot.dbPath - User-level
elot.dbPath $ELOT_DB_PATHenvironment variablecontext.globalStorageUri.fsPath+/elot.sqlite:- Windows:
%APPDATA%\Code\User\globalStorage\johanwk.elot\elot.sqlite - macOS:
~/Library/Application Support/Code/User/globalStorage/johanwk.elot/elot.sqlite - Linux:
~/.config/Code/User/globalStorage/johanwk.elot/elot.sqlite
- Windows:
The CLI uses the same logical defaults (its --db flag wins, then
$ELOT_DB_PATH, then the same per-platform globalStorage location,
then ~/.elot/elot.sqlite as a final fallback). When the publisher id
matches, the extension and the CLI converge on the same file.
The VS Code extension and Emacs each own their own DB by default — they do
not share state. Use --db or $ELOT_DB_PATH if you want a shared file.
# Create / open the DB (idempotent; creates parent dirs)
elot-cli db init [--db <path>]
# Register a source (replaces any existing rows for the source name)
elot-cli db register <file> --source <name> [--type csv|tsv|json|ttl|rq|org]
[--data-source <path-or-url>]
elot-cli db refresh <name> --file <file> [--type ...]
# Inspect
elot-cli db list [--prefixes] [--format tsv|json|table]
elot-cli db lookup <label> [--active] [--format tsv|json]
elot-cli db attr <id> [<prop>] [--format tsv|json]
# Remove a source (cascades to its entities, attributes, prefixes)
elot-cli db remove <name><id> accepts a literal stored id, a CURIE (expanded against the DB's
prefix table), or a full URI (contracted against the same table). All
read commands honour $ELOT_PREFERRED_LANGUAGES (BCP-47 tags,
comma-separated; the empty tag means "untagged").
| Type | Parser notes |
|---|---|
csv |
RFC-4180. First row is the header. Required column: id. Optional label and lang. Other columns become attributes. Header label@en is shorthand for a tagged label. |
tsv |
Same as CSV with tab delimiter. |
json |
Two shapes: flat [{id, label, lang?, ...attrs}, ...] and nested {prefixes?: {pfx: iri}, entities: [{id, label?, kind?, attrs?: {prop: value|[v..]|{lang: v}}}]}. |
ttl |
Runs ROBOT ($ELOT_ROBOT_JAR or robot on PATH) with a default SELECT ?id ?label (LANG(?label) AS ?lang) query. Override per-project: place <root>/.elot/ttl-label-query.rq (where <root> is the nearest ancestor with .git, .elot, or .elot-cache). Prefixes are harvested from the file's @prefix lines. |
rq |
Executes a SPARQL SELECT query against --data-source <ttl-file-or-http-endpoint> via ROBOT. Results are cached in <root>/.elot-cache/<query>.<sha1>.csv; an empty result preserves any existing cache (defensive). Prefixes are harvested from the query's PREFIX lines. |
org |
Reuses the orgize WASM parser + ELOT slurp builder. Supports the standard :prefixdefs: block, ID-suffix CURIE conventions, and language-tagged headings. |
Type is auto-detected from the file extension if --type is omitted.
# Initialise an empty DB
elot-cli db init --db /tmp/elot.sqlite
# Ingest a CSV with English and Korean labels
cat > /tmp/labels.csv <<'EOF'
id,label,lang
ex:widget,Widget,en
ex:widget,위젯,ko
ex:widget,Dings,de
EOF
elot-cli db register /tmp/labels.csv --db /tmp/elot.sqlite --source demo
# Lookup
elot-cli db lookup Widget --db /tmp/elot.sqlite # ex:widget
elot-cli db attr ex:widget rdfs:label --db /tmp/elot.sqlite # Widget (en wins)
# Korean prefs
ELOT_PREFERRED_LANGUAGES=ko \
elot-cli db attr ex:widget rdfs:label --db /tmp/elot.sqlite # 위젯
# List in JSON
elot-cli db list --db /tmp/elot.sqlite --format jsonSchema version 3 only. Older databases are refused with a clear message
directing the user to upgrade via Emacs. The canonical DDL is shipped as
elot-package/schema.sql and consumed verbatim by both the Elisp writer
and the TS writer; a byte-identical golden round-trip test
(test/fixtures/golden/) keeps the two implementations in lockstep.
ELOT introduces literate ontology engineering: a workflow where a single Org-mode plain-text file is the authoritative source for an OWL ontology and its documentation. Headlines are the taxonomy; description lists are the axioms and annotations.
This approach has been used across scores of ontology projects, including the
ISO 23726-3 Industrial Data Ontology. See the
ELOT repository for the full project,
including the Emacs package, examples, and the elot-exporter tool.
The extension includes a complete Org→OWL pipeline built on orgize (a Rust Org-mode parser compiled to WebAssembly). It supports:
- Heading-to-entity mapping (URIs, CURIEs, and labels from Org headings)
- Property drawer and keyword extraction
- Description list → OWL annotation/restriction conversion
- Prefix table management
- Ontology, Class, ObjectProperty, DataProperty, AnnotationProperty, and Individual frames
- Meta-annotations via nested description lists
- Automatic tangle to the file specified in
:header-args:omn: :tangle
The sections below are for contributors who want to build the extension from source.
cd tools/elot-cli
npm install# Full build (WASM + esbuild bundle):
npm run bundleThis runs two stages:
-
npm run build:wasm— runswasm-pack build --target nodejsinsideelot-orgize/, producingsrc/wasm/elot_orgize.jsandsrc/wasm/elot_orgize_bg.wasm. -
node esbuild.mjs— cleansdist/, copies the.wasmfile, then bundlessrc/extension.ts→dist/extension.jsandsrc/cli.ts→dist/cli.jsvia esbuild.
Generated files in src/wasm/ and dist/ are gitignored — run
npm run bundle after every fresh clone.
# Generate OMN, write to stdout
node dist/cli.js examples/bfo-core.org -
# Write to a specific file
node dist/cli.js examples/bfo-core.org output.omn
# Uses the :tangle target from the Org file, or stdout
node dist/cli.js examples/bfo-core.org
# Export to styled HTML (requires Pandoc on PATH)
node dist/cli.js --html examples/bfo-core.org
# Export to HTML with explicit output path
node dist/cli.js --html examples/bfo-core.org output.htmlThe --html flag exports to styled HTML via Pandoc instead of generating
Manchester Syntax. It applies the same CURIE linkification, label resolution,
and ELOT CSS/JS theming as the VS Code "Export to HTML" command. If no output
path is given, the .org extension is replaced with .html.
npx @vscode/vsce packageProduces elot-<version>.vsix containing dist/extension.js, dist/cli.js,
dist/elot_orgize_bg.wasm (~284 KB), package.json, LICENSE, and this
README.
npm test # Golden-file tests
npx tsx src/tests/entityFromHeader.test.ts # Unit tests
npx tsx src/tests/annotationValue.test.ts
npx tsx src/tests/omnKeywords.test.ts| What changed | What to run |
|---|---|
Rust code in elot-orgize/src/ |
make bundle (rebuilds WASM + re-bundles) |
TypeScript in src/ |
make bundle (re-bundles) |
| Only type-checking | make build |
Fresh .vsix |
make package (runs bundle via prepublish) |
| Forgot the commands | make help |
Org text → [orgize WASM] → JSON AST → parseOrg() → ElotNode tree → generateOmn() → OMN
parseOrgWasm.ts— Calls orgize WASM, maps JSON toElotNodetreecli.ts— CLI entry pointextension.ts— VS Code extension entry pointgenerateOmn.ts— Assembles OMN from node treeomnDeclarations.ts— Generates frames for each nodeomnFrame.ts— Formats individual OWL frames
The Rust crate elot-orgize/ contains lib.rs (WASM entry), parse.rs
(AST walker), and types.rs (data structures).
{ // Shorthand: "elot.activeLabelSources": ["bfo", "ro"], // Canonical (also written by the management commands): "elot.activeLabelSources": [ { "source": "bfo", "dataSource": "" }, { "source": "ro", "dataSource": "" } ], "elot.preferredLanguages": ["en", ""] }