A minimalistic VSCode extension implementing a Language Server Protocol (LSP) client with advanced file watching capabilities and command palette integration for TypeScript/JavaScript projects.
The project follows a service-oriented architecture with dependency injection and clear separation of concerns. It's built around these main concepts:
- Services: Independent units with clear lifecycle management
- Composition Root: Central point for dependency injection and service orchestration
- Event-Based Communication: Using EventEmitter for file system changes
- Command System: Extensible command palette integration
- Interface-based design over inheritance
- Clear service lifecycle (initialize/dispose)
- Dependency injection for better testability
- Event-driven architecture for file watching
- Command pattern for user interactions
- Explicit state management
- Strong typing throughout the codebase
src/
├── composition/
│ └── CompositionRoot.ts # Service orchestration and DI container
├── services/
│ ├── types.ts # Core service interfaces and types
│ ├── FileWatcherService.ts # File system watching and caching
│ ├── LanguageClientService.ts # LSP client implementation
│ └── CommandService.ts # Command palette integration
├── commands/
│ ├── types.ts # Command interfaces and types
│ └── implementations.ts # Concrete command implementations
├── logger/
│ └── Logger.ts # Logging infrastructure
└── extension.ts # VSCode extension entry point
Core contract that all services must implement:
interface IService {
readonly state: ServiceState;
initialize(): Promise<void>;
dispose(): Promise<void>;
}
Manages file system operations and maintains an in-memory cache of files:
- Watches for file changes using VSCode's FileSystemWatcher
- Maintains a Map of FileInfo objects
- Emits events for file system changes
- Provides API for file content manipulation
Handles LSP client implementation:
- Configures and manages the Language Client
- Connects to the language server
- Handles client lifecycle
Manages VSCode command palette integration:
- Registers commands with VSCode
- Handles command execution
- Manages command lifecycle
- Provides type-safe command registration
Example command registration:
interface Command {
id: string;
title: string;
handler: (...args: any[]) => Promise<void>;
}
commandService.registerCommand({
id: 'myExtension.commandId',
title: 'My Command',
handler: async () => {
// Command implementation
}
});
Manages service lifecycle and dependencies:
- Initializes all services in correct order
- Handles proper cleanup
- Centralizes error handling
- Orchestrates command registration
Each service follows a strict lifecycle:
- Construction: Services receive their dependencies
- Initialization: async setup operations
- Active State: service is running
- Disposal: cleanup of resources
Example:
const service = new FileWatcherService({
logger,
workspacePath
});
await service.initialize();
// ... use service
await service.dispose();
The FileWatcherService emits events for file changes:
fileWatcherService.on('add', ({ file }) => {
// Handle new file
});
fileWatcherService.on('change', ({ file }) => {
// Handle file modifications
});
fileWatcherService.on('unlink', ({ file }) => {
// Handle file deletion
});
Commands are defined as separate units:
interface Command {
id: string;
title: string;
handler: (...args: any[]) => Promise<void>;
}
// Command implementation
const command: Command = {
id: 'miniLanguageServer.showFileCount',
title: 'Show File Count',
handler: async () => {
// Implementation
}
};
- Clone the repository
- Install dependencies:
npm install
- Build the extension:
npm run compile
- Launch the extension in debug mode:
- Press F5 in VSCode
- A new VSCode window will open with the extension loaded
-
Adding a New Service
- Create a new file in
src/services
- Implement the
IService
interface - Add to CompositionRoot
- Create a new file in
-
Adding New Commands
- Define command in
commands/implementations.ts
- Add command contribution to
package.json
- Command will be automatically registered by CommandService
- Define command in
Example new command:
export function createCustomCommand(service: SomeService): Command {
return {
id: 'miniLanguageServer.customCommand',
title: 'Custom Command',
handler: async () => {
// Implementation
}
};
}
-
State Management
- Always check service state before operations
- Use state assertions
- Clean up resources properly
-
Error Handling
- Use async/await consistently
- Provide meaningful error messages
- Log errors appropriately
-
Dependency Injection
- Define clear interfaces for dependencies
- Use dependency injection in constructors
- Avoid service locator pattern
-
Command Implementation
- Keep commands focused and single-purpose
- Provide clear command titles
- Handle errors gracefully
- Log command execution
MIT