Skip to content

Add Code Lens support (textDocument/codeLens) #752

@polarmutex

Description

@polarmutex

Feature Request: Code Lens (LSP 3.17)

Description

Implement textDocument/codeLens capability to show inline, actionable information above code elements without editing the document.

Use Cases

Account Statistics Above Account Opens

Display usage statistics and current balance:

💰 152 transactions | Balance: $5,234.56 USD | ▶ Show details    ← Code lens
2020-01-01 open Assets:Bank:Checking

Transaction Validation Status

Show balance status for transactions:

✓ Balanced | 2 postings | ▶ View postings                        ← Code lens
2024-01-15 * "Grocery Store"
  Expenses:Food:Groceries    45.23 USD
  Assets:Bank:Checking      -45.23 USD

Or for unbalanced:

⚠ Unbalanced: -0.01 USD | ▶ Fix automatically                    ← Code lens
2024-01-15 * "Restaurant"
  Expenses:Food:Dining       52.34 USD
  Assets:Bank:Checking      -52.33 USD  ; Missing 0.01 USD

Account Usage Summary

Show where account is used:

📊 Used in 45 transactions | Last: 2024-03-15 | ▶ Show all      ← Code lens
2020-01-01 open Expenses:Food:Groceries

Commodity Price Information

Display latest price and changes:

💵 Latest: 1 AAPL = $150.25 USD (+2.3%) | ▶ Price history     ← Code lens
2020-01-01 commodity AAPL

Balance Assertion Verification

Show actual vs expected:

✓ Verified: Actual $1,000.00 matches expected | As of 2024-01-15  ← Code lens
2024-01-15 balance Assets:Bank:Checking  1000.00 USD

Or when failing:

❌ Failed: Actual $995.50 ≠ Expected $1,000.00 | Diff: -$4.50    ← Code lens
2024-01-15 balance Assets:Bank:Checking  1000.00 USD

Implementation Details

File: crates/lsp/src/providers/code_lens.rs

Pattern:

pub fn code_lens(
    snapshot: LspServerStateSnapshot,
    params: CodeLensParams,
) -> Result<Option<Vec<CodeLens>>> {
    let tree = snapshot.forest.get(&params.text_document.uri)?;
    let mut lenses = vec![];
    
    let mut cursor = tree.root_node().walk();
    for child in tree.root_node().children(&mut cursor) {
        match child.kind() {
            "open" => {
                let account = extract_account_name(&child);
                let stats = calculate_account_stats(&account, &snapshot);
                
                lenses.push(CodeLens {
                    range: node_to_range(&child),
                    command: Some(Command {
                        title: format!(
                            "💰 {} txns | {} | ▶ Show details",
                            stats.transaction_count,
                            stats.current_balance
                        ),
                        command: "beancount.showAccountDetails".to_string(),
                        arguments: Some(vec![json!(account)]),
                    }),
                    data: None,
                });
            }
            
            "txn" => {
                let balance_status = check_transaction_balance(&child);
                let icon = if balance_status.is_balanced { "✓" } else { "⚠" };
                let action = if balance_status.is_balanced {
                    "View postings"
                } else {
                    "Fix automatically"
                };
                
                lenses.push(CodeLens {
                    range: node_to_range(&child),
                    command: Some(Command {
                        title: format!(
                            "{} {} | {} postings | ▶ {}",
                            icon,
                            balance_status.message,
                            balance_status.posting_count,
                            action
                        ),
                        command: if balance_status.is_balanced {
                            "beancount.viewPostings".to_string()
                        } else {
                            "beancount.fixTransaction".to_string()
                        },
                        arguments: Some(vec![json!(child.id())]),
                    }),
                    data: None,
                });
            }
            
            "balance" => {
                let assertion = extract_balance_assertion(&child);
                let verification = verify_balance(&assertion, &snapshot);
                
                lenses.push(CodeLens {
                    range: node_to_range(&child),
                    command: Some(Command {
                        title: format!(
                            "{} {} | As of {}",
                            if verification.matches { "✓ Verified" } else { "❌ Failed" },
                            verification.message,
                            assertion.date
                        ),
                        command: "beancount.showBalanceDetails".to_string(),
                        arguments: Some(vec![json!(assertion)]),
                    }),
                    data: None,
                });
            }
            
            "commodity" => {
                let commodity = extract_commodity_name(&child);
                if let Some(price_info) = get_latest_price(&commodity, &snapshot) {
                    lenses.push(CodeLens {
                        range: node_to_range(&child),
                        command: Some(Command {
                            title: format!(
                                "💵 Latest: {} | ▶ Price history",
                                price_info.display()
                            ),
                            command: "beancount.showPriceHistory".to_string(),
                            arguments: Some(vec![json!(commodity)]),
                        }),
                        data: None,
                    });
                }
            }
            
            _ => {}
        }
    }
    
    Ok(Some(lenses))
}

struct AccountStats {
    transaction_count: usize,
    current_balance: String,
    last_transaction_date: Option<NaiveDate>,
}

fn calculate_account_stats(account: &str, snapshot: &LspServerStateSnapshot) -> AccountStats {
    // Query beancount data or bean-query for statistics
}

Capability Registration

Update crates/lsp/src/capabilities.rs:

code_lens_provider: Some(CodeLensOptions {
    resolve_provider: Some(false),
    ..Default::default()
}),

Commands to Implement

The code lenses reference these commands (to be registered):

  • beancount.showAccountDetails - Open panel with account transactions
  • beancount.viewPostings - Show posting details for transaction
  • beancount.fixTransaction - Auto-balance transaction (triggers code action)
  • beancount.showBalanceDetails - Display balance verification details
  • beancount.showPriceHistory - Show commodity price chart/history

Priority

MEDIUM - Provides valuable at-a-glance information without cluttering the editor

Configuration Options (Future)

{
  "beancountLanguageServer": {
    "codeLens": {
      "enableAccountStats": true,
      "enableTransactionStatus": true,
      "enableBalanceVerification": true,
      "enableCommodityPrices": true,
      "showIcons": true,
      "refreshIntervalSeconds": 60
    }
  }
}

Performance Considerations

  • Lazy calculation: Only calculate stats for visible lenses
  • Caching: Cache account statistics, refresh periodically
  • Debouncing: Don't recalculate on every keystroke
  • Incremental updates: Only update affected lenses when document changes
  • Background computation: Calculate expensive stats (balance queries) asynchronously

User Experience

  • Lenses should be clickable and provide immediate actions
  • Keep text concise (one line)
  • Use icons sparingly for visual scanning
  • Consider user preference to disable certain lens types
  • Allow hiding/showing code lenses via editor setting

Dependencies

Enhancement Ideas

  • Budget tracking: Show budget vs actual spending for expense accounts
  • Account hierarchy: Show total including subaccounts
  • Time-based stats: "This month: +$1,234.56" for income/expense accounts
  • Tags/links summary: "Used in 23 transactions" for tags
  • Net worth snapshot: Show total across all asset/liability accounts

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions