23 stable releases
Uses new Rust 2024
| new 1.9.2 | Feb 12, 2026 |
|---|---|
| 1.8.1 | Jan 25, 2026 |
| 1.3.7 | Jan 26, 2025 |
| 1.3.5 | Aug 5, 2024 |
| 0.1.0 |
|
#7 in Text editors
715KB
16K
SLoC
Beancount Language Server
A Language Server Protocol (LSP) implementation for Beancount, the double-entry bookkeeping language. This provides rich editing features like completions, diagnostics, formatting, and more for Beancount files in your favorite editor.
โจ Features
๐ Currently Implemented
| LSP Feature | Description | Status |
|---|---|---|
| Completions | Smart autocompletion for accounts, payees, dates, narration, tags, links, and transaction types | โ |
| Diagnostics | Real-time error checking and validation via beancount Python integration | โ |
| Formatting | Document formatting compatible with bean-format, with support for prefix-width, num-width, and currency-column options |
โ |
| Rename | Rename symbols across files | โ |
| References | Find all references to accounts, payees, etc. | โ |
| Semantic Highlighting | Advanced syntax highlighting with semantic information | โ |
| Inlay Hints | Show calculated balancing amounts and unbalanced transaction warnings | โ |
๐ Completion Types
- Accounts: Autocomplete account names with hierarchy support (
Assets:Checking) - Payees: Previously used payee names
- Dates: Smart date completion (today, this month, previous month, next month)
- Narration: Previously used transaction descriptions
- Tags: Complete hashtags (
#vacation) - Links: Complete links (
^receipt-123) - Transaction Types:
txn,balance,open,close, etc.
๐ก Inlay Hints
Non-intrusive inline annotations that help visualize implicit information:
- Calculated Balancing Amounts: When a posting omits an amount, shows the implicit balancing amount at the end of that posting line, aligned with other amounts
- Unbalanced Transaction Warnings: When all postings have explicit amounts but don't balance to zero, shows a warning with the unbalanced total on the transaction line
Examples:
2024-01-15 * "Grocery Store"
Expenses:Food:Groceries 45.23 USD
Assets:Bank:Checking -45.23 USD ; โ Shown as inlay hint
2024-01-15 * "Unbalanced Transfer" /* = 500.00 USD โ */ ; โ Warning shown
Assets:Savings 1000.00 USD
Assets:Checking -500.00 USD
๐ฎ Planned Features
| LSP Feature | Description | Priority |
|---|---|---|
| Hover | Show account balances, transaction details, account metadata | High |
| Go to Definition | Jump to account/payee/commodity definitions | High |
| Document Symbols | Outline view showing accounts, transactions, and structure | High |
| Folding Ranges | Fold transactions, account hierarchies, and multi-line entries | Medium |
| Code Actions | Quick fixes, refactoring, auto-balance transactions | Medium |
| Signature Help | Help with transaction syntax and directive parameters | Low |
| Workspace Symbols | Find accounts, payees, commodities across all files | Low |
๐ฆ Installation
Method 1: Cargo (Recommended)
cargo install beancount-language-server
Method 2: GitHub Releases (Pre-built Binaries)
Download the latest release for your platform from the releases page.
Supported Platforms:
- Linux (x86_64, aarch64, loongarch64)
- macOS (x86_64, aarch64)
- Windows (x86_64)
Method 3: Homebrew (macOS/Linux)
brew install beancount-language-server
Method 4: Nix
# Using nix-env
nix-env -iA nixpkgs.beancount-language-server
# Using nix shell
nix shell nixpkgs#beancount-language-server
# Development environment
nix develop
Method 5: Build from Source
git clone https://github.com/polarmutex/beancount-language-server.git
cd beancount-language-server
# Standard build (includes PyO3 embedded Python support by default)
cargo build --release
# Build without PyO3 (minimal binary, requires external bean-check/python)
cargo build --release --no-default-features
The binary will be available at target/release/beancount-language-server.
๐ง Requirements
For Diagnostics (Bean-check)
The language server requires one of the following for validation and diagnostics:
Option 1: PyO3 Embedded (Default - Recommended)
- Python 3.8+ installed on your system
- beancount Python package
pip install beancount - Pre-built binaries from GitHub releases include PyO3 support by default
- Performance: 60-66x faster than subprocess-based methods (~838ฮผs vs ~50ms per check)
- Note: If beancount is not available, automatically falls back to other methods
Python Environment Compatibility
The PyO3 embedded checker (recommended) works seamlessly with:
- โ System Python 3.8+ (any minor version)
- โ Virtual environments (venv, virtualenv)
- โ Conda environments
- โ pyenv-managed Python
- โ Multiple Python versions (uses abi3 stable ABI - one binary works with all Python 3.8+)
- โ Project-local .venv (automatically detected)
No special configuration needed - the binary automatically detects and uses available Python installations.
Python Discovery Order
The language server searches for Python in this order:
BEANCOUNT_LSP_PYTHONenvironment variable (if set)- User-configured
python_cmdin LSP settings - Project-local
.venv/bin/python(or.venv/Scripts/python.exeon Windows) - System
python3in PATH - System
pythonin PATH
Advanced: Explicit Python Override
For edge cases or custom Python installations, set the environment variable:
# Unix/Linux/macOS
export BEANCOUNT_LSP_PYTHON=/path/to/python
beancount-language-server
# Windows (PowerShell)
$env:BEANCOUNT_LSP_PYTHON = "C:\path\to\python.exe"
beancount-language-server
# Windows (CMD)
set BEANCOUNT_LSP_PYTHON=C:\path\to\python.exe
beancount-language-server
Or configure via LSP settings:
{
"bean_check": {
"python_cmd": "/path/to/python"
}
}
Option 2: System Python (Fallback)
- Python with beancount library
- Used automatically if PyO3 checker is unavailable
- Invokes Python via subprocess for validation
Option 3: Bean-check Binary (Fallback)
- Traditional
bean-checkcommand-line tool - Install via:
pip install beancount(includes bean-check) - Used if Python methods are unavailable
Performance Comparison
Based on comprehensive benchmarks with a 30-line beancount file:
| Method | Average Time | Relative Speed | Availability |
|---|---|---|---|
| PyO3 Embedded (default) | ~838ฮผs | 1x (baseline) | Requires Python 3.8+ + beancount |
| System Python | ~50.1ms | 60x slower | Requires Python + beancount |
| Bean-check Binary | ~55.2ms | 66x slower | Requires bean-check binary |
Recommendation: Use PyO3 embedded checker (default in pre-built binaries) for optimal performance.
โ๏ธ Configuration
The language server accepts configuration via LSP initialization options:
{
// Optional: Only needed for multi-file projects with include directives
"journal_file": "/path/to/main.beancount",
"formatting": {
"prefix_width": 30,
"num_width": 10,
"currency_column": 60,
"account_amount_spacing": 2,
"number_currency_spacing": 1
},
"diagnostic_flags": ["!"] // Optional: flags that generate warnings (default: ["!"])
}
Note: All configuration is optional. The language server will auto-detect the best checker method (PyO3 โ System Python โ Bean-check).
Configuration Options
| Option | Type | Description | Default |
|---|---|---|---|
journal_file |
string | Path to the main beancount journal file. Optional: Only required if your beancount files use include directives to span multiple files. Single-file projects work without this setting. |
None |
Workspace-Specific Configuration
The journal_file setting is workspace-specific. Each editor workspace (project folder) can have its own journal file configured. This means:
- Completions are scoped: Account names, payees, currencies, tags, and links are loaded only from the configured journal and its included files
- Separate ledgers: If you work on multiple beancount projects (personal finances, business, etc.), each workspace uses its own configuration
- No cross-contamination: Accounts from one ledger won't appear as completions in another
Example workflow with multiple ledgers:
~/finances/personal/ # Workspace 1: journal_file = "main.beancount"
โโโ main.beancount # Includes accounts/*.beancount
โโโ accounts/
โโโ assets.beancount
~/finances/business/ # Workspace 2: journal_file = "ledger.beancount"
โโโ ledger.beancount # Includes 2024/*.beancount
โโโ 2024/
โโโ transactions.beancount
When editing files in ~/finances/personal/, completions only show accounts like Assets:Personal:Checking. When editing in ~/finances/business/, completions show Assets:Business:Operating.
Bean-check Configuration
| Option | Type | Description | Default |
|---|---|---|---|
bean_check.method |
string | Validation method: "system", "python-system", or "python-embedded" | None |
bean_check.bean_check_cmd |
string | Path to bean-check binary (for "system" method) | None |
bean_check.python_cmd |
string | Path to Python executable (for Python methods) | None |
Preferred checker order (when bean_check.method is not set):
python-embedded(if built with the feature and available)python-system(if a compatible Python with beancount is available)system(if bean-check is available)
Configuration Examples
Default (no configuration needed):
The language server automatically selects the best available checker method:
- PyO3 Embedded (if Python 3.8+ with beancount is available)
- System Python (if Python with beancount is available)
- System Call (if bean-check binary is available)
No configuration required! Just install Python and beancount.
Override to force a specific method:
Only configure bean_check.method if you need to override auto-detection:
{
"bean_check": {
"method": "system", // Force bean-check binary
"bean_check_cmd": "/usr/local/bin/bean-check"
}
}
{
"bean_check": {
"method": "python-system", // Force Python subprocess
"python_cmd": "/usr/bin/python3"
}
}
{
"bean_check": {
"method": "python-embedded" // Force PyO3 (already default)
}
}
Troubleshooting PyO3 Checker
If the PyO3 embedded checker is not working:
-
Verify Python installation:
python3 --version # Should be 3.8 or higher -
Verify beancount installation:
python3 -c "import beancount.loader; print('Beancount OK')" -
Check language server logs for PyO3-related messages:
- VSCode: View โ Output โ Select "Beancount Language Server"
- Neovim:
:LspLog - Look for messages like "PyO3EmbeddedChecker: failed to import beancount.loader"
-
Install beancount if missing:
# System-wide pip3 install beancount # User installation (no sudo required) pip3 install --user beancount # Virtual environment (recommended) python3 -m venv ~/.beancount-env source ~/.beancount-env/bin/activate pip install beancount -
Override Python detection (if you have multiple Python installations):
# Set environment variable to use specific Python export BEANCOUNT_LSP_PYTHON=/path/to/python3 # Or configure in LSP settings { "bean_check": { "python_cmd": "/path/to/python3" } } -
Fallback methods: If PyO3 checker fails, the language server automatically tries:
- System Python method (python -c with beancount)
- System Call method (bean-check binary)
Check your configuration if you need to explicitly set a method.
Diagnostic Flags Configuration
By default, the language server generates warnings for all flagged transactions (entries with flags like !, P, etc.). You can configure which flags should generate diagnostics to reduce noise from intentional flags.
| Option | Type | Description | Default |
|---|---|---|---|
diagnostic_flags |
string[] | List of transaction flags that should generate warning diagnostics | ["!"] |
Default behavior (only ! flag generates warnings):
{
"diagnostic_flags": ["!"]
}
This means padding transactions with P flag, or other custom flags, won't generate warnings.
Include multiple flags:
{
"diagnostic_flags": ["!", "P"]
}
This generates warnings for both ! (needs attention) and P (padding) flags.
Disable all flag diagnostics:
{
"diagnostic_flags": []
}
This completely disables warnings for flagged transactions.
Use case: Some users intentionally use flags like P for padding transactions that will persist forever in the ledger. With the default configuration, these won't generate noise in your diagnostics panel, while ! flags (which typically indicate transactions needing review) will still show warnings.
Formatting Options
| Option | Type | Description | Default | Bean-format Equivalent |
|---|---|---|---|---|
prefix_width |
number | Fixed width for account names (overrides auto-detection) | Auto-calculated | --prefix-width (-w) |
num_width |
number | Fixed width for number alignment (overrides auto-detection) | Auto-calculated | --num-width (-W) |
currency_column |
number | Align currencies at this specific column | None (right-align) | --currency-column (-c) |
account_amount_spacing |
number | Minimum spaces between account names and amounts | 2 | N/A |
number_currency_spacing |
number | Number of spaces between number and currency | 1 | N/A |
Formatting Modes
Default Mode (no currency_column specified):
- Accounts are left-aligned
- Numbers are right-aligned with consistent end positions
- Behaves like
bean-formatwith no special options
Currency Column Mode (currency_column specified):
- Currencies are aligned at the specified column
- Numbers are positioned to place currencies at the target column
- Equivalent to
bean-format --currency-column N
Examples
Basic formatting with auto-detection:
{
"formatting": {}
}
Fixed prefix width (like bean-format -w 25):
{
"formatting": {
"prefix_width": 25
}
}
Currency column alignment (like bean-format -c 60):
{
"formatting": {
"currency_column": 60
}
}
Number-currency spacing control:
{
"formatting": {
"number_currency_spacing": 2
}
}
This controls the whitespace between numbers and currency codes:
0: No space (100.00USD)1: Single space (100.00 USD) - default2: Two spaces (100.00 USD)
Combined options:
{
"formatting": {
"prefix_width": 30,
"currency_column": 65,
"account_amount_spacing": 3,
"number_currency_spacing": 1
}
}
๐ฅ๏ธ Editor Setup
Visual Studio Code
- Install the Beancount extension from the marketplace
- Configure in
settings.json(optional):{ // Optional: Only needed for multi-file projects with include directives "beancountLangServer.journalFile": "/path/to/main.beancount", "beancountLangServer.formatting": { "prefix_width": 30, "currency_column": 60, "number_currency_spacing": 1 }, // Optional: flags that generate warnings (default: ["!"]) "beancountLangServer.diagnosticFlags": ["!"] }
Workspace-specific configuration: Create a .vscode/settings.json in each project folder:
{
"beancountLangServer.journalFile": "${workspaceFolder}/main.beancount"
}
This ensures each workspace uses its own journal file for completions and diagnostics.
Neovim
Using nvim.lsp (nvim > 0.11)
lsp/beancount.lua
return {
commands = { "beancount-language-server", "--stdio" },
root_markers = { "main.bean", ".git" },
-- init_options are optional
init_options = {
-- Optional: Only needed for multi-file projects with include directives
journal_file = "main.bean",
-- Optional: flags that generate warnings (default: ["!"])
diagnostic_flags = { "!" },
},
settings = {
beancount = {
formatting = {
prefix_width = 30,
currency_column = 60,
number_currency_spacing = 1,
}
}
}
}
Using nvim-lspconfig:
local lspconfig = require('lspconfig')
lspconfig.beancount.setup({
-- All init_options are optional
init_options = {
-- Optional: Only needed for multi-file projects with include directives
-- journal_file = "/path/to/main.beancount",
formatting = {
prefix_width = 30,
currency_column = 60,
number_currency_spacing = 1,
},
-- Optional: flags that generate warnings (default: {"!"})
diagnostic_flags = { "!" },
},
})
-- To override auto-detected checker method:
-- lspconfig.beancount.setup({
-- init_options = {
-- bean_check = {
-- method = "system", -- Force specific method: "python-embedded", "python-system", or "system"
-- },
-- },
-- })
File type detection: Ensure beancount files are detected. Add to your config:
vim.filetype.add({
extension = {
beancount = "beancount",
bean = "beancount",
},
})
Workspace-specific configuration: Use .nvim.lua or exrc for per-project settings:
-- .nvim.lua in your beancount project root
vim.lsp.config.beancount = {
init_options = {
journal_file = vim.fn.getcwd() .. "/main.beancount",
},
}
Or with nvim-lspconfig, use on_new_config to dynamically set the journal file:
lspconfig.beancount.setup({
on_new_config = function(new_config, new_root_dir)
new_config.init_options = new_config.init_options or {}
new_config.init_options.journal_file = new_root_dir .. "/main.beancount"
end,
})
Helix
Add to your languages.toml:
[language-server.beancount-language-server]
command = "beancount-language-server"
args = ["--stdio"]
# Configuration is optional
[language-server.beancount-language-server.config]
# Optional: Only needed for multi-file projects with include directives
# journal_file = "/path/to/main.beancount"
# Optional: bean_check config (uses python-embedded by default)
# [language-server.beancount-language-server.config.bean_check]
# method = "python-embedded" # or "python-system" or "system"
# Optional: formatting configuration
[language-server.beancount-language-server.config.formatting]
prefix_width = 30
currency_column = 60
number_currency_spacing = 1
[[language]]
name = "beancount"
language-servers = [{ name = "beancount-language-server" }]
Zed
Add to your settings.json (access via Zed > Settings > Open Settings):
{
"lsp": {
"beancount-language-server": {
"binary": {
"path": "beancount-language-server",
"arguments": ["--stdio"]
},
"initialization_options": {
// Optional: Only needed for multi-file projects with include directives
"journal_file": "/path/to/main.beancount",
"formatting": {
"prefix_width": 30,
"currency_column": 60,
"number_currency_spacing": 1
}
}
}
},
"languages": {
"Beancount": {
"language_servers": ["beancount-language-server"]
}
}
}
For workspace-specific configuration, create a .zed/settings.json in your project root:
{
"lsp": {
"beancount-language-server": {
"initialization_options": {
"journal_file": "main.beancount"
}
}
}
}
Note: Zed may require a Beancount extension for syntax highlighting. The language server provides completions, diagnostics, and formatting regardless of syntax highlighting support.
Emacs
Using lsp-mode:
(use-package lsp-mode
:hook (beancount-mode . lsp-deferred)
:config
(lsp-register-client
(make-lsp-client
:new-connection (lsp-stdio-connection "beancount-language-server")
:major-modes '(beancount-mode)
:server-id 'beancount-language-server
:initialization-options
;; All options are optional
(lambda () (list
;; Optional: Only needed for multi-file projects with include directives
;; :journal_file "/path/to/main.beancount"
;; Optional: bean_check config (uses python-embedded by default)
;; :bean_check '(:method "python-embedded")
:formatting '(:prefix_width 30 :currency_column 60 :number_currency_spacing 1))))))
Workspace-specific configuration: Use .dir-locals.el in your project root:
;; .dir-locals.el
((beancount-mode
. ((lsp-clients-beancount-langserver-init-options
. (:journal_file "./main.beancount")))))
Or dynamically set based on project root:
(defun my/beancount-lsp-init-options ()
"Generate init options with project-local journal file."
(let ((journal-file (expand-file-name "main.beancount" (project-root (project-current)))))
(when (file-exists-p journal-file)
(list :journal_file journal-file))))
;; Use in lsp-register-client with :initialization-options #'my/beancount-lsp-init-options
Vim
Using vim-lsp:
if executable('beancount-language-server')
au User lsp_setup call lsp#register_server({
\ 'name': 'beancount-language-server',
\ 'cmd': {server_info->['beancount-language-server']},
\ 'allowlist': ['beancount'],
\ 'initialization_options': {
\ 'formatting': {
\ 'prefix_width': 30,
\ 'currency_column': 60,
\ 'number_currency_spacing': 1
\ }
\ }
\ })
" Optional: For multi-file projects with include directives, add:
" \ 'journal_file': '/path/to/main.beancount',
" Optional: To override default checker method, add:
" \ 'bean_check': {'method': 'python-embedded'},
endif
Sublime Text
Using LSP:
Add to LSP settings:
{
"clients": {
"beancount-language-server": {
"enabled": true,
"command": ["beancount-language-server"],
"selector": "source.beancount",
// All initializationOptions are optional
"initializationOptions": {
// Optional: Only needed for multi-file projects with include directives
// "journal_file": "/path/to/main.beancount",
"formatting": {
"prefix_width": 30,
"currency_column": 60,
"number_currency_spacing": 1
}
}
}
}
}
๐๏ธ Architecture
High-Level Overview
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Editor โโโโโบโ LSP Server โโโโโบโ Beancount โ
โ โ โ โ โ (Python) โ
โ - VSCode โ โ - Completion โ โ - Validation โ
โ - Neovim โ โ - Formatting โ โ - Parsing โ
โ - Helix โ โ - Diagnostics โ โ - Bean-check โ
โ - Emacs โ โ - Tree-sitter โ โ โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
Core Components
- LSP Server: Main Rust application handling LSP protocol
- Tree-sitter Parser: Fast, incremental parsing of Beancount syntax
- Completion Engine: Smart autocompletion with context awareness
- Diagnostic Provider: Multi-method validation system with pluggable checkers
- Bean-check Integration: Three validation methods (system, python-embedded)
- Formatter: Code formatting fully compatible with bean-format, supporting prefix-width, num-width, and currency-column options
Project Structure
beancount-language-server/
โโโ crates/lsp/ # Main LSP server implementation
โ โโโ src/
โ โ โโโ handlers.rs # LSP request/notification handlers
โ โ โโโ providers/ # Feature providers (completion, diagnostics, etc.)
โ โ โโโ checkers/ # Bean-check validation implementations
โ โ โ โโโ mod.rs # Strategy trait and factory pattern
โ โ โ โโโ system_call.rs # Traditional bean-check binary
โ โ โ โโโ pyo3_embedded.rs # PyO3 embedded Python
โ โ โ โโโ types.rs # Shared data structures
โ โ โโโ server.rs # Core LSP server logic
โโโ vscode/ # VS Code extension
โโโ flake.nix # Nix development environment
๐ ๏ธ Development
Prerequisites
- Rust (stable toolchain)
- Python with beancount
- Node.js (for VS Code extension)
Development Environment
Using Nix (Recommended):
nix develop
Manual Setup:
# Install Rust dependencies
cargo build
# Install Node.js dependencies (for VS Code extension)
cd vscode && pnpm install
# Install development tools
cargo install cargo-watch
Running Tests
# Run all tests
cargo test
# Run with coverage
cargo llvm-cov --all-features --locked --workspace --lcov --output-path lcov.info
# Run tests with PyO3 feature
cargo test --features python-embedded
# Run specific test
cargo test test_completion
Code Quality
# Format code
cargo fmt
# Lint code
cargo clippy --all-targets --all-features
# Check formatting
cargo fmt -- --check
Development Workflow
- Make changes to the Rust code
- Test locally with
cargo test - Run LSP server in development mode:
cargo run --bin beancount-language-server - Test with editor by configuring it to use the local binary
VS Code Extension Development
cd vscode
pnpm run build # Build extension
pnpm run watch # Watch for changes
pnpm run package # Package extension
Release Process
- Tag a release:
git tag v1.0.0 && git push --tags - GitHub Actions automatically builds and publishes:
- Binaries for all supported platforms
- Crates.io release
- GitHub release with assets
๐ค Contributing
Contributions are welcome! Here are some ways to help:
๐ Bug Reports
- Search existing issues first
- Include beancount file examples that trigger the bug
- Provide editor and OS information
๐ก Feature Requests
- Check the planned features list
- Describe the use case and expected behavior
- Consider the LSP specification constraints
๐ง Code Contributions
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes with tests
- Ensure code quality:
cargo fmt && cargo clippy && cargo test - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
๐ฏ Good First Issues
Look for issues labeled good-first-issue:
- Add new completion types
- Improve error messages
- Add editor configuration examples
- Improve documentation
๐ Resources
- Beancount Documentation
- Language Server Protocol Specification
- Tree-sitter Beancount Grammar
- VSCode Extension API
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments
- Beancount - The amazing double-entry bookkeeping language
- Tree-sitter - Incremental parsing framework
- LSP - Language Server Protocol specification
- Twemoji - Emoji graphics used in the icon
Happy Beancounting! ๐โจ
Dependencies
~21โ28MB
~423K SLoC