Comprehensive implementation guide for the hone-extensions project: built-in extensions that ship with the Hone IDE.
hone-extensions is a collection of approximately 12 built-in extensions that ship with every Hone installation. Each extension is a small, self-contained TypeScript package that imports only @honeide/api. Extensions are AOT-compiled by Perry (TypeScript to native binary compiler) into dynamic libraries and loaded by hone-core's extension host at runtime.
Each extension provides one or more of the following capabilities:
- Language support via LSP server configuration (launch command, arguments, initialization options)
- Syntax definitions via TextMate grammars or language configuration files
- Snippets for common code patterns
- Contributed commands accessible from the command palette and keybindings
- Configuration schemas for user-facing settings
- UI contributions such as status bar items, webview panels, and gutter decorations
The target extensions are:
| Extension | Primary Languages / Scope |
|---|---|
| typescript | TypeScript, JavaScript, TSX, JSX |
| python | Python |
| rust | Rust |
| go | Go |
| cpp | C, C++, Objective-C |
| html-css | HTML, CSS, SCSS, Less |
| json | JSON, JSONC |
| markdown | Markdown |
| git | Git integration (SCM, blame, graph) |
| docker | Dockerfile, docker-compose |
| toml-yaml | TOML, YAML |
Every extension is entirely independent of every other extension. A user could disable any subset without affecting the rest.
@honeide/api— the sole import for every extension. This package exposes the Hone Extension API surface: commands, languages, workspace, window, LSP client, configuration, status bar, webview, and lifecycle types.
No extension may import from another extension or from hone-core internals.
- None at runtime. Extensions do not bundle third-party npm packages. All logic is self-contained TypeScript compiled by Perry.
- LSP servers are external executables (e.g.,
typescript-language-server,pyright-langserver,rust-analyzer,gopls,clangd,taplo,yaml-language-server,dockerfile-language-server-nodejs). These are installed by the user via their system package manager,npm,pip,cargo, etc. Extensions configure and launch these servers but do not ship them.
perry— the TypeScript-to-native compiler@honeide/api— type definitions for compilation- A test runner (e.g.,
vitestor a Perry-native test harness)
Each extension is fully independent. There are no cross-extension imports, no shared state at runtime, and no ordering requirements for activation. The only shared code is the optional shared/lsp-helpers.ts utility module, which is compiled into each extension that uses it (no shared dynamic library).
hone-extensions/
├── extensions/
│ ├── typescript/
│ │ ├── hone-extension.json # Extension manifest
│ │ ├── src/
│ │ │ └── index.ts # Activation entry point
│ │ ├── snippets/
│ │ │ ├── typescript.json # TS/TSX snippets
│ │ │ └── javascript.json # JS/JSX snippets
│ │ ├── language-configuration.json # Bracket pairs, comments, folding
│ │ └── tsconfig.json
│ │
│ ├── python/
│ │ ├── hone-extension.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── snippets/
│ │ │ └── python.json
│ │ ├── language-configuration.json
│ │ └── tsconfig.json
│ │
│ ├── rust/
│ │ ├── hone-extension.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── snippets/
│ │ │ └── rust.json
│ │ ├── language-configuration.json
│ │ └── tsconfig.json
│ │
│ ├── go/
│ │ ├── hone-extension.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── snippets/
│ │ │ └── go.json
│ │ ├── language-configuration.json
│ │ └── tsconfig.json
│ │
│ ├── cpp/
│ │ ├── hone-extension.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── snippets/
│ │ │ ├── c.json
│ │ │ └── cpp.json
│ │ ├── language-configuration.json
│ │ └── tsconfig.json
│ │
│ ├── html-css/
│ │ ├── hone-extension.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── snippets/
│ │ │ ├── html.json
│ │ │ └── css.json
│ │ ├── language-configuration.json
│ │ └── tsconfig.json
│ │
│ ├── json/
│ │ ├── hone-extension.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── schemas/
│ │ │ └── catalog.json # Built-in schema catalog
│ │ ├── language-configuration.json
│ │ └── tsconfig.json
│ │
│ ├── markdown/
│ │ ├── hone-extension.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── snippets/
│ │ │ └── markdown.json
│ │ ├── preview/
│ │ │ ├── preview.html # Webview template for preview
│ │ │ └── preview.css
│ │ ├── language-configuration.json
│ │ └── tsconfig.json
│ │
│ ├── git/
│ │ ├── hone-extension.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ ├── blame.ts # Blame annotation logic
│ │ │ ├── graph.ts # Git graph visualization
│ │ │ └── statusbar.ts # Branch / dirty-state indicator
│ │ └── tsconfig.json
│ │
│ ├── docker/
│ │ ├── hone-extension.json
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── snippets/
│ │ │ └── dockerfile.json
│ │ ├── language-configuration.json
│ │ └── tsconfig.json
│ │
│ └── toml-yaml/
│ ├── hone-extension.json
│ ├── src/
│ │ └── index.ts
│ ├── snippets/
│ │ ├── toml.json
│ │ └── yaml.json
│ ├── language-configuration.json
│ └── tsconfig.json
│
├── shared/
│ └── lsp-helpers.ts # Shared LSP server launch utilities
│
├── tests/
│ ├── manifest-validation.test.ts # Validate every hone-extension.json
│ ├── activation.test.ts # Extension activation smoke tests
│ ├── lsp-config.test.ts # LSP configuration correctness
│ ├── snippets.test.ts # Snippet file parsing
│ └── fixtures/
│ └── mock-api.ts # Mock @honeide/api for testing
│
├── scripts/
│ ├── build-all.sh # Compile all extensions via Perry
│ ├── validate-manifests.ts # Manifest schema validation script
│ └── bundle.sh # Bundle extensions into IDE binary
│
├── package.json
├── tsconfig.base.json # Shared TypeScript config
├── perry.config.json # Perry compiler configuration
├── PROJECT_PLAN.md
└── LICENSE
Every extension must include a hone-extension.json file at its root. This manifest follows a strict schema:
{
"id": "hone.typescript",
"name": "TypeScript & JavaScript",
"version": "1.0.0",
"publisher": "hone",
"description": "TypeScript and JavaScript language support including IntelliSense, refactoring, and snippets",
"license": "MIT",
"engines": {
"hone": ">=0.1.0"
},
"main": "src/index.ts",
"activationEvents": [
"onLanguage:typescript",
"onLanguage:javascript",
"onLanguage:typescriptreact",
"onLanguage:javascriptreact"
],
"contributes": {
"languages": [
{
"id": "typescript",
"extensions": [".ts", ".mts", ".cts"],
"aliases": ["TypeScript", "ts"],
"configuration": "./language-configuration.json"
},
{
"id": "typescriptreact",
"extensions": [".tsx"],
"aliases": ["TypeScript React", "tsx"],
"configuration": "./language-configuration.json"
},
{
"id": "javascript",
"extensions": [".js", ".mjs", ".cjs"],
"aliases": ["JavaScript", "js"],
"configuration": "./language-configuration.json"
},
{
"id": "javascriptreact",
"extensions": [".jsx"],
"aliases": ["JavaScript React", "jsx"],
"configuration": "./language-configuration.json"
}
],
"lspServers": [
{
"id": "typescript-language-server",
"name": "TypeScript Language Server",
"languageIds": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
"command": "typescript-language-server",
"args": ["--stdio"],
"transport": "stdio",
"initializationOptions": {
"preferences": {
"includeCompletionsForModuleExports": true,
"includeCompletionsWithInsertText": true
}
},
"settings": {
"typescript.format.semicolons": "insert",
"typescript.inlayHints.parameterNames.enabled": "none",
"typescript.inlayHints.parameterTypes.enabled": false,
"typescript.inlayHints.variableTypes.enabled": false
}
}
],
"commands": [
{
"command": "hone.typescript.organizeImports",
"title": "Organize Imports",
"category": "TypeScript"
},
{
"command": "hone.typescript.addMissingImports",
"title": "Add All Missing Imports",
"category": "TypeScript"
},
{
"command": "hone.typescript.renameFile",
"title": "Rename File and Update Imports",
"category": "TypeScript"
},
{
"command": "hone.typescript.goToSourceDefinition",
"title": "Go to Source Definition",
"category": "TypeScript"
},
{
"command": "hone.typescript.restartServer",
"title": "Restart Language Server",
"category": "TypeScript"
}
],
"snippets": [
{
"language": "typescript",
"path": "./snippets/typescript.json"
},
{
"language": "typescriptreact",
"path": "./snippets/typescript.json"
},
{
"language": "javascript",
"path": "./snippets/javascript.json"
},
{
"language": "javascriptreact",
"path": "./snippets/javascript.json"
}
],
"configuration": {
"title": "TypeScript",
"properties": {
"typescript.format.semicolons": {
"type": "string",
"default": "insert",
"enum": ["insert", "remove"],
"description": "Whether to insert or remove semicolons at the end of statements"
},
"typescript.format.indentSize": {
"type": "number",
"default": 2,
"description": "Number of spaces for indentation"
},
"typescript.suggest.includeCompletionsForModuleExports": {
"type": "boolean",
"default": true,
"description": "Include auto-imports in completions"
},
"typescript.inlayHints.parameterNames.enabled": {
"type": "string",
"default": "none",
"enum": ["none", "literals", "all"],
"description": "Show parameter name inlay hints"
},
"typescript.inlayHints.variableTypes.enabled": {
"type": "boolean",
"default": false,
"description": "Show variable type inlay hints"
},
"typescript.inlayHints.functionLikeReturnTypes.enabled": {
"type": "boolean",
"default": false,
"description": "Show return type inlay hints for functions"
},
"typescript.preferences.importModuleSpecifier": {
"type": "string",
"default": "shortest",
"enum": ["shortest", "relative", "non-relative", "project-relative"],
"description": "Preferred style for auto-import module specifiers"
}
}
},
"keybindings": [
{
"command": "hone.typescript.organizeImports",
"key": "shift+alt+o",
"mac": "shift+alt+o",
"when": "editorTextFocus && editorLangId =~ /typescript|javascript/"
}
]
}
}{
"id": "hone.python",
"name": "Python",
"version": "1.0.0",
"publisher": "hone",
"description": "Python language support with Pyright-powered IntelliSense, linting, and debugging",
"license": "MIT",
"engines": {
"hone": ">=0.1.0"
},
"main": "src/index.ts",
"activationEvents": [
"onLanguage:python",
"workspaceContains:**/*.py",
"workspaceContains:Pipfile",
"workspaceContains:pyproject.toml"
],
"contributes": {
"languages": [
{
"id": "python",
"extensions": [".py", ".pyw", ".pyi"],
"aliases": ["Python", "py"],
"filenames": ["SConstruct", "SConscript"],
"firstLine": "^#!.*\\bpython[23w]?\\b",
"configuration": "./language-configuration.json"
}
],
"lspServers": [
{
"id": "pyright",
"name": "Pyright",
"languageIds": ["python"],
"command": "pyright-langserver",
"args": ["--stdio"],
"transport": "stdio",
"initializationOptions": {},
"settings": {
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"python.analysis.diagnosticMode": "openFilesOnly"
}
}
],
"commands": [
{
"command": "hone.python.runFile",
"title": "Run Python File",
"category": "Python"
},
{
"command": "hone.python.runSelection",
"title": "Run Selection/Line in Python Terminal",
"category": "Python"
},
{
"command": "hone.python.selectInterpreter",
"title": "Select Python Interpreter",
"category": "Python"
},
{
"command": "hone.python.createVirtualEnvironment",
"title": "Create Virtual Environment",
"category": "Python"
},
{
"command": "hone.python.restartServer",
"title": "Restart Language Server",
"category": "Python"
}
],
"snippets": [
{
"language": "python",
"path": "./snippets/python.json"
}
],
"configuration": {
"title": "Python",
"properties": {
"python.pythonPath": {
"type": "string",
"default": "python3",
"description": "Path to the Python interpreter"
},
"python.analysis.typeCheckingMode": {
"type": "string",
"default": "basic",
"enum": ["off", "basic", "standard", "strict"],
"enumDescriptions": [
"No type checking",
"Basic type checking rules",
"Standard type checking rules",
"Strict type checking rules"
],
"description": "Level of type checking analysis"
},
"python.analysis.autoImportCompletions": {
"type": "boolean",
"default": true,
"description": "Include auto-import suggestions in completions"
},
"python.analysis.diagnosticMode": {
"type": "string",
"default": "openFilesOnly",
"enum": ["openFilesOnly", "workspace"],
"description": "Scope of analysis for diagnostics"
},
"python.formatting.provider": {
"type": "string",
"default": "none",
"enum": ["none", "black", "ruff", "autopep8", "yapf"],
"description": "Python formatting provider"
},
"python.linting.enabled": {
"type": "boolean",
"default": true,
"description": "Enable linting"
}
}
},
"keybindings": [
{
"command": "hone.python.runFile",
"key": "ctrl+shift+f10",
"mac": "cmd+shift+f10",
"when": "editorTextFocus && editorLangId == python"
},
{
"command": "hone.python.runSelection",
"key": "shift+enter",
"mac": "shift+enter",
"when": "editorTextFocus && editorLangId == python"
}
]
}
}{
"id": "hone.git",
"name": "Git",
"version": "1.0.0",
"publisher": "hone",
"description": "Git source control integration with blame, graph, and branch management",
"license": "MIT",
"engines": {
"hone": ">=0.1.0"
},
"main": "src/index.ts",
"activationEvents": [
"workspaceContains:.git",
"*"
],
"contributes": {
"commands": [
{
"command": "hone.git.stageFile",
"title": "Stage File",
"category": "Git"
},
{
"command": "hone.git.unstageFile",
"title": "Unstage File",
"category": "Git"
},
{
"command": "hone.git.stageAll",
"title": "Stage All Changes",
"category": "Git"
},
{
"command": "hone.git.commitAll",
"title": "Commit All",
"category": "Git"
},
{
"command": "hone.git.commit",
"title": "Commit Staged",
"category": "Git"
},
{
"command": "hone.git.push",
"title": "Push",
"category": "Git"
},
{
"command": "hone.git.pull",
"title": "Pull",
"category": "Git"
},
{
"command": "hone.git.fetch",
"title": "Fetch",
"category": "Git"
},
{
"command": "hone.git.createBranch",
"title": "Create Branch",
"category": "Git"
},
{
"command": "hone.git.switchBranch",
"title": "Switch Branch",
"category": "Git"
},
{
"command": "hone.git.deleteBranch",
"title": "Delete Branch",
"category": "Git"
},
{
"command": "hone.git.viewLog",
"title": "View Git Log",
"category": "Git"
},
{
"command": "hone.git.viewGraph",
"title": "View Git Graph",
"category": "Git"
},
{
"command": "hone.git.toggleBlame",
"title": "Toggle Blame Annotations",
"category": "Git"
},
{
"command": "hone.git.viewFileHistory",
"title": "View File History",
"category": "Git"
},
{
"command": "hone.git.diffWithPrevious",
"title": "Diff with Previous Revision",
"category": "Git"
},
{
"command": "hone.git.stash",
"title": "Stash Changes",
"category": "Git"
},
{
"command": "hone.git.stashPop",
"title": "Pop Latest Stash",
"category": "Git"
}
],
"configuration": {
"title": "Git",
"properties": {
"git.enabled": {
"type": "boolean",
"default": true,
"description": "Enable Git integration"
},
"git.path": {
"type": "string",
"default": "git",
"description": "Path to the Git executable"
},
"git.autofetch": {
"type": "boolean",
"default": false,
"description": "Automatically fetch from remotes periodically"
},
"git.autofetchPeriod": {
"type": "number",
"default": 180,
"description": "Period in seconds between automatic fetches"
},
"git.blame.enabled": {
"type": "boolean",
"default": true,
"description": "Enable inline blame annotations"
},
"git.blame.format": {
"type": "string",
"default": "${author}, ${date} - ${message}",
"description": "Format string for blame annotations"
},
"git.confirmSync": {
"type": "boolean",
"default": true,
"description": "Confirm before synchronizing git repositories"
},
"git.enableSmartCommit": {
"type": "boolean",
"default": false,
"description": "Commit all changes when there are no staged changes"
}
}
},
"keybindings": [
{
"command": "hone.git.commit",
"key": "ctrl+shift+g ctrl+enter",
"mac": "cmd+shift+g cmd+enter",
"when": "scmFocus"
},
{
"command": "hone.git.toggleBlame",
"key": "ctrl+shift+g b",
"mac": "cmd+shift+g b",
"when": "editorTextFocus"
}
]
}
}Every extension follows the same activation pattern in src/index.ts:
import * as hone from '@honeide/api';
/**
* Called when the extension is activated (based on activationEvents).
* This is the main entry point where the extension registers all its
* contributions: commands, LSP clients, event listeners, etc.
*/
export function activate(context: hone.ExtensionContext): void {
// 1. Read configuration
const config = hone.workspace.getConfiguration('extensionName');
// 2. Register LSP client (if applicable)
const lspClient = hone.languages.registerLanguageServer({
id: 'server-id',
name: 'Server Name',
languageIds: ['languageId'],
command: config.get<string>('serverCommand', 'default-command'),
args: ['--stdio'],
initializationOptions: {},
});
context.subscriptions.push(lspClient);
// 3. Register commands
context.subscriptions.push(
hone.commands.registerCommand('hone.ext.commandName', async () => {
// Command implementation
})
);
// 4. Register event listeners
context.subscriptions.push(
hone.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('extensionName')) {
// Handle configuration change
}
})
);
// 5. Set up status bar items, decorations, etc.
const statusItem = hone.window.createStatusBarItem(hone.StatusBarAlignment.Left);
statusItem.text = 'Extension Active';
statusItem.show();
context.subscriptions.push(statusItem);
}
/**
* Called when the extension is deactivated (editor shutdown or extension disabled).
* Clean up any resources not managed by context.subscriptions.
*/
export function deactivate(): void {
// Any additional cleanup beyond context.subscriptions disposal
}import * as hone from '@honeide/api';
/**
* Attempts to find an executable in PATH and common installation locations.
* Returns the resolved path or null if not found.
*/
export function findExecutable(name: string, additionalPaths?: string[]): string | null {
// Check PATH first
const pathResult = hone.env.which(name);
if (pathResult) return pathResult;
// Check additional common locations
const commonPaths = [
`/usr/local/bin/${name}`,
`/usr/bin/${name}`,
`${hone.env.homeDir}/.local/bin/${name}`,
`${hone.env.homeDir}/.cargo/bin/${name}`,
...(additionalPaths ?? []),
];
for (const p of commonPaths) {
if (hone.env.fileExists(p)) return p;
}
return null;
}
/**
* Creates a standard LSP server registration with error handling and
* automatic restart on crash.
*/
export function registerLspServer(
context: hone.ExtensionContext,
options: {
id: string;
name: string;
languageIds: string[];
command: string;
args: string[];
initializationOptions?: Record<string, unknown>;
settings?: Record<string, unknown>;
maxRestarts?: number;
}
): hone.LanguageServerClient | null {
const executable = findExecutable(options.command);
if (!executable) {
hone.window.showWarningMessage(
`${options.name}: Could not find '${options.command}' in PATH. ` +
`Please install it and reload, or set the path manually in settings.`
);
return null;
}
const client = hone.languages.registerLanguageServer({
id: options.id,
name: options.name,
languageIds: options.languageIds,
command: executable,
args: options.args,
transport: 'stdio',
initializationOptions: options.initializationOptions ?? {},
settings: options.settings ?? {},
maxRestarts: options.maxRestarts ?? 5,
});
context.subscriptions.push(client);
return client;
}
/**
* Sends a custom request to the LSP server and returns the result.
*/
export async function sendLspRequest<T>(
client: hone.LanguageServerClient,
method: string,
params: unknown
): Promise<T> {
return client.sendRequest<T>(method, params);
}Activation triggers: onLanguage:typescript, onLanguage:javascript, onLanguage:typescriptreact, onLanguage:javascriptreact
LSP Server: typescript-language-server --stdio
The TypeScript Language Server wraps the official tsserver and exposes it via the Language Server Protocol. It provides completions, diagnostics, hover, go-to-definition, rename, code actions, and formatting.
Activation Logic (src/index.ts):
import * as hone from '@honeide/api';
import { registerLspServer, sendLspRequest } from '../../shared/lsp-helpers';
let client: hone.LanguageServerClient | null = null;
export function activate(context: hone.ExtensionContext): void {
const config = hone.workspace.getConfiguration('typescript');
// Register LSP
client = registerLspServer(context, {
id: 'typescript-language-server',
name: 'TypeScript Language Server',
languageIds: ['typescript', 'typescriptreact', 'javascript', 'javascriptreact'],
command: 'typescript-language-server',
args: ['--stdio'],
initializationOptions: {
preferences: {
includeCompletionsForModuleExports:
config.get<boolean>('suggest.includeCompletionsForModuleExports', true),
includeCompletionsWithInsertText: true,
},
},
settings: {
'typescript.format.semicolons': config.get<string>('format.semicolons', 'insert'),
'typescript.format.indentSize': config.get<number>('format.indentSize', 2),
},
});
// Register commands
context.subscriptions.push(
hone.commands.registerCommand('hone.typescript.organizeImports', async () => {
const editor = hone.window.activeTextEditor;
if (!editor || !client) return;
const params = {
command: '_typescript.organizeImports',
arguments: [{ file: editor.document.uri.toString() }],
};
await sendLspRequest(client, 'workspace/executeCommand', params);
})
);
context.subscriptions.push(
hone.commands.registerCommand('hone.typescript.addMissingImports', async () => {
const editor = hone.window.activeTextEditor;
if (!editor || !client) return;
const params = {
command: '_typescript.addMissingImports',
arguments: [{ file: editor.document.uri.toString() }],
};
await sendLspRequest(client, 'workspace/executeCommand', params);
})
);
context.subscriptions.push(
hone.commands.registerCommand('hone.typescript.renameFile', async () => {
const editor = hone.window.activeTextEditor;
if (!editor) return;
const oldUri = editor.document.uri;
const newUri = await hone.window.showInputBox({
prompt: 'New file name',
value: oldUri.fsPath,
});
if (newUri && client) {
await sendLspRequest(client, 'workspace/executeCommand', {
command: '_typescript.applyRenameFile',
arguments: [{ sourceUri: oldUri.toString(), targetUri: newUri }],
});
}
})
);
context.subscriptions.push(
hone.commands.registerCommand('hone.typescript.goToSourceDefinition', async () => {
const editor = hone.window.activeTextEditor;
if (!editor || !client) return;
const position = editor.selection.active;
const result = await sendLspRequest<hone.Location[]>(
client,
'workspace/executeCommand',
{
command: '_typescript.goToSourceDefinition',
arguments: [editor.document.uri.toString(), position],
}
);
if (result && result.length > 0) {
await hone.window.showTextDocument(result[0].uri, {
selection: result[0].range,
});
}
})
);
context.subscriptions.push(
hone.commands.registerCommand('hone.typescript.restartServer', async () => {
if (client) {
await client.restart();
hone.window.showInformationMessage('TypeScript Language Server restarted.');
}
})
);
// React to configuration changes
context.subscriptions.push(
hone.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('typescript') && client) {
const updatedConfig = hone.workspace.getConfiguration('typescript');
client.sendNotification('workspace/didChangeConfiguration', {
settings: {
'typescript.format.semicolons': updatedConfig.get('format.semicolons'),
'typescript.format.indentSize': updatedConfig.get('format.indentSize'),
},
});
}
})
);
}
export function deactivate(): void {
client = null;
}Snippets (snippets/typescript.json):
{
"Import Default": {
"prefix": "impd",
"body": ["import ${2:name} from '${1:module}';$0"],
"description": "Import default export"
},
"Import Named": {
"prefix": "imp",
"body": ["import { $2 } from '${1:module}';$0"],
"description": "Import named exports"
},
"Export Default": {
"prefix": "expd",
"body": ["export default $1;$0"],
"description": "Export default"
},
"Export Named Function": {
"prefix": "expf",
"body": [
"export function ${1:name}(${2:params}): ${3:void} {",
"\t$0",
"}"
],
"description": "Export named function"
},
"Arrow Function": {
"prefix": "af",
"body": ["const ${1:name} = (${2:params})${3: => ${4}} => {", "\t$0", "};"],
"description": "Arrow function"
},
"Async Arrow Function": {
"prefix": "aaf",
"body": ["const ${1:name} = async (${2:params})${3} => {", "\t$0", "};"],
"description": "Async arrow function"
},
"Function": {
"prefix": "fn",
"body": ["function ${1:name}(${2:params}): ${3:void} {", "\t$0", "}"],
"description": "Function declaration"
},
"Interface": {
"prefix": "intf",
"body": ["interface ${1:Name} {", "\t$0", "}"],
"description": "Interface declaration"
},
"Type Alias": {
"prefix": "type",
"body": ["type ${1:Name} = ${2:string};$0"],
"description": "Type alias"
},
"Class": {
"prefix": "cls",
"body": [
"class ${1:Name} {",
"\tconstructor(${2:params}) {",
"\t\t$0",
"\t}",
"}"
],
"description": "Class declaration"
},
"Try Catch": {
"prefix": "trycatch",
"body": [
"try {",
"\t$1",
"} catch (${2:error}) {",
"\t$0",
"}"
],
"description": "Try/catch block"
},
"Console Log": {
"prefix": "clg",
"body": ["console.log($1);$0"],
"description": "Console log"
},
"Console Error": {
"prefix": "cle",
"body": ["console.error($1);$0"],
"description": "Console error"
},
"If Statement": {
"prefix": "if",
"body": ["if (${1:condition}) {", "\t$0", "}"],
"description": "If statement"
},
"For Of": {
"prefix": "forof",
"body": ["for (const ${1:item} of ${2:iterable}) {", "\t$0", "}"],
"description": "For...of loop"
}
}Configuration surface:
| Setting | Type | Default | Description |
|---|---|---|---|
typescript.format.semicolons |
string |
"insert" |
Insert or remove semicolons |
typescript.format.indentSize |
number |
2 |
Indentation size |
typescript.suggest.includeCompletionsForModuleExports |
boolean |
true |
Auto-import completions |
typescript.inlayHints.parameterNames.enabled |
string |
"none" |
Parameter name hints |
typescript.inlayHints.variableTypes.enabled |
boolean |
false |
Variable type hints |
typescript.inlayHints.functionLikeReturnTypes.enabled |
boolean |
false |
Return type hints |
typescript.preferences.importModuleSpecifier |
string |
"shortest" |
Import path style |
Activation triggers: onLanguage:python, workspaceContains:**/*.py, workspaceContains:Pipfile, workspaceContains:pyproject.toml
LSP Server: pyright-langserver --stdio (Pyright, the default) or pylsp (alternative, user-configurable)
Activation Logic (src/index.ts):
import * as hone from '@honeide/api';
import { registerLspServer } from '../../shared/lsp-helpers';
let client: hone.LanguageServerClient | null = null;
export function activate(context: hone.ExtensionContext): void {
const config = hone.workspace.getConfiguration('python');
// Register LSP (Pyright by default)
client = registerLspServer(context, {
id: 'pyright',
name: 'Pyright',
languageIds: ['python'],
command: 'pyright-langserver',
args: ['--stdio'],
settings: {
'python.analysis.typeCheckingMode': config.get('analysis.typeCheckingMode', 'basic'),
'python.analysis.autoImportCompletions': config.get('analysis.autoImportCompletions', true),
'python.analysis.diagnosticMode': config.get('analysis.diagnosticMode', 'openFilesOnly'),
'python.pythonPath': config.get('pythonPath', 'python3'),
},
});
// Run file command
context.subscriptions.push(
hone.commands.registerCommand('hone.python.runFile', async () => {
const editor = hone.window.activeTextEditor;
if (!editor) return;
const pythonPath = config.get<string>('pythonPath', 'python3');
const terminal = hone.window.createTerminal('Python');
terminal.sendText(`${pythonPath} "${editor.document.uri.fsPath}"`);
terminal.show();
})
);
// Run selection command
context.subscriptions.push(
hone.commands.registerCommand('hone.python.runSelection', async () => {
const editor = hone.window.activeTextEditor;
if (!editor) return;
const selection = editor.selection;
const text = selection.isEmpty
? editor.document.lineAt(selection.active.line).text
: editor.document.getText(selection);
const terminal = hone.window.getActiveTerminal() ?? hone.window.createTerminal('Python');
terminal.sendText(text);
terminal.show();
})
);
// Select interpreter command
context.subscriptions.push(
hone.commands.registerCommand('hone.python.selectInterpreter', async () => {
// Discover Python interpreters on the system
const interpreters = await discoverPythonInterpreters();
const selected = await hone.window.showQuickPick(
interpreters.map((i) => ({
label: i.version,
description: i.path,
detail: i.isVenv ? 'Virtual environment' : 'System',
})),
{ placeHolder: 'Select a Python interpreter' }
);
if (selected) {
await hone.workspace
.getConfiguration('python')
.update('pythonPath', selected.description);
hone.window.showInformationMessage(`Python interpreter set to: ${selected.description}`);
}
})
);
// Create virtual environment command
context.subscriptions.push(
hone.commands.registerCommand('hone.python.createVirtualEnvironment', async () => {
const name = await hone.window.showInputBox({
prompt: 'Virtual environment name',
value: '.venv',
});
if (!name) return;
const pythonPath = config.get<string>('pythonPath', 'python3');
const workspaceFolder = hone.workspace.workspaceFolders?.[0];
if (!workspaceFolder) return;
const terminal = hone.window.createTerminal('Python');
terminal.sendText(`${pythonPath} -m venv ${name}`);
terminal.show();
})
);
// Restart server command
context.subscriptions.push(
hone.commands.registerCommand('hone.python.restartServer', async () => {
if (client) {
await client.restart();
hone.window.showInformationMessage('Python Language Server restarted.');
}
})
);
// Status bar item showing current Python interpreter
const statusItem = hone.window.createStatusBarItem(hone.StatusBarAlignment.Right, 100);
statusItem.command = 'hone.python.selectInterpreter';
updatePythonStatusBar(statusItem, config.get<string>('pythonPath', 'python3'));
statusItem.show();
context.subscriptions.push(statusItem);
context.subscriptions.push(
hone.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('python.pythonPath')) {
const newPath = hone.workspace.getConfiguration('python').get<string>('pythonPath', 'python3');
updatePythonStatusBar(statusItem, newPath);
}
})
);
}
async function discoverPythonInterpreters(): Promise<
Array<{ path: string; version: string; isVenv: boolean }>
> {
// Implementation: search PATH, common locations, .venv directories
// Return list of discovered interpreters
return [];
}
function updatePythonStatusBar(item: hone.StatusBarItem, pythonPath: string): void {
item.text = `$(symbol-misc) ${pythonPath}`;
item.tooltip = `Python Interpreter: ${pythonPath}`;
}
export function deactivate(): void {
client = null;
}Snippets (snippets/python.json):
{
"Function Definition": {
"prefix": "def",
"body": [
"def ${1:function_name}(${2:params})${3: -> ${4:None}}:",
"\t${5:\"\"\"${6:Docstring.}\"\"\"}",
"\t${0:pass}"
],
"description": "Function definition"
},
"Class Definition": {
"prefix": "class",
"body": [
"class ${1:ClassName}${2:(${3:BaseClass})}:",
"\t${4:\"\"\"${5:Class docstring.}\"\"\"}",
"",
"\tdef __init__(self${6:, ${7:args}}):",
"\t\t${0:pass}"
],
"description": "Class definition"
},
"If Name Main": {
"prefix": "ifmain",
"body": [
"if __name__ == '__main__':",
"\t${0:main()}"
],
"description": "if __name__ == '__main__'"
},
"Try Except": {
"prefix": "tryex",
"body": [
"try:",
"\t${1:pass}",
"except ${2:Exception} as ${3:e}:",
"\t${0:raise}"
],
"description": "Try/except block"
},
"Try Except Finally": {
"prefix": "tryexf",
"body": [
"try:",
"\t${1:pass}",
"except ${2:Exception} as ${3:e}:",
"\t${4:raise}",
"finally:",
"\t${0:pass}"
],
"description": "Try/except/finally block"
},
"List Comprehension": {
"prefix": "lc",
"body": ["[${1:expr} for ${2:item} in ${3:iterable}${4: if ${5:condition}}]$0"],
"description": "List comprehension"
},
"Dictionary Comprehension": {
"prefix": "dc",
"body": ["{${1:key}: ${2:value} for ${3:item} in ${4:iterable}${5: if ${6:condition}}}$0"],
"description": "Dictionary comprehension"
},
"With Statement": {
"prefix": "with",
"body": [
"with ${1:expression} as ${2:variable}:",
"\t${0:pass}"
],
"description": "With statement (context manager)"
},
"Async Function": {
"prefix": "adef",
"body": [
"async def ${1:function_name}(${2:params})${3: -> ${4:None}}:",
"\t${0:pass}"
],
"description": "Async function definition"
},
"Dataclass": {
"prefix": "dataclass",
"body": [
"from dataclasses import dataclass",
"",
"@dataclass",
"class ${1:ClassName}:",
"\t${2:field}: ${3:str}$0"
],
"description": "Dataclass definition"
}
}Activation triggers: onLanguage:rust, workspaceContains:Cargo.toml
LSP Server: rust-analyzer
Commands:
| Command | Title | Description |
|---|---|---|
hone.rust.run |
Run | cargo run in terminal |
hone.rust.check |
Check | cargo check and show diagnostics |
hone.rust.test |
Test | cargo test in terminal |
hone.rust.expandMacro |
Expand Macro Recursively | Show expanded macro output |
hone.rust.viewHir |
View HIR | Show High-level IR for selection |
hone.rust.openDocs |
Open docs.rs | Open documentation for item under cursor |
hone.rust.parentModule |
Locate Parent Module | Navigate to parent module |
hone.rust.matchingBrace |
Go to Matching Brace | Navigate to matching brace |
hone.rust.restartServer |
Restart Server | Restart rust-analyzer |
Configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
rust.checkOnSave.command |
string |
"check" |
Cargo command to run on save (check, clippy) |
rust.cargo.features |
array |
[] |
List of features to activate |
rust.cargo.allFeatures |
boolean |
false |
Activate all available features |
rust.procMacro.enable |
boolean |
true |
Enable procedural macro support |
rust.inlayHints.typeHints.enable |
boolean |
true |
Show type inlay hints |
rust.inlayHints.parameterHints.enable |
boolean |
true |
Show parameter inlay hints |
rust.inlayHints.chainingHints.enable |
boolean |
true |
Show chaining inlay hints |
Activation Logic (summary):
- Register
rust-analyzeras LSP server with configured features and check-on-save behavior. - Register commands for
cargo run,cargo check,cargo testthat open a terminal and execute the command. - Register
expandMacroandviewHiras custom LSP requests (rust-analyzer/expandMacro,rust-analyzer/viewHir) and show results in a new editor tab. - Register
openDocscommand that queries rust-analyzer for the external documentation URL and opens it in the system browser. - Show a status bar item indicating the active Rust toolchain (e.g., "stable-x86_64-apple-darwin").
Activation triggers: onLanguage:go, onLanguage:gomod, workspaceContains:go.mod
LSP Server: gopls
Commands:
| Command | Title | Description |
|---|---|---|
hone.go.test |
Run Test | go test for current package |
hone.go.testFunction |
Run Test at Cursor | Run specific test function |
hone.go.tidy |
Go Mod Tidy | go mod tidy |
hone.go.generateTests |
Generate Tests | Generate table-driven tests for function |
hone.go.addTags |
Add Struct Tags | Add JSON/YAML tags to struct fields |
hone.go.fillStruct |
Fill Struct Literal | Fill struct fields with zero values |
hone.go.toggleTestFile |
Toggle Test File | Switch between foo.go and foo_test.go |
hone.go.restartServer |
Restart Server | Restart gopls |
Configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
go.gopath |
string |
"" |
Override GOPATH |
go.formatTool |
string |
"goimports" |
Format tool (gofmt, goimports, golines) |
go.lintTool |
string |
"staticcheck" |
Lint tool (staticcheck, golangci-lint) |
go.buildFlags |
array |
[] |
Build flags passed to go commands |
go.testFlags |
array |
[] |
Additional flags for go test |
go.useLanguageServer |
boolean |
true |
Use gopls |
Activation Logic (summary):
- Register
goplsas LSP server. Forward relevant settings (build flags, formatting preferences) as initialization options. - Register test commands that detect the test function at the cursor position (by parsing the document) and run
go test -run <TestName>. - Register
tidycommand that runsgo mod tidyin a terminal. - Register
toggleTestFilecommand that switches between implementation and test file. - Show a status bar item with the Go version.
Activation triggers: onLanguage:c, onLanguage:cpp, onLanguage:objective-c, workspaceContains:compile_commands.json, workspaceContains:CMakeLists.txt
LSP Server: clangd
Commands:
| Command | Title | Description |
|---|---|---|
hone.cpp.switchHeaderSource |
Switch Header/Source | Toggle between .h/.hpp and .c/.cpp |
hone.cpp.restartServer |
Restart Server | Restart clangd |
Configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
cpp.clangdPath |
string |
"clangd" |
Path to clangd executable |
cpp.clangdArgs |
array |
[] |
Additional arguments for clangd |
cpp.compileCommands |
string |
"" |
Path to compile_commands.json directory |
cpp.fallbackFlags |
array |
["-std=c++17"] |
Compiler flags when no compile_commands.json |
Activation Logic (summary):
- Register
clangdwith configuration for compile commands path and fallback flags (passed as--compile-commands-dirand--query-driverargs). - Implement
switchHeaderSourceusing clangd's customtextDocument/switchSourceHeaderLSP request. - Detect compile_commands.json in workspace and pass its directory to clangd.
- Show inlay hints for deduced types (forwarded from clangd's built-in inlay hint support).
Activation triggers: onLanguage:html, onLanguage:css, onLanguage:scss, onLanguage:less
LSP Server: Built-in HTML language service and CSS language service (bundled as part of the extension, not external executables). Alternatively, vscode-html-language-server --stdio and vscode-css-language-server --stdio from the vscode-langservers-extracted npm package.
Commands:
| Command | Title | Description |
|---|---|---|
hone.html.expandAbbreviation |
Emmet: Expand Abbreviation | Expand Emmet abbreviation |
hone.html.wrapWithAbbreviation |
Emmet: Wrap with Abbreviation | Wrap selection with Emmet |
hone.html.removeTag |
Remove Tag | Remove surrounding HTML tag |
hone.html.updateTag |
Update Tag | Rename matching open/close tags |
hone.css.colorPicker |
Pick Color | Open color picker for CSS color value |
Emmet Integration:
Emmet abbreviation expansion is a core feature. The extension includes a lightweight Emmet engine that expands abbreviations like div.container>ul>li*5 into full HTML. This is triggered:
- On Tab key when the cursor is at the end of an Emmet abbreviation
- Explicitly via the
expandAbbreviationcommand - As a completion item in the suggestions list
Additional Features:
- Auto-closing tags: When typing
>to close an opening tag, automatically insert the closing tag. - Auto-rename tags: When renaming an opening tag, automatically rename the matching closing tag.
- Color decorators: Show inline color swatches next to CSS color values.
- CSS property value completions: Including vendor prefixes and browser compatibility data.
Configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
html.autoClosingTags |
boolean |
true |
Auto-close HTML tags |
html.autoRenameTag |
boolean |
true |
Auto-rename matching tags |
html.format.enable |
boolean |
true |
Enable HTML formatting |
html.format.wrapLineLength |
number |
120 |
Maximum line length before wrapping |
css.validate |
boolean |
true |
Enable CSS validation |
css.lint.unknownProperties |
string |
"warning" |
Unknown property severity |
emmet.triggerExpansionOnTab |
boolean |
true |
Expand Emmet on Tab |
emmet.showAbbreviationSuggestions |
boolean |
true |
Show Emmet in suggestions |
Activation triggers: onLanguage:json, onLanguage:jsonc
LSP Server: vscode-json-language-server --stdio (from vscode-langservers-extracted)
Schema Validation:
The JSON extension provides intelligent validation, completion, and hover documentation by matching JSON files to schemas. It ships with a built-in catalog of common schemas (from SchemaStore.org) and allows users to define custom schema associations.
Built-in schema associations:
| File Pattern | Schema |
|---|---|
package.json |
npm package.json schema |
tsconfig.json, tsconfig.*.json |
TypeScript configuration schema |
.eslintrc.json |
ESLint configuration schema |
.prettierrc, .prettierrc.json |
Prettier configuration schema |
hone-extension.json |
Hone extension manifest schema |
perry.config.json |
Perry configuration schema |
*.schema.json |
JSON Schema draft-07 meta-schema |
Commands:
| Command | Title | Description |
|---|---|---|
hone.json.sortKeys |
Sort JSON Keys | Sort object keys alphabetically |
hone.json.formatDocument |
Format JSON | Pretty-print JSON document |
hone.json.minify |
Minify JSON | Remove whitespace from JSON |
Configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
json.schemas |
array |
[] |
Custom schema associations ([{ fileMatch, url }]) |
json.format.enable |
boolean |
true |
Enable JSON formatting |
json.format.keepLines |
boolean |
false |
Keep existing line structure when formatting |
json.validate.enable |
boolean |
true |
Enable JSON validation |
json.schemaDownload.enable |
boolean |
true |
Allow downloading schemas from the internet |
Activation triggers: onLanguage:markdown
LSP Server: None (Markdown support is implemented directly in the extension).
Commands:
| Command | Title | Description |
|---|---|---|
hone.markdown.showPreview |
Open Preview | Open rendered preview in side panel |
hone.markdown.showPreviewToSide |
Open Preview to the Side | Open preview beside editor |
hone.markdown.toggleBold |
Toggle Bold | Wrap/unwrap selection with ** |
hone.markdown.toggleItalic |
Toggle Italic | Wrap/unwrap selection with * |
hone.markdown.toggleCode |
Toggle Code | Wrap/unwrap with backticks |
hone.markdown.toggleCodeBlock |
Toggle Code Block | Wrap/unwrap with triple backticks |
hone.markdown.toggleStrikethrough |
Toggle Strikethrough | Wrap/unwrap with ~~ |
hone.markdown.insertLink |
Insert Link | Insert [text](url) template |
hone.markdown.insertImage |
Insert Image | Insert  template |
hone.markdown.insertTable |
Insert Table | Insert markdown table template |
hone.markdown.generateToc |
Generate Table of Contents | Insert TOC based on headings |
hone.markdown.updateToc |
Update Table of Contents | Refresh existing TOC |
Preview Rendering:
The Markdown preview uses hone.window.createWebviewPanel() to render a live HTML preview. The preview:
- Renders on every document change (debounced at 300ms)
- Supports GitHub Flavored Markdown (tables, task lists, strikethrough, autolinks)
- Scrolls in sync with the editor
- Supports light/dark theme matching the editor theme
- Uses a sandboxed webview with a Content Security Policy
Snippets (snippets/markdown.json):
{
"Link": {
"prefix": "link",
"body": ["[${1:text}](${2:url})$0"],
"description": "Insert link"
},
"Image": {
"prefix": "img",
"body": ["$0"],
"description": "Insert image"
},
"Code Block": {
"prefix": "code",
"body": ["```${1:language}", "${0}", "```"],
"description": "Insert fenced code block"
},
"Table": {
"prefix": "table",
"body": [
"| ${1:Header 1} | ${2:Header 2} | ${3:Header 3} |",
"| --- | --- | --- |",
"| ${4:Cell 1} | ${5:Cell 2} | ${6:Cell 3} |$0"
],
"description": "Insert table"
},
"Task List": {
"prefix": "task",
"body": ["- [ ] ${1:Task}$0"],
"description": "Insert task list item"
},
"Collapsible Section": {
"prefix": "details",
"body": [
"<details>",
"<summary>${1:Summary}</summary>",
"",
"${0}",
"",
"</details>"
],
"description": "Insert collapsible section"
}
}Configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
markdown.preview.fontSize |
number |
14 |
Preview font size |
markdown.preview.lineHeight |
number |
1.6 |
Preview line height |
markdown.preview.scrollSync |
boolean |
true |
Sync scrolling between editor and preview |
markdown.preview.typographer |
boolean |
false |
Enable typographic replacements (quotes, dashes) |
Activation triggers: workspaceContains:.git, * (activates on startup for status bar)
LSP Server: None. The Git extension uses hone-core's built-in Git client API (hone.git.*) to interact with the repository.
Architecture:
The Git extension is the most complex built-in extension. It is split into multiple source files:
src/index.ts— Main activation, command registration, event wiringsrc/blame.ts— Inline blame annotations and gutter blame decorationssrc/graph.ts— Git graph visualization (webview-based)src/statusbar.ts— Status bar item showing branch name, sync state, and dirty indicators
Blame Annotations (src/blame.ts):
import * as hone from '@honeide/api';
const blameDecorationType = hone.window.createTextEditorDecorationType({
after: {
color: 'rgba(153, 153, 153, 0.6)',
margin: '0 0 0 3em',
fontStyle: 'italic',
},
});
let blameEnabled = false;
export function toggleBlame(context: hone.ExtensionContext): void {
blameEnabled = !blameEnabled;
if (blameEnabled) {
updateBlameAnnotations(hone.window.activeTextEditor);
context.subscriptions.push(
hone.window.onDidChangeActiveTextEditor(updateBlameAnnotations),
hone.window.onDidChangeTextEditorSelection(
(e) => updateBlameForLine(e.textEditor, e.selections[0]?.active.line)
)
);
} else {
hone.window.activeTextEditor?.setDecorations(blameDecorationType, []);
}
}
async function updateBlameAnnotations(editor: hone.TextEditor | undefined): Promise<void> {
if (!editor || !blameEnabled) return;
const blame = await hone.git.blame(editor.document.uri);
if (!blame) return;
const config = hone.workspace.getConfiguration('git.blame');
const format = config.get<string>('format', '${author}, ${date} - ${message}');
const decorations: hone.DecorationOptions[] = blame.lines.map((line, index) => ({
range: new hone.Range(index, Number.MAX_SAFE_INTEGER, index, Number.MAX_SAFE_INTEGER),
renderOptions: {
after: {
contentText: formatBlame(line, format),
},
},
}));
editor.setDecorations(blameDecorationType, decorations);
}
function formatBlame(
line: { author: string; date: string; message: string; hash: string },
format: string
): string {
return format
.replace('${author}', line.author)
.replace('${date}', line.date)
.replace('${message}', line.message.substring(0, 50))
.replace('${hash}', line.hash.substring(0, 7));
}
async function updateBlameForLine(editor: hone.TextEditor, line: number | undefined): Promise<void> {
// Show detailed blame for just the current line (lighter weight)
if (!editor || line === undefined || !blameEnabled) return;
// Implementation: query blame for single line, update decoration
}Status Bar Integration (src/statusbar.ts):
import * as hone from '@honeide/api';
export function createGitStatusBar(context: hone.ExtensionContext): void {
const branchItem = hone.window.createStatusBarItem(hone.StatusBarAlignment.Left, 1000);
branchItem.command = 'hone.git.switchBranch';
context.subscriptions.push(branchItem);
const syncItem = hone.window.createStatusBarItem(hone.StatusBarAlignment.Left, 999);
syncItem.command = 'hone.git.push';
context.subscriptions.push(syncItem);
async function update(): Promise<void> {
const repo = hone.git.getRepository(hone.workspace.workspaceFolders?.[0]?.uri);
if (!repo) {
branchItem.hide();
syncItem.hide();
return;
}
const head = await repo.getHEAD();
const status = await repo.getStatus();
// Branch name
branchItem.text = `$(git-branch) ${head.name ?? head.commit?.substring(0, 7) ?? 'unknown'}`;
branchItem.tooltip = `Branch: ${head.name}`;
// Dirty state indicator
const dirty = status.modified.length + status.added.length + status.deleted.length;
if (dirty > 0) {
branchItem.text += ` $(circle-filled)`;
branchItem.tooltip += ` (${dirty} change${dirty !== 1 ? 's' : ''})`;
}
branchItem.show();
// Sync state (ahead/behind)
const tracking = await repo.getTrackingBranch();
if (tracking) {
const { ahead, behind } = tracking;
if (ahead > 0 || behind > 0) {
const parts: string[] = [];
if (behind > 0) parts.push(`$(arrow-down) ${behind}`);
if (ahead > 0) parts.push(`$(arrow-up) ${ahead}`);
syncItem.text = parts.join(' ');
syncItem.tooltip = `${behind} behind, ${ahead} ahead of ${tracking.name}`;
syncItem.show();
} else {
syncItem.hide();
}
} else {
syncItem.hide();
}
}
// Update on various events
update();
context.subscriptions.push(
hone.workspace.onDidSaveTextDocument(() => update()),
hone.git.onDidChangeRepository(() => update()),
hone.window.onDidChangeActiveTextEditor(() => update())
);
// Periodic update for fetch/push changes from remote
const interval = setInterval(update, 30_000);
context.subscriptions.push({ dispose: () => clearInterval(interval) });
}Git Graph (src/graph.ts):
The Git graph is rendered in a webview panel. The extension queries the git log with graph information and renders a visual commit graph with:
- Commit nodes with branch coloring
- Branch/tag labels
- Merge lines
- Click-to-inspect commit details
- Context menu for cherry-pick, revert, reset, create branch
Activation triggers: onLanguage:dockerfile, workspaceContains:Dockerfile, workspaceContains:docker-compose.yml, workspaceContains:docker-compose.yaml
LSP Server: dockerfile-language-server-nodejs --stdio
Commands:
| Command | Title | Description |
|---|---|---|
hone.docker.buildImage |
Build Image | Build Docker image from current Dockerfile |
hone.docker.runContainer |
Run Container | Run a container from built image |
hone.docker.composeUp |
Compose Up | docker compose up |
hone.docker.composeDown |
Compose Down | docker compose down |
hone.docker.restartServer |
Restart Server | Restart Dockerfile language server |
Additional Features:
- docker-compose.yml schema validation via the YAML extension's schema infrastructure
- Dockerfile directive completions and hover documentation
- Image name completions from Docker Hub (when online)
Configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
docker.dockerPath |
string |
"docker" |
Path to Docker executable |
docker.composeCommand |
string |
"docker compose" |
Docker Compose command |
docker.defaultBuildArgs |
object |
{} |
Default build arguments |
Activation triggers: onLanguage:toml, onLanguage:yaml, workspaceContains:Cargo.toml, workspaceContains:*.yml, workspaceContains:*.yaml
LSP Servers:
- TOML:
taplo lsp stdio(Taplo language server) - YAML:
yaml-language-server --stdio
Schema Validation:
Both TOML and YAML support schema-based validation. Built-in schema associations:
| File Pattern | Schema | Format |
|---|---|---|
Cargo.toml |
Cargo manifest schema | TOML |
pyproject.toml |
PEP 621 schema | TOML |
docker-compose.yml / docker-compose.yaml |
Docker Compose schema | YAML |
.github/workflows/*.yml |
GitHub Actions workflow schema | YAML |
.gitlab-ci.yml |
GitLab CI schema | YAML |
mkdocs.yml |
MkDocs schema | YAML |
.pre-commit-config.yaml |
pre-commit schema | YAML |
netlify.toml |
Netlify configuration schema | TOML |
Commands:
| Command | Title | Description |
|---|---|---|
hone.toml.restartServer |
Restart TOML Server | Restart Taplo |
hone.yaml.restartServer |
Restart YAML Server | Restart yaml-language-server |
Configuration:
| Setting | Type | Default | Description |
|---|---|---|---|
toml.formatter.alignEntries |
boolean |
false |
Align consecutive entries |
toml.formatter.arrayTrailingComma |
boolean |
true |
Trailing comma in arrays |
toml.formatter.columnWidth |
number |
80 |
Maximum column width |
toml.schemas |
array |
[] |
Custom TOML schema associations |
yaml.schemas |
object |
{} |
Custom YAML schema associations |
yaml.format.enable |
boolean |
true |
Enable YAML formatting |
yaml.format.singleQuote |
boolean |
false |
Use single quotes |
yaml.validate |
boolean |
true |
Enable YAML validation |
yaml.hover |
boolean |
true |
Enable hover documentation |
yaml.completion |
boolean |
true |
Enable completions |
Each extension is compiled by Perry from TypeScript to a native dynamic library (.dylib on macOS, .so on Linux, .dll on Windows). The compilation command:
perry compile extensions/typescript/src/index.ts \
--output-type dylib \
--output extensions/typescript/dist/extension.dylib \
--target currentPerry resolves the @honeide/api import to its type definitions at compile time and generates native bindings that call into the extension host's API surface at runtime. No FFI layer is needed because Perry understands the @honeide/api interface natively.
#!/bin/bash
set -euo pipefail
EXTENSIONS_DIR="extensions"
OUTPUT_DIR="dist"
for ext_dir in "$EXTENSIONS_DIR"/*/; do
ext_name=$(basename "$ext_dir")
echo "Building extension: $ext_name"
perry compile "$ext_dir/src/index.ts" \
--output-type dylib \
--output "$OUTPUT_DIR/$ext_name/extension.dylib" \
--target current \
--optimize release
# Copy manifest and static assets
cp "$ext_dir/hone-extension.json" "$OUTPUT_DIR/$ext_name/"
[ -d "$ext_dir/snippets" ] && cp -r "$ext_dir/snippets" "$OUTPUT_DIR/$ext_name/"
[ -f "$ext_dir/language-configuration.json" ] && cp "$ext_dir/language-configuration.json" "$OUTPUT_DIR/$ext_name/"
[ -d "$ext_dir/schemas" ] && cp -r "$ext_dir/schemas" "$OUTPUT_DIR/$ext_name/"
[ -d "$ext_dir/preview" ] && cp -r "$ext_dir/preview" "$OUTPUT_DIR/$ext_name/"
echo " -> $OUTPUT_DIR/$ext_name/extension.dylib"
done
echo "All extensions built successfully."At IDE startup, hone-core's extension host:
- Scans the built-in extensions directory for
hone-extension.jsonmanifests - Parses each manifest and registers activation events
- When an activation event fires, loads the corresponding
.dylibviadlopen - Calls the exported
activatefunction, passing anExtensionContext - The extension's registrations (commands, LSP clients, event handlers) are live until deactivation
For distribution, all built-in extensions can be bundled directly into the Hone binary:
perry build hone-core/src/main.ts \
--bundle-extensions dist/ \
--output honeThe --bundle-extensions <dir> flag causes Perry to embed the compiled extension dylibs and their static assets (manifests, snippets, language configs) as resources inside the final executable. At runtime, the extension host extracts them to a temporary directory on first launch.
- Extensions import only
@honeide/api. Any other import is a compile error. - No Node.js APIs. No
fs,path,child_process. All system interaction goes through the@honeide/apisurface. - No dynamic
requireorimport(). All code must be statically analyzable by Perry. - No third-party npm packages. If utility code is needed, it must be written inline or placed in
shared/.
Every hone-extension.json is validated against the manifest JSON schema at test time. This ensures:
- All required fields are present (
id,name,version,publisher,main,engines,activationEvents) - All contributed language IDs are valid identifiers
- All command IDs follow the
hone.<extension>.<command>convention - All snippet and configuration file paths reference files that exist
- LSP server definitions have valid
command,args, andtransportfields - Configuration property types match their declared types
- No duplicate command or configuration keys across extensions
// tests/manifest-validation.test.ts
import { describe, it, expect } from 'vitest';
import { readFileSync, readdirSync, existsSync } from 'fs';
import { join } from 'path';
import Ajv from 'ajv';
const EXTENSIONS_DIR = join(__dirname, '..', 'extensions');
const MANIFEST_SCHEMA = JSON.parse(
readFileSync(join(__dirname, '..', 'schemas', 'hone-extension.schema.json'), 'utf-8')
);
const ajv = new Ajv({ allErrors: true });
const validate = ajv.compile(MANIFEST_SCHEMA);
describe('Extension manifests', () => {
const extensions = readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
for (const ext of extensions) {
describe(ext, () => {
const manifestPath = join(EXTENSIONS_DIR, ext, 'hone-extension.json');
it('has a valid hone-extension.json', () => {
expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
const valid = validate(manifest);
if (!valid) {
console.error(validate.errors);
}
expect(valid).toBe(true);
});
it('references existing files for snippets', () => {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
const snippets = manifest.contributes?.snippets ?? [];
for (const snippet of snippets) {
const snippetPath = join(EXTENSIONS_DIR, ext, snippet.path);
expect(existsSync(snippetPath)).toBe(true);
}
});
it('has a valid entry point', () => {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
const entryPath = join(EXTENSIONS_DIR, ext, manifest.main);
expect(existsSync(entryPath)).toBe(true);
});
});
}
});Each extension is activated with a mock @honeide/api to verify:
activate()runs without throwing- Expected commands are registered
- Expected LSP servers are started (mock verifies correct command/args)
- Status bar items are created where expected
deactivate()runs without throwing and cleans up resources
// tests/activation.test.ts
import { describe, it, expect, vi } from 'vitest';
import { createMockApi, MockExtensionContext } from './fixtures/mock-api';
describe('Extension activation', () => {
it('TypeScript extension activates correctly', async () => {
const { api, context } = createMockApi();
// Dynamic import with mocked @honeide/api
vi.doMock('@honeide/api', () => api);
const ext = await import('../extensions/typescript/src/index');
ext.activate(context);
expect(context.subscriptions.length).toBeGreaterThan(0);
expect(api.commands.registeredCommands).toContain('hone.typescript.organizeImports');
expect(api.commands.registeredCommands).toContain('hone.typescript.addMissingImports');
expect(api.languages.registeredServers).toHaveLength(1);
expect(api.languages.registeredServers[0].command).toBe('typescript-language-server');
ext.deactivate();
});
});Verify that each extension's LSP server configuration matches the expected server:
- Correct executable name
- Correct arguments (e.g.,
--stdio) - Correct transport mode
- Language IDs match the extension's declared languages
- Initialization options are well-formed
Every snippet JSON file is parsed and validated:
- Valid JSON syntax
- Each snippet has
prefix,body, anddescription bodyis a string or array of stringsprefixvalues are unique within each file- No empty snippets
- Tab stops (
$1,$2,$0) are used correctly (sequential,$0is final cursor)
// tests/snippets.test.ts
import { describe, it, expect } from 'vitest';
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { globSync } from 'glob';
const snippetFiles = globSync('extensions/*/snippets/*.json');
describe('Snippet files', () => {
for (const file of snippetFiles) {
describe(file, () => {
const content = JSON.parse(readFileSync(file, 'utf-8'));
it('is valid JSON with correct structure', () => {
for (const [name, snippet] of Object.entries(content)) {
const s = snippet as { prefix: string; body: string | string[]; description: string };
expect(s.prefix).toBeDefined();
expect(s.body).toBeDefined();
expect(s.description).toBeDefined();
expect(typeof s.prefix).toBe('string');
expect(
typeof s.body === 'string' || Array.isArray(s.body)
).toBe(true);
}
});
it('has unique prefixes', () => {
const prefixes = Object.values(content).map(
(s: any) => s.prefix
);
expect(new Set(prefixes).size).toBe(prefixes.length);
});
});
}
});Integration tests activate extensions with real (or stubbed) LSP servers:
- Start extension against a real
typescript-language-serverand verify completion response for a.tsfile - Start extension against
pyright-langserverand verify hover information - Verify that diagnostic events flow correctly from LSP to the editor
These tests require the LSP servers to be installed and run in CI with appropriate server binaries available.
Goal: Enable dogfooding Hone for its own TypeScript development.
| Extension | Deliverables |
|---|---|
| TypeScript | Full LSP integration, all commands, snippets, configuration |
| JSON | LSP integration, schema validation for package.json / tsconfig.json / hone-extension.json |
| Markdown | Preview rendering, formatting commands, snippets |
Exit criteria:
- Can open the
hone-extensionsrepo in Hone and get completions, diagnostics, hover, go-to-definition for TypeScript - Can edit
hone-extension.jsonmanifests with schema validation - Can preview this
PROJECT_PLAN.mdin the Markdown preview
Goal: Support the most popular programming languages.
| Extension | Deliverables |
|---|---|
| Python | Pyright LSP, interpreter selection, run file/selection, snippets |
| Rust | rust-analyzer LSP, cargo commands, macro expansion, snippets |
| Go | gopls LSP, test commands, mod tidy, snippets |
Exit criteria:
- Each language has working completions, diagnostics, hover, and go-to-definition
- Language-specific commands work (run, test, etc.)
- Configuration options are respected
Goal: Round out language support.
| Extension | Deliverables |
|---|---|
| HTML/CSS | LSP integration, Emmet, auto-closing tags, color decorators |
| C/C++ | clangd LSP, switch header/source, compile_commands.json detection |
| Docker | Dockerfile LSP, compose commands |
| TOML/YAML | Taplo + yaml-language-server, schema validation |
Exit criteria:
- All 11 language-related extensions functional
- Schema validation working for common config file formats
Goal: Full source control integration.
| Extension | Deliverables |
|---|---|
| Git | All SCM commands, blame annotations, status bar, file history |
| Git (continued) | Git graph visualization, stash support, merge conflict resolution |
Dependencies: Requires hone-core's Git client API to be stable.
Exit criteria:
- Can stage, commit, push, pull without leaving Hone
- Blame annotations work inline and in gutter
- Branch management (create, switch, delete) works
- Status bar shows branch and sync state
Goal: Production-quality built-in extensions.
| Task | Description |
|---|---|
| Configuration schemas | Complete JSON schemas for all extension settings, enable IntelliSense in settings editor |
| Snippet libraries | Expand snippet collections based on community feedback |
| Error handling | Graceful degradation when LSP servers are not installed |
| Performance | Profile and optimize extension activation time |
| Documentation | In-editor documentation for all commands and settings |
| Testing | Full integration test suite, CI pipeline for all extensions |
| Accessibility | Keyboard navigation, screen reader support for all contributed UI |
Exit criteria:
- All extensions handle missing LSP servers gracefully (warning message, degraded mode)
- Extension activation adds less than 50ms to editor startup
- All tests pass in CI
- All configuration properties have descriptions and validation
Question: How does an extension reliably find an LSP server executable on the user's system?
Proposed Strategy:
- Check
PATH(most common case for properly installed tools) - Check common installation locations:
~/.local/bin/(pip install --user)~/.cargo/bin/(cargo install)~/go/bin/(go install)/usr/local/bin/~/.nvm/versions/node/*/bin/(Node.js tools via nvm)~/.volta/bin/(Volta-managed tools)
- Check extension-specific configuration (e.g.,
python.pythonPath,cpp.clangdPath) - If not found, show a warning with installation instructions
Risk: Different platforms and package managers install binaries in different locations. We may need platform-specific discovery logic.
Question: How large will each compiled extension be?
Target: Each extension should compile to under 1 MB as a dynamic library. Most extensions are thin wrappers around LSP configuration and command registration, so they should be well under this limit.
Risk: If Perry's compiled output is larger than expected, bundling 12 extensions could add significant size to the Hone binary. Mitigation: profile Perry output size early in Phase 1 and optimize if needed.
Question: Should we build a custom Emmet engine or use an existing library?
Options:
- Port a minimal Emmet engine to Perry-compatible TypeScript. This would be self-contained but requires significant effort. Emmet's abbreviation grammar is nontrivial.
- Compile the existing
emmetnpm package with Perry. This depends on whether Perry can handle the package's dynamic patterns. Theemmetpackage is pure JavaScript with no native dependencies. - Implement a subset of Emmet covering the 20 most common abbreviations. This is pragmatic for Phase 3 with the option to expand later.
Recommendation: Option 3 for Phase 3, with a plan to expand to option 2 if Perry compatibility allows.
Question: Should Hone use VSCode-compatible snippet syntax?
Answer: Yes. VSCode snippet syntax ($1, ${2:placeholder}, ${3|choice1,choice2|}, $0 for final cursor) is the de facto standard. Using it enables:
- Migration from VSCode (users can bring their snippets)
- Reuse of community snippet collections
- Familiar syntax for extension authors
The snippet engine in hone-core should implement the full VSCode snippet grammar including:
- Tab stops and placeholders
- Choice elements
- Variables (
$TM_FILENAME,$CLIPBOARD,$CURRENT_YEAR, etc.) - Nested placeholders
- Regex transforms (
${1/pattern/replacement/flags})
Question: How do built-in extensions get updated?
Answer: Built-in extensions ship with the IDE and are updated as part of IDE releases. There is no separate update channel for built-in extensions.
Implications:
- Bug fixes in built-in extensions require a new IDE release
- The extension manifest
versionfield tracks the extension version independently of the IDE version, but updates are tied to IDE releases - In the future, a marketplace/registry could allow third-party extensions with independent update cycles, but this is out of scope for the initial built-in extensions
Question: How should extensions handle LSP server crashes?
Strategy:
- The
registerLanguageServerAPI includes amaxRestartsoption (default: 5) - After a crash, the extension host automatically restarts the server with exponential backoff (1s, 2s, 4s, 8s, 16s)
- After
maxRestartsconsecutive crashes, show an error message and stop restarting - A manual "Restart Language Server" command is provided by each LSP extension
- Crash telemetry (if the user opts in) helps identify unstable server versions
Question: How do extensions handle workspaces with multiple root folders?
Strategy:
- LSP servers that support
workspaceFolders(most modern servers do) receive all workspace roots - Extensions that run commands (e.g.,
cargo test,go test) operate in the context of the active file's workspace root - The Git extension shows status for all repositories found in workspace roots
- Configuration can be overridden per workspace folder
Risk: The @honeide/api surface is still evolving. Changes to the API require updates to all built-in extensions.
Mitigation:
- Built-in extensions are co-located in the same repository and can be updated atomically
- The API surface should be designed with stability in mind; breaking changes require a version bump in
engines.hone - A compatibility layer can bridge minor API differences across versions
Question: Where do syntax highlighting grammars come from?
Options:
- Write custom TextMate grammars. Very high effort; TextMate grammars are complex.
- Use existing open-source TextMate grammars (e.g., from the VS Code repository, which are MIT-licensed). This is the pragmatic choice.
- Use Tree-sitter grammars instead. Tree-sitter provides more accurate parsing but requires a different integration path in
hone-core.
Recommendation: Start with existing TextMate grammars (option 2) for Phase 1-3. Investigate Tree-sitter integration as a future enhancement for improved accuracy, especially for languages with complex syntax (C++, Rust).
Risk: Configuration changes in Hone settings must be propagated to running LSP servers. If the propagation is incorrect or delayed, the user sees stale behavior.
Mitigation:
- Every extension listens to
onDidChangeConfigurationevents - On relevant changes, the extension sends
workspace/didChangeConfigurationto the LSP server - For settings that require a server restart (rare), the extension prompts the user to restart
{ // ── Identity ────────────────────────────────────────────── "id": "string", // Unique extension ID (e.g., "hone.typescript") "name": "string", // Human-readable name "version": "string", // Semver (e.g., "1.0.0") "publisher": "string", // Publisher identifier (e.g., "hone") "description": "string", // One-line description "license": "string", // SPDX license identifier // ── Compatibility ───────────────────────────────────────── "engines": { "hone": "string" // Semver range (e.g., ">=0.1.0") }, // ── Entry Point ─────────────────────────────────────────── "main": "string", // Path to compiled entry (e.g., "src/index.ts") // ── Activation ──────────────────────────────────────────── "activationEvents": [ // When to activate this extension. Possible values: // "onLanguage:<languageId>" — when a file of this language is opened // "onCommand:<commandId>" — when this command is invoked // "workspaceContains:<glob>" — when workspace matches glob // "*" — activate on startup (use sparingly) ], // ── Contributions ───────────────────────────────────────── "contributes": { // Languages this extension provides support for "languages": [ { "id": "string", // Language identifier (e.g., "typescript") "extensions": [".ts", ".tsx"], // File extensions "aliases": ["TypeScript", "ts"], // Display names "filenames": [], // Exact filename matches (e.g., ["Dockerfile"]) "filenamePatterns": [], // Glob patterns for filenames "configuration": "string", // Path to language-configuration.json "firstLine": "string" // Regex to match first line of file } ], // LSP server definitions "lspServers": [ { "id": "string", // Server identifier "name": "string", // Human-readable server name "languageIds": ["string"], // Languages this server handles "command": "string", // Executable name or path "args": ["string"], // Command-line arguments "transport": "stdio | tcp | pipe", // Communication transport (default: "stdio") "initializationOptions": {}, // LSP InitializeParams.initializationOptions "settings": {} // Default server settings (workspace/configuration) } ], // Commands contributed to the command palette "commands": [ { "command": "string", // Command identifier (e.g., "hone.typescript.organizeImports") "title": "string", // Display title "category": "string", // Grouping category (e.g., "TypeScript") "icon": "string" // Optional icon identifier } ], // Snippet file references "snippets": [ { "language": "string", // Language ID these snippets apply to "path": "string" // Relative path to snippet JSON file } ], // User-facing configuration (settings) "configuration": { "title": "string", // Settings group title "properties": { "settingKey": { "type": "string | number | boolean | array | object", "default": "any", "description": "string", "enum": [], // Optional: allowed values "enumDescriptions": [] // Optional: descriptions for enum values } } }, // Keybinding defaults "keybindings": [ { "command": "string", // Command to invoke "key": "string", // Key combination (e.g., "ctrl+shift+o") "mac": "string", // macOS override (e.g., "cmd+shift+o") "when": "string" // Context condition (e.g., "editorTextFocus && editorLangId == typescript") } ], // Grammars (TextMate-style syntax highlighting) "grammars": [ { "language": "string", // Language ID "scopeName": "string", // TextMate scope (e.g., "source.ts") "path": "string" // Path to tmLanguage.json } ] } }