mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-09-09 03:40:34 +00:00
3.8 KiB
3.8 KiB
Architecture Insights from Ruff Investigation
Key Discovery: Two-Layer Architecture
The Problem
- LSP documents change frequently (every keystroke)
- Salsa invalidation is expensive
- Tower-lsp requires Send+Sync, but Salsa Database contains RefCell/UnsafeCell
The Solution (Ruff Pattern)
Layer 1: LSP Document Management (Outside Salsa)
- Store overlays in
Session
usingArc<DashMap<Url, TextDocument>>
- TextDocument contains actual content, version, language_id
- Changes are immediate, no Salsa invalidation
Layer 2: Salsa Incremental Computation
- Database is pure Salsa, no file storage
- Queries read through FileSystem trait
- LspFileSystem intercepts reads, returns overlay or disk content
Critical Insights
-
Overlays NEVER become Salsa inputs directly
- They're intercepted at FileSystem::read_to_string() time
- Salsa only knows "something changed", reads content lazily
-
StorageHandle Pattern (for tower-lsp)
- Session stores
StorageHandle<Database>
not Database directly - StorageHandle IS Send+Sync even though Database isn't
- Create Database instances on-demand:
session.db()
- Session stores
-
File Management Location
- WRONG: Store files in Database (what we initially did)
- RIGHT: Store overlays in Session, Database is pure Salsa
-
The Bridge
- LspFileSystem has Arc to same overlays as Session
- When Salsa queries need content, they call FileSystem
- FileSystem checks overlays first, falls back to disk
Implementation Flow
- did_open/did_change/did_close → Update overlays in Session
- notify_file_changed() → Tell Salsa something changed
- Salsa query executes → Calls FileSystem::read_to_string()
- LspFileSystem intercepts → Returns overlay if exists, else disk
- Query gets content → Without knowing about LSP/overlays
Why This Works
- Fast: Overlay updates don't trigger Salsa invalidation cascade
- Thread-safe: DashMap for overlays, StorageHandle for Database
- Clean separation: LSP concerns vs computation concerns
- Efficient: Salsa caching still works, just reads through FileSystem
Tower-lsp vs lsp-server
- Ruff uses lsp-server: No Send+Sync requirement, can store Database directly
- We use tower-lsp: Requires Send+Sync, must use StorageHandle pattern
- Both achieve same result, different mechanisms
Critical Implementation Details (From Ruff Expert)
The Revision Dependency Trick
THE MOST CRITICAL INSIGHT: In the source_text
tracked function, calling file.revision(db)
is what creates the Salsa dependency chain:
#[salsa::tracked]
pub fn source_text(db: &dyn Db, file: SourceFile) -> Arc<str> {
// THIS LINE IS CRITICAL - Creates Salsa dependency on revision!
let _ = file.revision(db);
// Now read from FileSystem (checks overlays first)
db.read_file_content(file.path(db))
}
Without that file.revision(db)
call, revision changes won't trigger invalidation!
Key Implementation Points
- Files have no text: SourceFile inputs only have
path
andrevision
, nevertext
- Revision bumping triggers invalidation: Change revision → source_text invalidated → dependent queries invalidated
- Files created lazily: Don't pre-create, let them be created on first access
- Simple counters work: Revision can be a simple u64 counter, doesn't need timestamps
- StorageHandle update required: After DB modifications in LSP handlers, must update the handle
Common Pitfalls
- Forgetting the revision dependency - Without
file.revision(db)
, nothing invalidates - Storing text in Salsa inputs - Breaks the entire pattern
- Not bumping revision on overlay changes - Queries won't see new content
- Creating files eagerly - Unnecessary and inefficient