This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is an OWL 2 Manchester Syntax Language Server written in Rust with a VS Code extension. It provides IDE features (diagnostics, hover, completion, go-to-definition, rename, etc.) for .omn files containing OWL ontologies.
The project uses:
- tower-lsp for LSP protocol implementation
- tree-sitter-owl-ms for parsing (custom tree-sitter grammar)
- horned-owl for OWL/RDF ontology manipulation
- ropey for efficient text buffer management
- tokio for async runtime
# Build the language server
cargo build
# Build release version
cargo build --release
# Run tests
cargo test
# Run specific test
cargo test test_name
# Run tests with logging output
cargo test -- --nocapture
# Lint
cargo clippy
# Format code
cargo fmt
# Run benchmarks
cargo benchcd editors/code
# Install dependencies
npm install
# Compile TypeScript
npm run compile
# Watch mode for development
npm run watch
# Package extension
npm run packageFor VS Code development:
- Build the language server with
cargo build - Open
editors/codein VS Code - Press F5 to launch Extension Development Host
To use local language server binary, either:
- Install it:
cargo install --path crates/owl-ms-language-server - Set
_OWL_MS_LSP_SERVER_DEBUGenvironment variable to the binary path (e.g.,target/debug/owl-ms-language-server)
The project requires nightly Rust toolchain: rustup default nightly
Backend (src/lib.rs):
- Main LSP implementation implementing
tower_lsp::LanguageServer - Owns a
SyncBackend(viaArc<RwLock<SyncBackend>>) that manages workspaces - Handles all LSP requests (hover, completion, diagnostics, etc.)
- Uses async/await extensively with tokio
- Clones itself for spawning async diagnostic tasks
SyncBackend (src/sync_backend.rs):
- Container for multiple
Workspaceinstances - Protected by RwLock for concurrent access
- Maps URLs to appropriate workspaces
- Creates single-file workspaces if no workspace folder contains the file
Workspace (src/workspace.rs):
- Contains
internal_documents(HashMap<PathBuf, InternalDocument>) - files opened/indexed by the server - Contains
external_documents(HashMap<Url, ExternalDocument>) - external ontologies fetched from web - Has a
WorkspaceFolderand loadedCatalogfiles - Provides frame lookup and IRI resolution across all documents
InternalDocument (src/workspace.rs):
- Represents an opened
.omnfile - Contains:
Rope(text buffer),Tree(parsed tree-sitter AST), version, path - Incrementally updates tree on edits using tree-sitter's
InputEdit - Extracts "frames" (Class, ObjectProperty, etc.) from the AST
- Maps IRIs to their definitions and references
ExternalDocument (src/workspace.rs):
- Represents external ontologies loaded via HTTP or file system
- Parsed using horned-owl (OWL/XML, RDF/XML, etc.)
- Converted to in-memory graph using sophia
The language server supports multi-ontology projects using catalog-v001.xml files:
- Catalog files map ontology IRIs (used in
Import:statements) to local file paths - Example:
<uri name="http://a.b/multi-file/other" uri="other.omn"/> - Catalogs are loaded recursively from workspace folders on initialization
- When a document imports an IRI, the server uses the catalog to resolve it to a local file
- If not in catalog, attempts to fetch from the IRI URL directly (can be disabled with
--offlineflag)
When a document is opened (did_open):
- Parse the document and extract all
Import:IRIs - Spawn background task via
load_dependencies()to resolve imports - For each import IRI:
- Check catalog for local file mapping
- If found locally, load as
InternalDocumentand recursively load its dependencies (depth limit: 2) - If not found, attempt HTTP fetch and parse as
ExternalDocument
- Background tasks use
.index_handlesto track completion (in tests, awaited synchronously)
Diagnostics are computed asynchronously after document changes:
did_changetriggersupdate_diagnostics_for_url_and_dependent()- Spawns async task that:
- Computes diagnostics for the changed document
- Publishes diagnostics via LSP
client.publish_diagnostics() - Finds dependent documents (documents that import this one)
- Recursively triggers diagnostics for each dependent
- Race conditions are acceptable - diagnostics will converge to correct state
The server supports both UTF-8 and UTF-16 position encoding:
- Negotiated during LSP initialization
- Stored in
Backend.position_encoding: OnceCell<PositionEncodingKind> - All position conversions go through
Position::from_lsp()/Position::into_lsp() Ropeis used for efficient column/offset conversions
A "frame" is an OWL entity definition (Class, ObjectProperty, DataProperty, AnnotationProperty, Individual, Datatype):
- Extracted using tree-sitter queries (
src/queries.rs) - Each frame has: IRI, type, label (from rdfs:label), definitions (locations), references
FrameInfoaggregates all information about an IRI across documents- Used for hover, completion, go-to-definition, references, and rename
Three IRI forms are supported:
- Full IRI:
<http://example.org/Thing> - Abbreviated IRI:
ex:Thing(prefix must be declared) - Simple IRI:
Thing(uses default prefix)
Functions like abbreviated_iri_to_full_iri() convert between forms using the document's prefix declarations.
- Grammar is in separate crate:
crates/tree-sitter-owl-ms/ - Queries defined in
src/queries.rs(highlighting, frame extraction, etc.) - Global parser in
GLOBAL_PARSERprotected by Mutex - Incremental reparsing on document edits using
tree.edit()andInputEdit
src/lib.rs- Main Backend and LanguageServer implementationsrc/sync_backend.rs- Workspace container with lockingsrc/workspace.rs- Document management and frame extraction (large file ~3000 lines)src/catalog.rs- Catalog XML parsing and IRI resolutionsrc/web.rs- HTTP client abstraction for fetching external ontologiessrc/queries.rs- Tree-sitter queries for syntax highlighting and frame detectionsrc/rope_provider.rs- Adapter for tree-sitter to use ropey Ropesrc/pos.rs,src/range.rs- Position/Range types with LSP conversion
- Unit tests in
src/tests.rs - Test files in
demo-ontologies/including multi-file examples - Tests use
#[test_log::test]attribute for logging - Some tests create temporary directories with
tempdircrate - Tests await indexing tasks synchronously (controlled by
#[cfg(test)])
Reading a document:
let sync = backend.read_sync().await;
let (doc, workspace) = sync.get_internal_document(&url)?;
// doc: &InternalDocument, workspace: &WorkspaceModifying a document:
let mut sync = backend.write_sync().await;
let (document, workspace) = sync.take_internal_document(&url)?;
let new_document = document.edit(¶ms, encoding)?;
workspace.insert_internal_document(new_document);Converting positions:
let pos: Position = Position::from_lsp(lsp_position, doc.rope(), encoding)?;
let lsp_pos = pos.into_lsp(doc.rope(), encoding)?;Tree traversal:
let node = doc.tree().root_node()
.named_descendant_for_point_range(pos.into(), pos.into())?;
let text = node_text(&node, doc.rope());- Position encoding must be checked for all LSP position/range conversions
- Errors should use the custom
Errortype insrc/error.rs - Use
ResultExttrait for.log_if_error()and.inspect_log() - Diagnostics are published asynchronously - spawned tasks should clone
Backend - Lock ordering: always acquire
synclock before any document operations - Tree-sitter parser is behind a global mutex - keep parse calls short
- Catalog files must be named exactly
catalog-v001.xml