Skip to content

Commit a6fe2e4

Browse files
committed
docs: update README and add architectural design guide
1 parent 1d6d4cf commit a6fe2e4

File tree

2 files changed

+181
-14
lines changed

2 files changed

+181
-14
lines changed

README.md

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,23 @@
66

77
A full-featured, well-typed, and easy-to-use Python client for the Language Server Protocol (LSP). This library provides a clean, async-first interface for interacting with language servers, supporting both local and Docker-based runtimes.
88

9+
## Why lsp-client?
10+
11+
lsp-client` is designed specifically for developers who need **high control**, **isolation**, and **extensibility**:
12+
13+
- **🐳 Native Docker Support**: Unlike other clients that focus on local process management, `lsp-client` treats Docker as a first-class citizen. It handles the "magic" of mounting workspaces, translating file paths between your host and the container, and managing container lifecycles.
14+
- **🧩 SDK for Custom Tooling**: Instead of being a closed wrapper, this is a true SDK. Our **Modular Capability System** allows you to build custom clients by mixing and matching only the LSP features you need, or even adding your own protocol extensions seamlessly.
15+
- **🛠️ Explicit over Implicit**: We prioritize predictable environments. While other tools might auto-download binaries, `lsp-client` gives you full control over your server environment (Local or Docker), making it ideal for production-grade tools where version pinning is critical.
16+
- **⚡ Modern Async-First Architecture**: Built from the ground up for Python 3.12+, utilizing advanced async patterns to ensure high-performance concurrent operations without blocking your main event loop.
17+
918
## Features
1019

11-
- **🚀 Async-first Design**: Built for high-performance concurrent operations
12-
- **🔧 Full LSP Support**: Comprehensive implementation of LSP 3.17 specification
13-
- **🐳 Docker Support**: Run language servers in isolated containers
14-
- **📝 Type Safety**: Full type annotations with Pydantic validation
15-
- **🧩 Modular Architecture**: Mixin-based capability system for easy extension
16-
- **🎯 Production Ready**: Robust error handling with tenacity retries
17-
- **📚 Well Documented**: Extensive documentation and examples
20+
- **🚀 Environment Agnostic**: Seamlessly switch between local processes and isolated Docker containers.
21+
- **🔧 Full LSP 3.17 Support**: Comprehensive implementation of the latest protocol specification.
22+
- **🎯 Specialized Clients**: Out-of-the-box support for popular servers (Pyright, Deno, Rust-Analyzer, etc.).
23+
- **📝 Zero-Config Capabilities**: Automatically manages complex protocol handshakes and feature negotiations.
24+
- **🧩 Pluggable & Modular**: Easily extend functionality or add support for custom LSP extensions.
25+
- **🔒 Production-Grade Reliability**: Robust error handling, automatic retries, and full type safety.
1826

1927
## Quick Start
2028

@@ -90,13 +98,13 @@ if __name__ == "__main__":
9098

9199
The library includes pre-configured clients for popular language servers:
92100

93-
| Language Server | Module Path | Language |
94-
| ---------------------------- | ------------------------------------ | --------------------- |
95-
| Pyright | `lsp_client.clients.pyright` | Python |
96-
| Pyrefly | `lsp_client.clients.pyrefly` | Python |
97-
| Rust Analyzer | `lsp_client.clients.rust_analyzer` | Rust |
98-
| Deno | `lsp_client.clients.deno` | TypeScript/JavaScript |
99-
| TypeScript Language Server | `lsp_client.clients.typescript` | TypeScript/JavaScript |
101+
| Language Server | Module Path | Language |
102+
| -------------------------- | ---------------------------------- | --------------------- |
103+
| Pyright | `lsp_client.clients.pyright` | Python |
104+
| Pyrefly | `lsp_client.clients.pyrefly` | Python |
105+
| Rust Analyzer | `lsp_client.clients.rust_analyzer` | Rust |
106+
| Deno | `lsp_client.clients.deno` | TypeScript/JavaScript |
107+
| TypeScript Language Server | `lsp_client.clients.typescript` | TypeScript/JavaScript |
100108

101109
## Contributing
102110

docs/DESIGN.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# lsp-client: Architectural Design & Replication Guide
2+
3+
This document provides an in-depth analysis of the core design patterns used in `lsp-client`. It serves as a technical blueprint for engineers looking to replicate this architecture in other languages (e.g., Rust, Go, TypeScript).
4+
5+
---
6+
7+
## 1. Recursive Capability Discovery (MRO-based)
8+
9+
### Design Core
10+
The architecture leverages **Method Resolution Order (MRO)** and multiple inheritance to aggregate protocol parameters from decentralized "Capability Mixins" into a unified client declaration.
11+
12+
### Logic Flow
13+
```mermaid
14+
graph TD
15+
A[build_client_capabilities] --> B{Check: TextDoc Capability?}
16+
B -- Yes --> C[WithDefinition.register_text_document_capability]
17+
C --> D[super.register_text_document_capability]
18+
D --> E[WithHover.register_text_document_capability]
19+
E --> F[...]
20+
F --> G[Result: Fully Accumulated Capabilities Object]
21+
```
22+
23+
### Replication Pseudo-code (Rust/Trait approach)
24+
```rust
25+
// Define the base capability interface
26+
trait LspCapability {
27+
fn register_client_cap(&self, caps: &mut ClientCapabilities);
28+
}
29+
30+
// Module-specific implementation
31+
impl LspCapability for DefinitionModule {
32+
fn register_client_cap(&self, caps: &mut ClientCapabilities) {
33+
// Accumulate specific field into the shared container
34+
caps.text_document.definition = Some(DefinitionClientCapabilities { ... });
35+
}
36+
}
37+
38+
// Client aggregation
39+
struct MyLspClient {
40+
capabilities: Vec<Box<dyn LspCapability>>
41+
}
42+
43+
impl MyLspClient {
44+
fn build_initialize_params(&self) -> InitializeParams {
45+
let mut caps = ClientCapabilities::default();
46+
for cap in &self.capabilities {
47+
cap.register_client_cap(&mut caps); // Simulating the recursive accumulation
48+
}
49+
InitializeParams { capabilities: caps, .. }
50+
}
51+
}
52+
```
53+
54+
> **Engineer's Note**: Languages without native MRO (like Go or TS) should use **Explicit Registration**. Manually maintain a list of modules and iterate over them during initialization rather than relying on implicit inheritance chains.
55+
56+
---
57+
58+
## 2. Response Routing & Async Decoupling
59+
60+
### Design Core
61+
Uses a **Global ID Mapping Table** combined with **One-shot Channels** to realign the asynchronous JSON-RPC request-response flow.
62+
63+
### Sequence Diagram
64+
```mermaid
65+
sequenceDiagram
66+
participant User as Caller (Async Task)
67+
participant Table as OneShotTable
68+
participant Server as Remote LSP Server
69+
participant Loop as Background Read Loop
70+
71+
User->>Table: Register ID=123, Create Channel(tx, rx)
72+
User->>Server: Send Request (id: 123)
73+
User->>User: await rx.receive()
74+
75+
Server-->>Loop: Raw Response (id: 123)
76+
Loop->>Table: pop(123) -> Retrieve tx
77+
Loop->>Table: tx.send(Result)
78+
Table->>User: Wake up and return data
79+
```
80+
81+
---
82+
83+
## 3. Template Method for Server-Specific Config
84+
85+
### Design Core
86+
Utilizes the **Template Method Pattern** to allow specific clients (e.g., Pyright, Rust-Analyzer) to define unique initialization options while keeping the core lifecycle logic consistent.
87+
88+
### Implementation Logic
89+
```python
90+
class LSPClient(ABC):
91+
async def initialize(self):
92+
# Core Lifecycle
93+
options = self.create_initialization_options() # The Hook
94+
params = self.build_params(options)
95+
await self.request("initialize", params)
96+
97+
@abstractmethod
98+
def create_initialization_options(self) -> dict:
99+
return {} # Default implementation
100+
101+
class PyrightClient(LSPClient):
102+
def create_initialization_options(self) -> dict:
103+
# Override with server-specific logic
104+
return {
105+
"diagnosticMode": "workspace",
106+
"analysis": { "autoSearchPaths": True }
107+
}
108+
```
109+
110+
---
111+
112+
## 4. Server-to-Client Hook Registry
113+
114+
### Design Core
115+
A centralized **Registry** allows mixins to independently declare handlers for server-initiated requests (e.g., `workspace/configuration`) or notifications (e.g., `publishDiagnostics`).
116+
117+
### Dispatching Logic (Pseudo-code)
118+
```typescript
119+
// Logic inside the background read loop
120+
async function onPackageReceived(pkg: RawPackage) {
121+
if (isNotification(pkg)) {
122+
const handlers = registry.get(pkg.method);
123+
for (const handler of handlers) {
124+
// Trigger all registered capability handlers
125+
await handler(pkg.params);
126+
}
127+
} else if (isServerRequest(pkg)) {
128+
const handler = registry.getSingle(pkg.method);
129+
const result = await handler(pkg.params);
130+
await transport.sendResponse(pkg.id, result);
131+
}
132+
}
133+
```
134+
135+
---
136+
137+
## 5. Abstract Transport Layer (Strategy Pattern)
138+
139+
### Design Core
140+
Decouples the client core from the underlying I/O mechanism. The client communicates with an abstract interface, whether the server is a local process, a Docker container, or a remote socket.
141+
142+
### Interface Definition
143+
```typescript
144+
interface LSPServer {
145+
send(data: string): Promise<void>;
146+
receive(): AsyncIterable<string>;
147+
start(): Promise<void>;
148+
stop(): Promise<void>;
149+
}
150+
```
151+
152+
---
153+
154+
## Replication Checklist
155+
- [ ] **Ordered Registration**: In languages without MRO, ensure capability registration functions execute in a predictable order.
156+
- [ ] **Concurrency-Safe Registry**: Ensure the ID-to-Channel mapping is thread-safe or handled within a single-threaded event loop.
157+
- [ ] **URI/Path Translation**: Implement a robust utility for `path <-> uri` conversion, especially when dealing with Docker volume mounts.
158+
- [ ] **Structured Concurrency**: Bind the background Read Loop's lifetime to the Client instance to prevent resource leaks.
159+
- [ ] **Strongly Typed Schema**: Prefer generating types from the LSP Meta-model (3.17) rather than using untyped JSON objects.

0 commit comments

Comments
 (0)