A Language Server Protocol (LSP) for beancount files
Rust
250
790 commits
updated Sep 1, 2026
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.
| 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 | โ |
Assets:Checking)#vacation)^receipt-123)txn, balance, open, close, etc.Non-intrusive inline annotations that help visualize implicit information:
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
| 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 |
cargo install beancount-language-server
Download the latest release for your platform from the releases page.
Supported Platforms:
brew install beancount-language-server
# Using nix-env
nix-env -iA nixpkgs.beancount-language-server
# Using nix shell
nix shell nixpkgs#beancount-language-server
# Development environment
nix develop
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.
The language server requires one of the following for validation and diagnostics:
Option 1: PyO3 Embedded (Default - Recommended)
pip install beancount
The PyO3 embedded checker (recommended) works seamlessly with:
No special configuration needed - the binary automatically detects and uses available Python installations.
The language server searches for Python in this order:
BEANCOUNT_LSP_PYTHON environment variable (if set)python_cmd in LSP settings.venv/bin/python (or .venv/Scripts/python.exe on Windows)python3 in PATHpython in PATHFor 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)
Option 3: Bean-check Binary (Fallback)
bean-check command-line toolpip install beancount (includes bean-check)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.
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
},
"completion": {
"fuzzy_match_accounts": true // Optional: cross-segment fuzzy matching (default: false)
},
"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).
| 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 |
The journal_file setting is workspace-specific. Each editor workspace (project folder) can have its own journal file configured. This means:
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.
| 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)Default (no configuration needed):
The language server automatically selects the best available checker method:
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)
}
}
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:
:LspLogInstall 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:
Check your configuration if you need to explicitly set a method.
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.
| 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 |
Default Mode (no currency_column specified):
bean-format with no special optionsCurrency Column Mode (currency_column specified):
bean-format --currency-column NBasic 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
}
}
| Option | Type | Description | Default |
|---|---|---|---|
completion.fuzzy_match_accounts | boolean | Enable cross-segment fuzzy matching for account completions. When enabled, typing "BankCheck" can match "Assets:US:Bank:Checking" without typing each segment hierarchically. | false |
Disable cross-segment fuzzy matching:
{
"completion": {
"fuzzy_match_accounts": false
}
}
When disabled, account completions use standard prefix matching only (e.g., you must type "Assets:US:Bank" to match accounts under that path).
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.
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 = { "!" },
-- Optional: completion behavior
completion = {
fuzzy_match_accounts = true, -- cross-segment fuzzy matching (default: false)
},
},
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: completion behavior
-- completion = {
-- fuzzy_match_accounts = true, -- cross-segment fuzzy matching (default: false)
-- },
-- 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,
})
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: completion behavior
# [language-server.beancount-language-server.config.completion]
# fuzzy_match_accounts = true # cross-segment fuzzy matching (default: false)
# 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" }]
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
},
// Optional: completion behavior
"completion": {
"fuzzy_match_accounts": true // cross-segment fuzzy matching (default: true)
}
}
}
},
"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.
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")
;; Optional: completion behavior
;; :completion '(:fuzzy_match_accounts t)
: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
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
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
},
// Optional: completion behavior
// "completion": {
// "fuzzy_match_accounts": true
// }
}
}
}
}
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Editor โโโโโบโ LSP Server โโโโโบโ Beancount โ
โ โ โ โ โ (Python) โ
โ - VSCode โ โ - Completion โ โ - Validation โ
โ - Neovim โ โ - Formatting โ โ - Parsing โ
โ - Helix โ โ - Diagnostics โ โ - Bean-check โ
โ - Emacs โ โ - Tree-sitter โ โ โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
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
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
# 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
# Format code
cargo fmt
# Lint code
cargo clippy --all-targets --all-features
# Check formatting
cargo fmt -- --check
cargo testcargo run --bin beancount-language-server
cd vscode
pnpm run build # Build extension
pnpm run watch # Watch for changes
pnpm run package # Package extension
git tag v1.0.0 && git push --tagsContributions are welcome! Here are some ways to help:
git checkout -b feature/amazing-feature)cargo fmt && cargo clippy && cargo testgit commit -m 'Add amazing feature')git push origin feature/amazing-feature)Look for issues labeled good-first-issue:
This project is licensed under the MIT License - see the LICENSE file for details.
Happy Beancounting! ๐โจ
Rust
94.2%
Just
1.5%
Python
1.3%
TypeScript
1.3%
A Language Server Protocol (LSP) for beancount files
Rust
250
790 commits
updated Sep 1, 2026
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.
| 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 | โ |
Assets:Checking)#vacation)^receipt-123)txn, balance, open, close, etc.Non-intrusive inline annotations that help visualize implicit information:
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
| 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 |
cargo install beancount-language-server
Download the latest release for your platform from the releases page.
Supported Platforms:
brew install beancount-language-server
# Using nix-env
nix-env -iA nixpkgs.beancount-language-server
# Using nix shell
nix shell nixpkgs#beancount-language-server
# Development environment
nix develop
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.
The language server requires one of the following for validation and diagnostics:
Option 1: PyO3 Embedded (Default - Recommended)
pip install beancount
The PyO3 embedded checker (recommended) works seamlessly with:
No special configuration needed - the binary automatically detects and uses available Python installations.
The language server searches for Python in this order:
BEANCOUNT_LSP_PYTHON environment variable (if set)python_cmd in LSP settings.venv/bin/python (or .venv/Scripts/python.exe on Windows)python3 in PATHpython in PATHFor 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)
Option 3: Bean-check Binary (Fallback)
bean-check command-line toolpip install beancount (includes bean-check)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.
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
},
"completion": {
"fuzzy_match_accounts": true // Optional: cross-segment fuzzy matching (default: false)
},
"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).
| 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 |
The journal_file setting is workspace-specific. Each editor workspace (project folder) can have its own journal file configured. This means:
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.
| 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)Default (no configuration needed):
The language server automatically selects the best available checker method:
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)
}
}
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:
:LspLogInstall 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:
Check your configuration if you need to explicitly set a method.
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.
| 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 |
Default Mode (no currency_column specified):
bean-format with no special optionsCurrency Column Mode (currency_column specified):
bean-format --currency-column NBasic 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
}
}
| Option | Type | Description | Default |
|---|---|---|---|
completion.fuzzy_match_accounts | boolean | Enable cross-segment fuzzy matching for account completions. When enabled, typing "BankCheck" can match "Assets:US:Bank:Checking" without typing each segment hierarchically. | false |
Disable cross-segment fuzzy matching:
{
"completion": {
"fuzzy_match_accounts": false
}
}
When disabled, account completions use standard prefix matching only (e.g., you must type "Assets:US:Bank" to match accounts under that path).
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.
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 = { "!" },
-- Optional: completion behavior
completion = {
fuzzy_match_accounts = true, -- cross-segment fuzzy matching (default: false)
},
},
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: completion behavior
-- completion = {
-- fuzzy_match_accounts = true, -- cross-segment fuzzy matching (default: false)
-- },
-- 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,
})
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: completion behavior
# [language-server.beancount-language-server.config.completion]
# fuzzy_match_accounts = true # cross-segment fuzzy matching (default: false)
# 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" }]
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
},
// Optional: completion behavior
"completion": {
"fuzzy_match_accounts": true // cross-segment fuzzy matching (default: true)
}
}
}
},
"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.
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")
;; Optional: completion behavior
;; :completion '(:fuzzy_match_accounts t)
: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
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
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
},
// Optional: completion behavior
// "completion": {
// "fuzzy_match_accounts": true
// }
}
}
}
}
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Editor โโโโโบโ LSP Server โโโโโบโ Beancount โ
โ โ โ โ โ (Python) โ
โ - VSCode โ โ - Completion โ โ - Validation โ
โ - Neovim โ โ - Formatting โ โ - Parsing โ
โ - Helix โ โ - Diagnostics โ โ - Bean-check โ
โ - Emacs โ โ - Tree-sitter โ โ โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
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
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
# 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
# Format code
cargo fmt
# Lint code
cargo clippy --all-targets --all-features
# Check formatting
cargo fmt -- --check
cargo testcargo run --bin beancount-language-server
cd vscode
pnpm run build # Build extension
pnpm run watch # Watch for changes
pnpm run package # Package extension
git tag v1.0.0 && git push --tagsContributions are welcome! Here are some ways to help:
git checkout -b feature/amazing-feature)cargo fmt && cargo clippy && cargo testgit commit -m 'Add amazing feature')git push origin feature/amazing-feature)Look for issues labeled good-first-issue:
This project is licensed under the MIT License - see the LICENSE file for details.
Happy Beancounting! ๐โจ
Rust
94.2%
Just
1.5%
Python
1.3%
TypeScript
1.3%