-
-
Notifications
You must be signed in to change notification settings - Fork 30
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Feature Request: Document Links (LSP 3.17)
Description
Implement textDocument/documentLink capability to make file paths, URLs, and document references clickable within beancount files.
Use Cases
Receipt and Document File Links
Make file paths in metadata clickable:
2024-01-15 * "Office Supplies" ^receipt-12345
Expenses:Office:Supplies 45.23 USD
Assets:Bank:Checking
receipt: "/path/to/receipts/2024-01-15-office-supplies.pdf" ← Clickable link
invoice: "file:///documents/invoices/INV-2024-001.pdf" ← Clickable link
Clicking opens the file in default application or editor.
External URLs in Comments and Metadata
Make URLs clickable:
2024-01-15 * "Domain Renewal"
Expenses:Web:DomainNames 12.99 USD
Assets:Bank:Checking
url: "https://example.com/receipt/12345" ← Clickable link
; Reference: https://docs.beancount.org/ ← Clickable link
Include Directive File Paths
Make included file paths navigable:
include "accounts.beancount" ← Click to open file
include "prices.beancount" ← Click to open file
include "2024/january.bean" ← Click to open file
Relative File References
Support relative paths from current file:
2024-01-15 * "Expense Report"
Expenses:Travel:Hotel 150.00 USD
Assets:Bank:Checking
attachment: "../receipts/hotel-2024-01-15.pdf" ← Clickable relative path
report: "./reports/travel-january-2024.xlsx" ← Clickable relative path
Account Documentation Links
Link to account planning documents:
2020-01-01 open Assets:Investment:Brokerage
description: "Main investment account"
documentation: "https://wiki.example.com/accounts/brokerage" ← Clickable link
policy: "/documents/investment-policy.pdf" ← Clickable link
Implementation Details
File: crates/lsp/src/providers/document_link.rs
Pattern:
pub fn document_links(
snapshot: LspServerStateSnapshot,
params: DocumentLinkParams,
) -> Result<Option<Vec<DocumentLink>>> {
let tree = snapshot.forest.get(¶ms.text_document.uri)?;
let content = snapshot.open_docs.get(¶ms.text_document.uri)?.content.clone();
let mut links = vec![];
// 1. Find include directives
for include_node in find_includes(&tree) {
let path = extract_string_value(&include_node, &content);
if let Some(resolved_uri) = resolve_include_path(&path, ¶ms.text_document.uri) {
links.push(DocumentLink {
range: node_to_range(&include_node),
target: Some(resolved_uri),
tooltip: Some(format!("Open {}", path)),
data: None,
});
}
}
// 2. Find metadata with file paths
for metadata_node in find_metadata_with_files(&tree) {
let value = extract_metadata_value(&metadata_node, &content);
if is_file_path(&value) {
if let Some(uri) = resolve_file_path(&value, ¶ms.text_document.uri) {
links.push(DocumentLink {
range: metadata_value_range(&metadata_node),
target: Some(uri),
tooltip: Some(format!("Open file: {}", value)),
data: None,
});
}
} else if is_url(&value) {
if let Ok(uri) = Url::parse(&value) {
links.push(DocumentLink {
range: metadata_value_range(&metadata_node),
target: Some(uri),
tooltip: Some(format!("Open URL: {}", value)),
data: None,
});
}
}
}
// 3. Find URLs in comments
for comment_node in find_comments(&tree) {
let comment_text = extract_comment_text(&comment_node, &content);
for url in extract_urls_from_text(&comment_text) {
if let Ok(uri) = Url::parse(&url) {
links.push(DocumentLink {
range: calculate_url_range(&comment_node, &url, &comment_text),
target: Some(uri),
tooltip: Some(format!("Open URL: {}", url)),
data: None,
});
}
}
}
Ok(Some(links))
}
fn is_file_path(value: &str) -> bool {
// Check if string looks like a file path
value.starts_with('/')
|| value.starts_with("./")
|| value.starts_with("../")
|| value.starts_with("file://")
|| value.contains("\\") // Windows paths
|| value.ends_with(".pdf")
|| value.ends_with(".png")
|| value.ends_with(".jpg")
|| value.ends_with(".xlsx")
// etc.
}
fn is_url(value: &str) -> bool {
value.starts_with("http://")
|| value.starts_with("https://")
|| value.starts_with("ftp://")
}
fn resolve_file_path(path: &str, base_uri: &Url) -> Option<Url> {
// Handle absolute paths
if path.starts_with('/') {
return Url::from_file_path(path).ok();
}
// Handle relative paths
if let Some(base_path) = base_uri.to_file_path().ok() {
let base_dir = base_path.parent()?;
let resolved = base_dir.join(path);
return Url::from_file_path(resolved).ok();
}
None
}
fn resolve_include_path(include_path: &str, base_uri: &Url) -> Option<Url> {
// Resolve include paths relative to current file
resolve_file_path(include_path, base_uri)
}Capability Registration
Update crates/lsp/src/capabilities.rs:
document_link_provider: Some(DocumentLinkOptions {
resolve_provider: Some(false),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
}),Link Detection Patterns
| Pattern | Example | Link Type |
|---|---|---|
| Include directive | include "file.bean" |
Beancount file |
| Absolute path | /path/to/receipt.pdf |
Local file |
| Relative path | ./receipts/file.pdf |
Local file |
| file:// URI | file:///documents/file.pdf |
Local file |
| HTTP(S) URL | https://example.com |
Web URL |
| Metadata values | receipt: "file.pdf" |
Local file |
| Comment URLs | ; See https://... |
Web URL |
Priority
MEDIUM - Convenient feature for accessing external documents and receipts
Configuration Options (Future)
{
"beancountLanguageServer": {
"documentLinks": {
"enableIncludeLinks": true,
"enableMetadataFileLinks": true,
"enableCommentUrls": true,
"enableReceiptLinks": true,
"receiptBasePath": "/home/user/documents/receipts",
"supportedFileExtensions": [".pdf", ".png", ".jpg", ".xlsx", ".csv"]
}
}
}User Experience
- Links should be visually distinct (underlined or colored)
- Ctrl+Click (or Cmd+Click on Mac) to follow link
- Tooltip shows target path/URL on hover
- Support opening in external application or editor
- Handle missing files gracefully (show error message)
Platform Considerations
- Path separators: Handle both (Unix) and (Windows)
- File URI encoding: Properly encode special characters
- Relative path resolution: Resolve relative to current file's directory
- Home directory expansion: Support expansion on Unix
- Case sensitivity: Consider platform differences
Enhancement Ideas
- Receipt organization: Suggest receipt file naming conventions
- Auto-create links: Code action to browse for file and insert link
- Link validation: Warn if linked file doesn't exist
- Receipt preview: Show thumbnail preview on hover (if client supports)
- Cloud storage: Support Dropbox, Google Drive, OneDrive links
- Smart path completion: Autocomplete file paths in metadata
Dependencies
- Path resolution utilities
- URL parsing (using
urlcrate) - File system access to verify paths exist (optional)
Error Handling
- Invalid URLs → Skip, don't create link
- Missing files → Create link anyway (editor will show error on click)
- Permission denied → Create link (let OS handle permission errors)
- Malformed paths → Skip
References
- LSP 3.17 Document Link specification
- Related: Add Document Symbols support (textDocument/documentSymbol) #748 (Document Symbols for navigation)
- Common metadata keys:
receipt,invoice,document,attachment,url,link
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request