How KCL's Virtual File System (VFS) Operates Within the LSP Implementation
KCL's LSP implementation leverages the ra_ap_vfs crate wrapped in an RwLock to maintain a thread-safe, in-memory view of every source file, enabling real-time diagnostics, import resolution, and semantic analysis on both saved and unsaved code buffers.
The KCL language server (LSP) provides IDE features like hover, completion, and diagnostics by virtualizing filesystem access. According to the kcl-lang/kcl source code, the server integrates the same Virtual File System (VFS) used by rust-analyzer to decouple file operations from the underlying OS, ensuring consistent performance and instant feedback during editing sessions.
Architecture of the KCL VFS LSP Integration
The ra_ap_vfs Foundation
KCL's LSP builds on the ra_ap_vfs crate, the battle-tested virtual file system extracted from rust-analyzer. This crate provides a versioned, in-memory store that tracks file contents independently of disk state, allowing the KCL compiler to operate on unsaved editor buffers as if they were canonical filesystem entries.
Thread-Safe State Management with KCLVfs
In crates/tools/src/LSP/src/state.rs, the server defines a type alias KCLVfs as an RwLock<Vfs>. This wrapper allows concurrent read access for features like hover and completion while ensuring exclusive write access during document mutations. The RwLock guarantees that analyses always observe a consistent snapshot of the code, even under heavy concurrent load.
Core VFS Operations in the Language Server
File Registration and FileId Resolution
When a client opens a document, the server converts the LSP URI to an absolute path and queries the VFS for a FileId. In crates/tools/src/LSP/src/util.rs (lines 71-84), the load_files_code_from_vfs function calls vfs.file_id(&path.into()) to retrieve an existing identifier or trigger allocation of a new one. This FileId serves as the canonical handle for all subsequent operations, abstracting away filesystem paths.
Reading File Contents with Fallback Logic
The VFS stores file contents as raw bytes. To retrieve source code, the LSP calls vfs.file_contents(file_id), which returns a Vec<u8> that the KCL parser consumes directly. If the VFS lacks an entry—for instance, when resolving imports for files not yet opened by the client—the implementation falls back to fs::read_to_string to load from disk, as shown in lines 80-94 of crates/tools/src/LSP/src/util.rs.
Handling Document Changes
Incremental synchronization happens via textDocument/didChange notifications. The server invokes apply_document_changes (lines 51-66 in crates/tools/src/LSP/src/util.rs) to compute the new text, then writes it back using vfs.set_file_contents(file_id, new_bytes). Because the VFS is versioned, downstream analyses automatically invalidate stale caches and recompute diagnostics against the latest buffer state.
Import Resolution and Module Paths
KCL’s module resolver queries the VFS before accessing the filesystem. By checking vfs.file_id for import paths, the server determines whether to use in-memory buffer contents or load from disk. This enables "open-file-first" semantics where unsaved changes in dependent modules are immediately visible to the type checker without requiring manual saves.
VFS Lifecycle in LSP Workflows
The virtual file system evolves through distinct phases during a session:
- Initialization: The
initializehandler creates an emptyKCLVfsinstance as part of the global server state. - Document Open:
textDocument/didOpenconverts the document URI to a path, allocates aFileId, and populates the VFS with the initial content. - Incremental Sync: Each
textDocument/didChangeacquires a write lock, applies edits viaapply_document_changes, and updates the VFS entry. - Feature Requests: Handlers for hover, completion, and diagnostics acquire read locks to fetch current file contents via
vfs.file_contents. - Document Close:
textDocument/didCloseoptionally evicts the file from the VFS or retains it cached for faster reopening.
Implementation Details and Code Examples
The following patterns demonstrate the VFS integration drawn directly from the KCL LSP source:
// 1️⃣ Create a new VFS (inside LSP state initialization)
use ra_ap_vfs::Vfs;
use parking_lot::RwLock;
type KCLVfs = RwLock<Vfs>;
let vfs: KCLVfs = RwLock::new(Vfs::default());
// 2️⃣ Register a newly opened file
fn open_file(vfs: &KCLVfs, uri: &Url, text: &str) -> anyhow::Result<()> {
let path = from_lsp::abs_path(uri)?; // turn `file://` → PathBuf
let mut guard = vfs.write(); // exclusive lock
let file_id = guard.file_id(&path.into())
.unwrap_or_else(|| guard.add_file(path.clone(), text.as_bytes()));
guard.set_file_contents(file_id, text.as_bytes().to_vec());
Ok(())
}
// 3️⃣ Apply a change from the LSP `textDocument/didChange`
fn apply_change(vfs: &KCLVfs, file_id: FileId, changes: Vec<TextDocumentContentChangeEvent>) {
let mut guard = vfs.write();
let mut current = String::from_utf8(guard.file_contents(file_id).to_vec()).unwrap();
apply_document_changes(&mut current, changes);
guard.set_file_contents(file_id, current.into_bytes());
}
// 4️⃣ Read a file for diagnostics / analysis
fn read_file(vfs: &KCLVfs, file_id: FileId) -> String {
let guard = vfs.read(); // concurrent read
String::from_utf8(guard.file_contents(file_id).to_vec()).unwrap()
}
Diagnostic reporting relies on path resolution via vfs.file_path(file_id).as_path(), with the helper get_file_name (lines 35-48 in crates/tools/src/LSP/src/util.rs) converting the path to a string for LSP payloads. Additional path normalization logic resides in crates/config/src/vfs.rs, which handles KCL-specific import path resolution.
Summary
- KCL's LSP uses the
ra_ap_vfscrate wrapped inRwLock<Vfs>(aliased asKCLVfs) to provide thread-safe file access. - FileId-based addressing abstracts filesystem paths into stable integer identifiers via
vfs.file_id(). - Dual-read strategy prefers in-memory VFS contents via
vfs.file_contents(), falling back tofs::read_to_stringfor unopened files. - Incremental updates through
vfs.set_file_contents()ensure the compiler always analyzes the latest unsaved buffer state. - Import resolution queries the VFS first, enabling cross-module analysis on dirty buffers without disk I/O.
Frequently Asked Questions
What crate does KCL use for its Virtual File System in the LSP?
KCL adopts the ra_ap_vfs crate, which is the same library powering rust-analyzer. This crate provides a versioned, in-memory filesystem abstraction that handles concurrent access and incremental updates efficiently.
How does the KCL LSP handle unsaved file changes during editing?
When a client sends textDocument/didChange notifications, the server acquires a write lock on the KCLVfs, computes the new text using apply_document_changes, and commits the update via vfs.set_file_contents(). All subsequent analyses read from this updated in-memory buffer, ensuring diagnostics reflect unsaved edits immediately.
Why does KCL wrap the VFS in an RwLock?
The RwLock (defined as type KCLVfs = RwLock<Vfs> in crates/tools/src/LSP/src/state.rs) enables multiple LSP handlers to read file contents simultaneously for features like hover and completion, while exclusive write locks ensure atomic updates during document changes. This prevents race conditions between the compiler and the editor state.
How does the LSP resolve imports when some files are open in the editor and others are not?
The import resolver first queries vfs.file_id() to check for in-memory entries. If the VFS contains the file, the parser uses the buffered content; otherwise, the system falls back to fs::read_to_string to load the file from disk. This hybrid approach ensures consistent module resolution regardless of whether dependencies are currently open in the IDE.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →