Skip to content

Latest commit

 

History

History
2436 lines (2075 loc) · 84.6 KB

File metadata and controls

2436 lines (2075 loc) · 84.6 KB

Hone Extensions — Project Plan

Comprehensive implementation guide for the hone-extensions project: built-in extensions that ship with the Hone IDE.


1. Overview

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.


2. Dependencies

Internal Dependencies

  • @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.

External Dependencies

  • 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.

Dev Dependencies (build-time only)

  • perry — the TypeScript-to-native compiler
  • @honeide/api — type definitions for compilation
  • A test runner (e.g., vitest or a Perry-native test harness)

Inter-Extension Independence

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).


3. Repository Structure

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

4. Core Interfaces & Types

4.1 Extension Manifest Schema (hone-extension.json)

Every extension must include a hone-extension.json file at its root. This manifest follows a strict schema:

{
  // ── 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
      }
    ]
  }
}

4.2 Example Manifests

TypeScript Extension Manifest

{
  "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/"
      }
    ]
  }
}

Python Extension Manifest

{
  "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"
      }
    ]
  }
}

Git Extension Manifest

{
  "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"
      }
    ]
  }
}

4.3 Standard Extension Entry Point Pattern

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
}

4.4 Shared LSP Helpers (shared/lsp-helpers.ts)

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);
}

5. Implementation Guide

5.1 TypeScript Extension

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

5.2 Python Extension

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"
  }
}

5.3 Rust Extension

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):

  1. Register rust-analyzer as LSP server with configured features and check-on-save behavior.
  2. Register commands for cargo run, cargo check, cargo test that open a terminal and execute the command.
  3. Register expandMacro and viewHir as custom LSP requests (rust-analyzer/expandMacro, rust-analyzer/viewHir) and show results in a new editor tab.
  4. Register openDocs command that queries rust-analyzer for the external documentation URL and opens it in the system browser.
  5. Show a status bar item indicating the active Rust toolchain (e.g., "stable-x86_64-apple-darwin").

5.4 Go Extension

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):

  1. Register gopls as LSP server. Forward relevant settings (build flags, formatting preferences) as initialization options.
  2. Register test commands that detect the test function at the cursor position (by parsing the document) and run go test -run <TestName>.
  3. Register tidy command that runs go mod tidy in a terminal.
  4. Register toggleTestFile command that switches between implementation and test file.
  5. Show a status bar item with the Go version.

5.5 C/C++ Extension

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):

  1. Register clangd with configuration for compile commands path and fallback flags (passed as --compile-commands-dir and --query-driver args).
  2. Implement switchHeaderSource using clangd's custom textDocument/switchSourceHeader LSP request.
  3. Detect compile_commands.json in workspace and pass its directory to clangd.
  4. Show inlay hints for deduced types (forwarded from clangd's built-in inlay hint support).

5.6 HTML/CSS Extension

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 expandAbbreviation command
  • 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

5.7 JSON Extension

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

5.8 Markdown Extension

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 ![alt](url) 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": ["![${1:alt}](${2:url})$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)

5.9 Git Extension

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 wiring
  • src/blame.ts — Inline blame annotations and gutter blame decorations
  • src/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

5.10 Docker Extension

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

5.11 TOML/YAML Extension

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

6. Perry Integration

6.1 Compilation Model

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 current

Perry 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.

6.2 Build Script (scripts/build-all.sh)

#!/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."

6.3 Extension Host Loading

At IDE startup, hone-core's extension host:

  1. Scans the built-in extensions directory for hone-extension.json manifests
  2. Parses each manifest and registers activation events
  3. When an activation event fires, loads the corresponding .dylib via dlopen
  4. Calls the exported activate function, passing an ExtensionContext
  5. The extension's registrations (commands, LSP clients, event handlers) are live until deactivation

6.4 Bundling into IDE Binary

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 hone

The --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.

6.5 Constraints

  • 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/api surface.
  • No dynamic require or import(). 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/.

7. Test Strategy

7.1 Manifest Validation Tests

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, and transport fields
  • 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);
      });
    });
  }
});

7.2 Activation Tests

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();
  });
});

7.3 LSP Configuration Tests

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

7.4 Snippet Validation Tests

Every snippet JSON file is parsed and validated:

  • Valid JSON syntax
  • Each snippet has prefix, body, and description
  • body is a string or array of strings
  • prefix values are unique within each file
  • No empty snippets
  • Tab stops ($1, $2, $0) are used correctly (sequential, $0 is 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);
      });
    });
  }
});

7.5 Integration Tests

Integration tests activate extensions with real (or stubbed) LSP servers:

  • Start extension against a real typescript-language-server and verify completion response for a .ts file
  • Start extension against pyright-langserver and 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.


8. Phased Milestones

Phase 1: Core Editor Experience (Weeks 1-3)

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-extensions repo in Hone and get completions, diagnostics, hover, go-to-definition for TypeScript
  • Can edit hone-extension.json manifests with schema validation
  • Can preview this PROJECT_PLAN.md in the Markdown preview

Phase 2: Popular Languages (Weeks 4-6)

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

Phase 3: Additional Languages (Weeks 7-9)

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

Phase 4: Git Extension (Weeks 10-12)

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

Phase 5: Polish and Completeness (Weeks 13-15)

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

9. Open Questions / Risks

9.1 LSP Server Binary Discovery

Question: How does an extension reliably find an LSP server executable on the user's system?

Proposed Strategy:

  1. Check PATH (most common case for properly installed tools)
  2. 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)
  3. Check extension-specific configuration (e.g., python.pythonPath, cpp.clangdPath)
  4. 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.

9.2 Extension Compiled Size

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.

9.3 Emmet Integration

Question: Should we build a custom Emmet engine or use an existing library?

Options:

  1. Port a minimal Emmet engine to Perry-compatible TypeScript. This would be self-contained but requires significant effort. Emmet's abbreviation grammar is nontrivial.
  2. Compile the existing emmet npm package with Perry. This depends on whether Perry can handle the package's dynamic patterns. The emmet package is pure JavaScript with no native dependencies.
  3. 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.

9.4 Snippet Format Compatibility

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})

9.5 Extension Auto-Update Mechanism

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 version field 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

9.6 LSP Server Crash Recovery

Question: How should extensions handle LSP server crashes?

Strategy:

  • The registerLanguageServer API includes a maxRestarts option (default: 5)
  • After a crash, the extension host automatically restarts the server with exponential backoff (1s, 2s, 4s, 8s, 16s)
  • After maxRestarts consecutive 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

9.7 Multi-Root Workspace Support

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

9.8 Extension API Stability

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

9.9 TextMate Grammar Sourcing

Question: Where do syntax highlighting grammars come from?

Options:

  1. Write custom TextMate grammars. Very high effort; TextMate grammars are complex.
  2. Use existing open-source TextMate grammars (e.g., from the VS Code repository, which are MIT-licensed). This is the pragmatic choice.
  3. 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).

9.10 Configuration Sync Between Extension and LSP Server

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 onDidChangeConfiguration events
  • On relevant changes, the extension sends workspace/didChangeConfiguration to the LSP server
  • For settings that require a server restart (rare), the extension prompts the user to restart