Overview
Scimax VS Code is designed with extensibility in mind. The architecture separates core parsing logic (npm library extractable) from VS Code-specific adapters, enabling both internal extension and external plugin development.
This document describes the extension points available for adding new functionality.
Architecture Layers
┌────────────────────────────────────────────────────────────┐
│ VS Code Extension │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Plugin Adapter Layer ││
│ │ • src/adapters/linkFollowAdapter.ts ││
│ │ • VS Code-specific actions (commands, UI) ││
│ └─────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Core Library (npm extractable) ││
│ │ • src/parser/ - Parsing, export, link resolution ││
│ │ • No VS Code dependencies ││
│ └─────────────────────────────────────────────────────────┘│
└────────────────────────────────────────────────────────────┘
Extension Points
Link Types
Add new link type handlers to support custom link syntax like [[mytype:path]].
Core: LinkTypeHandler (Pure Functions)
Location: src/parser/orgLinkTypes.ts
import { linkTypeRegistry, LinkTypeHandler } from './parser/orgLinkTypes';
const myHandler: LinkTypeHandler = {
type: 'jira',
description: 'Link to Jira issues',
// Pure resolution - no side effects
resolve(path, context) {
return {
displayText: path,
url: `https://jira.example.com/browse/${path}`,
tooltip: `Open Jira issue ${path}`
};
},
// Export for different backends
export(path, description, backend, context) {
const url = `https://jira.example.com/browse/${path}`;
const text = description || path;
switch (backend) {
case 'html':
return `<a href="${url}">${text}</a>`;
case 'latex':
return `\\href{${url}}{${text}}`;
default:
return text;
}
},
// Optional: completion suggestions
async complete(prefix, context) {
return [
{ text: 'PROJ-123', label: 'PROJ-123', detail: 'Bug fix' }
];
}
};
linkTypeRegistry.register(myHandler);
Adapter: LinkFollowHandler (VS Code Actions)
Location: src/adapters/linkFollowAdapter.ts
import { linkFollowRegistry, LinkFollowHandler } from './adapters/linkFollowAdapter';
const myFollowHandler: LinkFollowHandler = {
type: 'jira',
async follow(path, context) {
await vscode.env.openExternal(
vscode.Uri.parse(`https://jira.example.com/browse/${path}`)
);
}
};
linkFollowRegistry.register(myFollowHandler);
Built-in Link Types
| Type | Description | Example |
|---|---|---|
| http | Web URLs | [[http://example.com]] |
| https | Secure web URLs | [[https://example.com]] |
| file | Local files | [[file:./notes.org]] |
| id | Org-id targets | [[id:abc123]] |
| doi | DOI links | [[doi:10.1234/abc]] |
| cite | BibTeX citations | [[cite:smith2020]] |
| mailto | Email addresses | [[mailto:user@example.com]] |
| cmd | VS Code commands | [[cmd:workbench.action.openSettings]] |
| nb | Project/notebook links | [[nb:myproject::file.org]] |
| ref | Named element references | [[ref:fig1]] |
Custom Exporters
Add custom export formats using Handlebars templates - no code required.
Directory Structure
Each exporter is a subdirectory containing manifest.json, placed in one of
the searched locations:
| Location | Notes |
|---|---|
{scimax.directory}/exporters/ | Default ~/scimax/exporters/ |
~/.scimax/exporters/ | Always searched |
$XDG_CONFIG_HOME/scimax/exporters/ | Only when XDG_CONFIG_HOME is set |
<workspace>/.scimax/exporters/ | Per-project exporters |
scimax.export.customExporterPaths | Any extra directories you list (~ is expanded) |
A plain exporters/ directory at the root of a project is not one of them --
it has to be .scimax/exporters/, or added to
scimax.export.customExporterPaths.
~/.scimax/exporters/
├── my-journal/
│ ├── manifest.json # Configuration
│ ├── template.tex # Handlebars template
│ └── partials/ # Optional partial templates
│ └── header.tex
Run Scimax: Reload Custom Exporters after adding or editing one; exporters are
loaded when the extension starts.
When an Exporter Does Not Appear
An exporter that fails to load is reported rather than silently skipped:
Scimax: Show Custom Exporter Problems lists each directory that could not be
read, with the reason, and opens its manifest.json. The export dispatcher also
shows a [c !] entry when something failed. The usual causes are:
Invalid JSON in manifest.json -- most often a trailing comma:
"documentClass": "elsarticle",followed by}. Comments and trailing commas are tolerated (with a warning), but anything else JSON rejects will keep the exporter from loading.Missing
templatefile -- thetemplatepath is relative to the exporter directory.A missing required field:
id,name,parent,outputFormat,template.The directory is not in a searched location (see the table above).
manifest.json
{
"id": "my-journal",
"name": "My Journal Format",
"description": "Custom journal article format",
"parent": "latex",
"outputFormat": "pdf",
"template": "template.tex",
"orgTemplate": "template.org",
"keywords": {
"JOURNAL": {
"required": true,
"description": "Target journal name"
},
"VOLUME": {
"default": "1"
}
},
"latexOptions": {
"documentClass": "article",
"packages": ["hyperref", "graphicx"]
}
}
The Org Header an Exporter Needs
An exporter that declares keywords needs those keywords in the document
(#+TO:, #+FROM:, #+SUBJECT: for a memo). You do not have to remember
them:
Scimax: Insert Custom Exporter Headerinserts the header for an exporter you pick, with defaults filled in and required keywords left as<<<PLACEHOLDER>>>.The same header appears in the template pickers (
Scimax: Insert Template/Insert Org Template) under the Custom Exporters category, named after the exporter.Exporting with a required keyword missing warns and offers to insert the header, instead of quietly writing
[NOT FOUND: to]into the output.
Ship a real skeleton with an exporter by adding an orgTemplate to the
manifest -- a .org file in the exporter directory:
{
"id": "cmu-memo",
"template": "template.tex",
"orgTemplate": "template.org"
}
That file may use the template conventions: {{author}}, {{date}},
${1:tab stops}, $0 for the final cursor position, and <<<PLACEHOLDER>>>
for text to replace by hand. Without orgTemplate, the header is generated from
the keywords in the manifest.
Running a Custom Exporter
| How | Notes |
|---|---|
C-c C-e, then [c 1], [c 2], ... | Loaded exporters appear in the export dispatcher |
C-c C-e, then [c c] | Opens the custom exporter picker |
Scimax: Export with Custom Exporter | The same picker from the palette |
#+LATEX_CLASS routing | See below - the normal LaTeX/PDF exports use the exporter |
Automatic Routing by LaTeX Class
You do not have to remember to run the custom exporter by hand. Map a
#+LATEX_CLASS value to an exporter id and the normal LaTeX/PDF exports
(C-c C-e l l, l p, l o) use that exporter automatically:
{
"scimax.export.latexClassExporters": {
"cmu-memo": "cmu-memo",
"cmu-ms-report": "cmu-ms-report"
}
}
With that setting, a document starting with
#+LATEX_CLASS: cmu-memo
#+TO: Selection Committee
#+FROM: John Kitchin
#+SUBJECT: Recommendation
exports through the cmu-memo exporter on C-c C-e l o, no custom command
needed. The class name is matched case-insensitively, and the mapped value may
be any exporter id (the class and the exporter do not have to share a name).
A class that names a loaded exporter routes to it even with no mapping. There
is no cmu-memo.cls for LaTeX to load -- the exporter's template is what
supplies \documentclass -- so an unmapped #+LATEX_CLASS: cmu-memo would
otherwise fail the compile with File `cmu-memo.cls' not found. Map the class
to none if you really do have a class file of that name and want the built-in
backend.
Per-document overrides use the #+EXPORTER keyword, which needs no
configuration at all and is what the inserted header uses:
| Keyword | Effect |
|---|---|
#+EXPORTER: cmu-memo | Use this exporter, whatever #+LATEX_CLASS says |
#+EXPORTER: none | Force the built-in LaTeX backend for this file |
Notes:
Routing applies to
l l(writes the rendered.tex),l p(compiles the PDF), andl o(compiles and opens the PDF). Body-only export is never routed, since a template needs the full document.The CLI follows the same rules:
scimax export file.org --format pdfroutes through the mapped exporter.--exporter <id>still wins over routing.If the mapped exporter is not installed, you get a warning and the built-in LaTeX export runs instead. Run Scimax: Reload Custom Exporters after adding a new exporter directory.
Template Helpers
| Helper | Description |
|---|---|
{{latex value}} | Escape for LaTeX |
{{html value}} | Escape for HTML |
{{default field "fallback"}} | Default value if empty |
{{required field "name"}} | Mark field as required |
{{join items ", "}} | Join array with separator |
{{#ifeq a b}}...{{/ifeq}} | Conditional equality |
{{today}} | Current date |
{{year}} | Current year |
{{upper text}} | Uppercase |
{{lower text}} | Lowercase |
{{{body}}} | Raw body content (triple braces) |
{{{latexHeaders}}} | Document #+LATEX_HEADER: lines |
Document #+LATEX_HEADER: Lines
A document's #+LATEX_HEADER: and #+LATEX_HEADER_EXTRA: lines are collected
into the latexHeaders variable, so a letter can add packages the template
does not know about:
#+LATEX_CLASS: cmu-memo
#+LATEX_HEADER: \usepackage{setspace}
#+LATEX_HEADER: \onehalfspacing
Place {{{latexHeaders}}} in the template wherever the headers belong. A
LaTeX template that does not mention latexHeaders gets them inserted
automatically just before \begin{document}, so older templates keep working.
Export Backends (Programmatic)
For complex export needs, implement the full ExportBackend interface.
Location: src/parser/orgExport.ts
interface ExportBackend {
name: string;
exportDocument(doc: OrgDocumentNode, options?: ExportOptions): string;
exportElement(element: OrgElement, state: ExportState): string;
exportObject(object: OrgObject, state: ExportState): string;
exportAffiliatedKeywords?(affiliated: AffiliatedKeywords, state: ExportState): string;
}
See implementations:
src/parser/orgExportHtml.ts- HTML backendsrc/parser/orgExportLatex.ts- LaTeX backendsrc/parser/orgExportIpynb.ts- Jupyter notebook backend
Babel Executors
Add new language execution engines for source blocks.
Location: src/adapters/babelExecutorAdapter.ts
LanguageExecutor Interface
interface LanguageExecutor {
/** Language names this executor handles (e.g., ['ruby', 'rb']) */
languages: string[];
/** Execute code and return result */
execute(code: string, context: ExecutionContext): Promise<ExecutionResult>;
/** Initialize a persistent session (optional) */
initSession?(sessionName: string, context: ExecutionContext): Promise<void>;
/** Close a session (optional) */
closeSession?(sessionName: string): Promise<void>;
/** Check if the runtime is available */
isAvailable(): Promise<boolean>;
}
interface ExecutionContext {
cwd?: string; // Working directory
env?: Record<string, string>; // Environment variables
session?: string; // Session name for persistence
variables?: Record<string, unknown>; // From :var arguments
timeout?: number; // Execution timeout
}
interface ExecutionResult {
success: boolean;
stdout: string;
stderr: string;
executionTime: number; // milliseconds
}
Registering a Custom Executor
import { registerBabelExecutor, LanguageExecutor } from './adapters/babelExecutorAdapter';
const rubyExecutor: LanguageExecutor = {
languages: ['ruby', 'rb'],
async execute(code, context) {
// Spawn ruby process, capture output
const startTime = Date.now();
// ... execution logic ...
return {
success: true,
stdout: output,
stderr: '',
executionTime: Date.now() - startTime
};
},
async isAvailable() {
// Check if ruby is installed
return true;
}
};
// Register returns a Disposable for cleanup
const disposable = registerBabelExecutor(rubyExecutor);
context.subscriptions.push(disposable);
Using the Helper Functions
For simple command-based executors, use the helper:
import { createSimpleExecutor, registerBabelExecutor } from './adapters/babelExecutorAdapter';
// Create an executor that writes code to a temp file and runs a command
const rubyExecutor = createSimpleExecutor({
languages: ['ruby', 'rb'],
command: 'ruby',
extension: '.rb',
args: [], // Optional: arguments before the file
timeout: 30000, // Optional: default timeout
transformCode: (code, ctx) => code, // Optional: transform code
});
context.subscriptions.push(registerBabelExecutor(rubyExecutor));
Built-in Executors
| Language | Aliases | Notes |
|---|---|---|
| python | python3, py | Supports sessions |
| javascript | js, node, nodejs | Via Node.js |
| typescript | ts | Via tsx, ts-node, or bun |
| shell | sh, bash | |
| julia | jl | |
| R | r, Rscript |
Jupyter kernels are also supported via jupyter-python, jupyter-julia, etc.
Block Export Handlers
Define how custom special blocks (#+BEGIN_X...#+END_X) render in exports.
Location: src/adapters/blockExportAdapter.ts
BlockExportHandler Interface
interface BlockExportHandler {
/** Block type name (case-insensitive, e.g., 'sidebar') */
blockType: string;
/** Optional description for documentation */
description?: string;
/**
* Export the block content for a specific backend
* @param content The processed content inside the block
* @param backend The export backend ('html', 'latex', 'text')
* @param context Additional context about the block
* @returns The exported string, or undefined to use default handling
*/
export(
content: string,
backend: 'html' | 'latex' | 'text',
context: BlockExportContext
): string | undefined;
}
interface BlockExportContext {
blockType: string;
parameters?: string; // From #+BEGIN_foo :param value
filePath?: string;
affiliated?: {
name?: string;
caption?: string;
[key: string]: string | undefined;
};
}
Registering a Custom Block Handler
import { registerBlockExport, BlockExportHandler } from './adapters/blockExportAdapter';
const calloutHandler: BlockExportHandler = {
blockType: 'callout',
description: 'Custom callout blocks',
export(content, backend, context) {
const title = context.parameters?.trim() || 'Note';
switch (backend) {
case 'html':
return `<div class="callout">\n<strong>${title}</strong>\n${content}</div>`;
case 'latex':
return `\\begin{tcolorbox}[title=${title}]\n${content}\\end{tcolorbox}`;
default:
return undefined; // Use default handling for 'text' backend
}
}
};
const disposable = registerBlockExport(calloutHandler);
context.subscriptions.push(disposable);
Built-in Block Types
| Type | Description | HTML Class |
|---|---|---|
| warning | Warning/alert admonition | org-warning |
| note | Informational note | org-note |
| tip | Tip/hint admonition | org-tip |
| important | Important notice | org-important |
| caution | Caution notice | org-caution |
| sidebar | Sidebar content | org-sidebar |
| details | Collapsible details (HTML only) | <details> |
Block Highlight Configuration
Define visual highlighting (decorations) for custom blocks in the editor.
Location: src/adapters/blockHighlightAdapter.ts
BlockHighlightConfig Interface
interface BlockHighlightConfig {
/** Block type to highlight (case-insensitive) */
blockType: string;
/** Background color for the block (CSS color or theme variable) */
backgroundColor?: string;
/** Color for the #+BEGIN/#+END lines */
headerColor?: string;
/** Border color (left border is common) */
borderColor?: string;
/** Whether content should be italic */
italic?: boolean;
}
Registering Block Highlights
import { registerBlockHighlight } from './adapters/blockHighlightAdapter';
const disposable = registerBlockHighlight({
blockType: 'callout',
backgroundColor: 'rgba(100, 149, 237, 0.1)',
borderColor: 'rgba(100, 149, 237, 0.5)',
headerColor: 'cornflowerblue',
});
context.subscriptions.push(disposable);
Built-in Highlight Configs
| Block Type | Background Color | Border Color |
|---|---|---|
| warning | rgba(255, 193, 7, 0.1) | rgba(255, 193, 7, 0.5) |
| note | rgba(33, 150, 243, 0.1) | rgba(33, 150, 243, 0.5) |
| tip | rgba(76, 175, 80, 0.1) | rgba(76, 175, 80, 0.5) |
| important | rgba(244, 67, 54, 0.1) | rgba(244, 67, 54, 0.5) |
| caution | rgba(255, 152, 0, 0.1) | rgba(255, 152, 0, 0.5) |
| sidebar | rgba(158, 158, 158, 0.1) | rgba(158, 158, 158, 0.5) |
| details | rgba(103, 58, 183, 0.1) | rgba(103, 58, 183, 0.5) |
Export Hooks
Lightweight callbacks that run at specific points during export, allowing customization without implementing a full ExportBackend.
Location: src/adapters/exportHooksAdapter.ts
ExportHook Interface
interface ExportHook {
/** Unique identifier for this hook */
id: string;
/** Optional description */
description?: string;
/** Priority for ordering (higher runs first, default 0) */
priority?: number;
/**
* Pre-export hook - runs before export starts
* Can modify export options
*/
preExport?: (context: PreExportContext) => ExportOptions | undefined;
/**
* Post-export hook - runs after export completes
* Can transform the final output
*/
postExport?: (output: string, context: PostExportContext) => string | undefined;
/**
* Element filter - runs for each element during export
* Can override the rendered output for specific elements
*/
elementFilter?: (rendered: string, context: ElementFilterContext) => string | undefined;
}
interface PreExportContext {
document: OrgDocumentNode;
options: ExportOptions;
backend: string; // 'html', 'latex', etc.
filePath?: string;
}
interface PostExportContext {
backend: string;
options: ExportOptions;
filePath?: string;
}
interface ElementFilterContext {
element: OrgElement;
backend: string;
options: ExportOptions;
parent?: OrgElement;
}
Registering Export Hooks
import { registerExportHook } from './adapters/exportHooksAdapter';
// Add a company header/footer to HTML exports
const disposable = registerExportHook({
id: 'company-branding',
description: 'Add company header and footer',
postExport: (output, context) => {
if (context.backend === 'html') {
return `<!-- Company Header -->\n${output}\n<!-- Company Footer -->`;
}
return undefined; // Don't modify other backends
}
});
context.subscriptions.push(disposable);
Use Cases
| Hook Type | Use Case |
|---|---|
| preExport | Inject metadata, set default options, add custom macros |
| postExport | Add headers/footers, wrap in template, minify, post-process |
| elementFilter | Custom handling for specific elements, transform rendering |
Helper Functions
createWrapperHook
Creates a simple hook that wraps output with before/after content.
import { createWrapperHook, registerExportHook } from './adapters/exportHooksAdapter';
// Add copyright notice to HTML/LaTeX exports
const hook = createWrapperHook('copyright', {
backend: ['html', 'latex'], // Only these backends
before: '',
after: '\n<!-- Copyright 2024 MyCompany -->',
});
context.subscriptions.push(registerExportHook(hook));
createElementReplacerHook
Creates a hook that modifies specific element types.
import { createElementReplacerHook, registerExportHook } from './adapters/exportHooksAdapter';
// Wrap all paragraphs in a custom div
const hook = createElementReplacerHook('para-wrapper', {
elementType: 'paragraph',
backend: 'html',
replace: (rendered) => `<div class="custom-para">${rendered}</div>`,
});
context.subscriptions.push(registerExportHook(hook));
Hook Execution Order
Hooks are sorted by priority (higher numbers run first)
Default priority is 0
Multiple hooks chain together - each receives the output of the previous
Return
undefinedfrom a hook to skip modification and pass through
Error Handling
Hooks that throw errors are caught and logged. The export continues with the unmodified content, so a failing hook won't break the entire export.
Database Extension Points
Extension points for extracting custom data during indexing, running custom queries, and enriching graph visualizations. These enable building knowledge graphs, custom search algorithms, and entity extraction systems.
IndexerAdapter - Custom Content Extraction
Hook into the file indexing pipeline to extract entities, relationships, or custom metadata from files as they are indexed.
Location: src/adapters/indexerAdapter.ts
IndexerAdapter Interface
interface IndexerAdapter {
/** Unique identifier */
id: string;
/** Human-readable description */
description?: string;
/** Priority for ordering (higher first) */
priority?: number;
/** File types to process (['org', 'md']). Empty = all. */
fileTypes?: string[];
/** Extract custom data during indexing */
extract(
content: string,
ast: OrgDocumentNode | undefined,
context: IndexContext
): Promise<ExtractedData[]>;
/** Called when file is removed (cleanup) */
onFileRemoved?(filePath: string, fileId: number, db: unknown): Promise<void>;
}
interface IndexContext {
filePath: string;
fileId: number;
fileType: string;
mtime: number;
db: unknown; // Database client for custom queries
}
Extracted Data Types
// Entity - a named concept, person, project, etc.
interface ExtractedEntity {
type: 'entity';
category: string; // 'person', 'concept', 'project'
name: string;
properties?: Record<string, unknown>;
lineNumber?: number;
}
// Relationship between entities or files
interface ExtractedRelationship {
type: 'relationship';
source: string;
target: string;
relation: string; // 'references', 'is_part_of', 'related_to'
weight?: number;
properties?: Record<string, unknown>;
}
// Custom metadata about a file
interface ExtractedMetadata {
type: 'metadata';
key: string;
value: unknown;
}
Example: Entity Extractor
import { registerIndexer, IndexerAdapter } from './adapters/indexerAdapter';
const entityExtractor: IndexerAdapter = {
id: 'knowledge-graph-entities',
description: 'Extract named entities for knowledge graph',
fileTypes: ['org', 'md'],
async extract(content, ast, context) {
const entities: ExtractedEntity[] = [];
// Example: Extract #+CONCEPT: definitions
const conceptMatches = content.matchAll(/^#\+CONCEPT:\s*(.+)$/gm);
for (const match of conceptMatches) {
entities.push({
type: 'entity',
category: 'concept',
name: match[1].trim()
});
}
return entities;
}
};
context.subscriptions.push(registerIndexer(entityExtractor));
QueryProviderAdapter - Custom Search Types
Register custom query providers for specialized searches like graph traversal, path finding, or semantic similarity queries.
Location: src/adapters/queryProviderAdapter.ts
QueryProvider Interface
interface QueryProvider {
/** Query type identifier */
queryType: string;
/** Human-readable name */
name: string;
/** Description */
description?: string;
/** Capabilities (pagination, filtering, ranking) */
capabilities?: QueryCapabilities;
/** Validate parameters (return true or error message) */
validate?(params: QueryParams): true | string;
/** Execute the query */
execute(params: QueryParams, db: unknown): Promise<QueryResponse>;
}
interface QueryResponse {
results: QueryResult[];
totalCount?: number;
executionTimeMs?: number;
metadata?: Record<string, unknown>;
}
Built-in Query Providers
| Query Type | Description | Parameters |
|---|---|---|
| graph-path | Find shortest path between files | from, to, maxHops |
| related-files | Find files related by links and tags | filePath, limit |
Example: Custom Similarity Query
import { registerQueryProvider, QueryProvider } from './adapters/queryProviderAdapter';
const similarityProvider: QueryProvider = {
queryType: 'semantic-similarity',
name: 'Semantic Similarity Search',
description: 'Find semantically similar files using embeddings',
validate(params) {
if (!params.filePath) return 'Missing filePath';
return true;
},
async execute(params, db) {
const filePath = params.filePath as string;
const limit = (params.limit as number) || 10;
// Query embedding vectors and compute similarity
// ...
return {
results: similarFiles.map(f => ({
type: 'file',
id: f.path,
label: f.name,
score: f.similarity
}))
};
}
};
context.subscriptions.push(registerQueryProvider(similarityProvider));
GraphDataProviderAdapter - Graph Enrichment
Add custom data to link graph nodes and edges, or inject additional edges beyond the standard file links (e.g., semantic relationships).
Location: src/adapters/graphDataAdapter.ts
GraphDataProvider Interface
interface GraphDataProvider {
id: string;
name: string;
description?: string;
priority?: number;
/** Enrich node with custom properties */
enrichNode?(node: GraphNode, context: GraphDataContext): Promise<NodeEnrichment | undefined>;
/** Enrich edge with custom properties */
enrichEdge?(edge: GraphEdge, context: GraphDataContext): Promise<EdgeEnrichment | undefined>;
/** Add custom edges (e.g., semantic relationships) */
getCustomEdges?(filePath: string, context: GraphDataContext): Promise<CustomEdge[]>;
/** Filter out nodes */
filterNode?(node: GraphNode, context: GraphDataContext): boolean;
/** Filter out edges */
filterEdge?(edge: GraphEdge, context: GraphDataContext): boolean;
}
interface NodeEnrichment {
color?: string;
size?: number;
shape?: string;
label?: string;
tooltip?: string;
group?: string;
importance?: number;
properties?: Record<string, unknown>;
}
Built-in Providers
| Provider ID | Description |
|---|---|
| link-count-importance | Sizes nodes based on link count |
| recency-coloring | Colors nodes by modification recency |
Example: Cluster Provider
import { registerGraphDataProvider, GraphDataProvider } from './adapters/graphDataAdapter';
const clusterProvider: GraphDataProvider = {
id: 'topic-clusters',
name: 'Topic Clustering',
description: 'Group files by topic similarity',
async enrichNode(node, context) {
// Compute cluster membership
const cluster = await getFileCluster(node.id, context.db);
return {
group: cluster.name,
color: cluster.color,
properties: { clusterId: cluster.id }
};
},
async getCustomEdges(filePath, context) {
// Add edges to semantically similar files
const similar = await getSimilarFiles(filePath, context.db);
return similar.map(s => ({
from: filePath,
to: s.path,
type: 'semantic-similarity',
label: 'Similar',
style: 'dashed',
color: '#999'
}));
}
};
context.subscriptions.push(registerGraphDataProvider(clusterProvider));
Potential Future Extension Points
These patterns could be extended for additional plugin types:
Speed Commands
Custom single-key commands at headline beginnings.
interface SpeedCommand {
key: string;
description: string;
execute(editor: TextEditor, heading: HeadingNode): Promise<void>;
}
Capture Templates
Custom org-capture templates.
interface CaptureTemplate {
key: string;
description: string;
template: string;
target: CaptureTarget;
}
Entity Definitions
Custom special character entities (like org-entities).
interface OrgEntity {
name: string;
latex: string;
html: string;
unicode: string;
}
Document Linters
Custom lint rules for org documents.
interface LintRule {
id: string;
severity: 'error' | 'warning' | 'info';
check(document: OrgDocument): Diagnostic[];
}
VS Code Extension API
External VS Code extensions can contribute to scimax via the extension API.
Accessing the API
// In your extension's activate() function
export async function activate(context: vscode.ExtensionContext) {
const scimaxExt = vscode.extensions.getExtension('scimax.scimax-vscode');
if (!scimaxExt) {
console.log('Scimax extension not installed');
return;
}
// Ensure scimax is activated
const scimaxApi = scimaxExt.isActive
? scimaxExt.exports
: await scimaxExt.activate();
// Now use the API...
}
Available API Methods
registerBabelExecutor(executor)
Register a custom language executor for source blocks.
const disposable = scimaxApi.registerBabelExecutor({
languages: ['ruby', 'rb'],
async execute(code, context) {
return { success: true, stdout: 'result', stderr: '', executionTime: 100 };
},
async isAvailable() {
return true;
}
});
context.subscriptions.push(disposable);
createSimpleExecutor(options)
Helper to create a command-based executor.
const rubyExec = scimaxApi.createSimpleExecutor({
languages: ['ruby'],
command: 'ruby',
extension: '.rb',
});
context.subscriptions.push(scimaxApi.registerBabelExecutor(rubyExec));
registerLinkType(handler)
Register a custom link type handler.
const disposable = scimaxApi.registerLinkType({
type: 'jira',
description: 'Jira issue links',
resolve: (path) => ({
displayText: path,
url: `https://jira.example.com/browse/${path}`,
tooltip: `Open Jira issue ${path}`
}),
export: (path, desc, backend) => {
const url = `https://jira.example.com/browse/${path}`;
const text = desc || path;
if (backend === 'html') return `<a href="${url}">${text}</a>`;
if (backend === 'latex') return `\\href{${url}}{${text}}`;
return text;
}
});
context.subscriptions.push(disposable);
registerLinkFollowHandler(handler)
Register a VS Code action for when a link type is clicked.
const disposable = scimaxApi.registerLinkFollowHandler({
type: 'jira',
async follow(path, context) {
await vscode.env.openExternal(
vscode.Uri.parse(`https://jira.example.com/browse/${path}`)
);
}
});
context.subscriptions.push(disposable);
registerBlockExport(handler)
Register a custom export handler for special blocks.
const disposable = scimaxApi.registerBlockExport({
blockType: 'callout',
export: (content, backend, ctx) => {
if (backend === 'html') return `<div class="callout">${content}</div>`;
if (backend === 'latex') return `\\begin{tcolorbox}${content}\\end{tcolorbox}`;
return content;
},
});
context.subscriptions.push(disposable);
registerBlockHighlight(config)
Register visual highlighting for custom block types in the editor.
const disposable = scimaxApi.registerBlockHighlight({
blockType: 'callout',
backgroundColor: 'rgba(100, 149, 237, 0.1)',
borderColor: 'rgba(100, 149, 237, 0.5)',
headerColor: 'cornflowerblue',
});
context.subscriptions.push(disposable);
registerExportHook(hook)
Register a hook to customize the export process.
const disposable = scimaxApi.registerExportHook({
id: 'my-custom-hook',
// Modify options before export
preExport: (ctx) => ({ ...ctx.options, toc: true }),
// Transform output after export
postExport: (output, ctx) => {
if (ctx.backend === 'html') {
return `<!-- Custom Header -->\n${output}`;
}
return undefined;
},
// Filter specific elements
elementFilter: (rendered, ctx) => {
if (ctx.element.type === 'paragraph') {
return `<div class="para">${rendered}</div>`;
}
return undefined;
},
});
context.subscriptions.push(disposable);
createWrapperHook(id, options)
Helper to create a simple wrapper hook.
const hook = scimaxApi.createWrapperHook('my-wrapper', {
backend: 'html', // Optional: filter by backend
before: '<!-- Start -->',
after: '<!-- End -->',
priority: 10, // Optional: higher runs first
});
context.subscriptions.push(scimaxApi.registerExportHook(hook));
createElementReplacerHook(id, options)
Helper to create a hook that modifies specific element types.
const hook = scimaxApi.createElementReplacerHook('para-wrapper', {
elementType: ['paragraph', 'headline'], // Can be string or array
backend: 'html', // Optional: filter by backend
replace: (rendered, element) => {
return `<div class="custom">${rendered}</div>`;
},
});
context.subscriptions.push(scimaxApi.registerExportHook(hook));
Direct Registry Access
For advanced use cases, you can access the registries directly:
// Access all registries
const { registries } = scimaxApi;
// Check registered block types
registries.blockExport.getBlockTypes(); // ['warning', 'note', ...]
// Check if a block has a custom handler
registries.blockExport.hasHandler('callout'); // true/false
// Get highlight config for a block type
registries.blockHighlight.getConfig('warning');
// Get all export hooks
registries.exportHook.getHooks(); // Sorted by priority
// Check if a hook is registered
registries.exportHook.hasHook('my-hook'); // true/false
Utility Functions
// Check if a language is supported
scimaxApi.isLanguageSupported('python'); // true
// Get all registered languages
scimaxApi.getRegisteredLanguages(); // ['python', 'py', 'bash', ...]
Core Library Extraction
The src/parser/ directory is designed to be extractable as a standalone
npm library (@scimax/org-core) with no VS Code dependencies.
What's in Core
AST types (
orgElementTypes.ts)Parser (
orgParser.ts,orgExportParser.ts,orgParserUnified.ts)Export backends (
orgExport*.ts)Link resolution (
orgLinkTypes.ts- resolve/export only)Entities (
orgEntities.ts)Babel execution (
orgBabel.ts)
What's in VS Code Layer
Providers (
src/org/*.ts)Link follow actions (
src/adapters/linkFollowAdapter.ts)Commands and UI
Database and indexing