This guide covers developing the MLisp VSCode extension, which is uniquely implemented in OCaml and compiled to JavaScript via js_of_ocaml.
- OCaml 5.0+
- opam (OCaml package manager)
- Node.js 18+
- VSCode 1.80.0+
cd packages/vscode-ext
# Install OCaml dependencies
opam install . --deps-only
# Install npm dependencies
npm install
# Build the extension
npm run buildThe extension follows a unique build pipeline:
OCaml Source (.ml files)
↓
Dune Build (ocamlopt/jsoo)
↓
JavaScript Bytecode (.bc.js)
↓
esbuild Bundle
↓
dist/vscode_mlisp.bc.js
↓
VSCode Extension Host
# Full build (OCaml + bundle)
npm run build
# OCaml compilation only
npm run build:ocaml
# esbuild bundling only
npm run bundle
# Clean build artifacts
dune clean
rm -rf dist/For development with automatic rebuilding:
# Watch both OCaml and bundling
npm run dev
# Individual watch commands
npm run watch:ocaml # Dune watch mode
npm run watch:bundle # esbuild watch modepackages/vscode-ext/
├── src/ # Extension source code
│ ├── vscode_mlisp.ml # Main entry point (activate/deactivate)
│ ├── vscode_mlisp.mli # Type signatures
│ └── dune # Build configuration
├── src-bindings/ # VSCode API bindings (gen_js_api)
│ └── vscode/
│ ├── vscode.ml # API implementations
│ ├── vscode.mli # API signatures
│ ├── vscode_stub.js # JavaScript stub for gen_js_api
│ └── dune # Build configuration
├── syntaxes/ # TextMate grammar
│ └── mlisp.tmLanguage.json # Syntax highlighting rules
├── dist/ # Bundled output (generated)
│ └── vscode_mlisp.bc.js # Final extension bundle
├── package.json # VSCode extension manifest
├── language-configuration.json # Language behavior settings
├── dune-project # OCaml project metadata
├── DEVELOPMENT.md # This file
├── README.md # User-facing documentation
├── CHANGELOG.md # Version history
└── LICENSE # MPL-2.0
The main extension file exports activate and deactivate functions:
(* Activation function called by VSCode *)
let activate (context : Vscode_bindings.ExtensionContext.t) =
(* Register commands *)
(* Set up disposables *)
Js.undefined
(* Export for VSCode *)
let () =
Js.export "activate" (Js.wrap_callback activate)
let () =
Js.export "deactivate" (Js.wrap_callback (fun () -> Js.undefined))Type-safe OCaml bindings for the VSCode Extension API using gen_js_api:
(* Example: Creating an output channel *)
let channel = Window.createOutputChannel ~name:"MLisp REPL"
(* Example: Registering a command *)
let disposable = Commands.registerCommand
~command:"mlisp.myCommand"
~callback:(fun args -> (* handle command *)) ()- Update
package.json:
"contributes": {
"commands": [
{
"command": "mlisp.myNewCommand",
"title": "MLisp: My New Command"
}
]
}- Implement in
vscode_mlisp.ml:
let my_new_command () =
(* Your implementation here *)
Js.undefined
(* Register in activate function *)
let cmd = Commands.registerCommand
~command:"mlisp.myNewCommand"
~callback:(fun _args -> my_new_command ())
()- Test in Extension Development Host:
code --extensionDevelopmentPath=$PWD- Edit
syntaxes/mlisp.tmLanguage.json - Add patterns to the
patternsarray - Reload VSCode window (Ctrl+R)
- Update
package.json:
"contributes": {
"keybindings": [
{
"command": "mlisp.myNewCommand",
"key": "ctrl+shift+m",
"when": "editorLangId == mlisp"
}
]
}-
Launch Extension Development Host:
code --extensionDevelopmentPath=$PWD -
Open a
.mlisptest file and verify:- Syntax highlighting works
- Commands appear in Command Palette (Ctrl+Shift+P)
- Keybindings function correctly
- Output channels display properly
Create test files with various MLisp constructs:
;; test_syntax.mlisp
;; Comments
(define x 42)
;; Keywords
(if #t 1 2)
(cond ((> x 0) "positive"))
(lambda (n) (* n n))
;; Strings and booleans
"hello world"
#t
#f
;; Quasiquoting
`(1 ,x 3)
`(1 ,@'(2 3) 4)"Unbound module Vscode_bindings"
Solution: Make sure dependencies are installed:
opam install . --deps-onlyesbuild fails with "Cannot find module"
Solution: Build OCaml first:
dune build
npm run bundle"Cannot read property of undefined"
- Check JavaScript console in VSCode (Help > Toggle Developer Tools)
- Verify
gen_js_apibindings match VSCode API
Extension doesn't activate
- Check
activationEventsinpackage.json - Verify
mainfield points to correct bundled file - Check VSCode error logs
- Open VSCode
- Help > Toggle Developer Tools
- Check Console tab for errors
Since OCaml is compiled to JavaScript, debugging requires:
-
Add
Js.logstatements for logging:Js.log "Debug point reached"
-
Check browser console output
# View generated JavaScript (before bundling)
cat _build/default/src/vscode_mlisp.bc.js | head -50
# Verify exports
grep -o "exports.activate" dist/vscode_mlisp.bc.js# Run Biome linter
npm run check
# Auto-fix issues
npm run fixOCaml provides strong type safety at compile time:
# Type check without compiling
ocamlc -i src/vscode_mlisp.mlnpm run packageThis creates mlisp-vscode.vsix ready for:
- Local installation:
code --install-extension mlisp-vscode.vsix - Publishing to VSCode Marketplace
- Create a publisher account at marketplace.visualstudio.com
- Create a Personal Access Token
- Login to vsce:
npx vsce login <publisher-name>
- Publish:
npm run publish
- Fork the repository
- Create a feature branch
- Make your changes
- Test thoroughly:
- Run
npm run build - Test in Extension Development Host
- Run
npm run check
- Run
- Submit a pull request
- Follow OCaml naming conventions
- Use descriptive variable names
- Add comments for non-obvious code
- Keep functions small and focused
For user-facing documentation, see README.md.