This repository contains a set of packages that collectively implement language support for Salesforce Apex, following the Language Server Protocol (LSP) specification.
The project is structured as a monorepo with several interconnected packages that serve different purposes in the language support ecosystem.
graph TD
subgraph "Core Components"
apex-parser-ast[apex-parser-ast]
custom-services[custom-services]
lsp-compliant-services[lsp-compliant-services]
apex-lsp-shared[apex-lsp-shared]
end
subgraph "Runtime"
apex-ls[apex-ls]
apex-lsp-vscode-extension[apex-lsp-vscode-extension]
end
subgraph "Testing & Development"
apex-lsp-testbed[apex-lsp-testbed]
end
%% Core dependencies
apex-parser-ast --> custom-services
apex-parser-ast --> lsp-compliant-services
apex-lsp-shared --> custom-services
apex-lsp-shared --> lsp-compliant-services
%% Runtime implementation
custom-services --> apex-ls
lsp-compliant-services --> apex-ls
apex-ls --> apex-lsp-vscode-extension
%% Testing dependencies
apex-ls --> apex-lsp-testbed
- apex-parser-ast: Provides AST (Abstract Syntax Tree) parsing capabilities for Apex code
- custom-services: Implements custom services beyond the standard LSP specification
- lsp-compliant-services: Implements standard LSP services (completion, hover, etc.)
- apex-lsp-shared: Provides shared utilities including logging, notifications, and common functionality used across the language server ecosystem
- apex-ls: Apex Language Server that works across browser, Node.js, and web worker environments
- apex-lsp-vscode-extension: The VS Code extension package that integrates with VS Code's extension API
- apex-lsp-testbed: Testing utilities and integration tests for the language server
The apex-ls package implements the language server. It runs in Node.js (desktop), browser, and web worker environments, using the appropriate storage backend for each (file system for Node.js, IndexedDB for browser). Feature parity is maintained across all environments through the same LSP handlers and capabilities.
All LSP requests follow a single mandatory path regardless of whether workers are active:
LSP Client → LCSAdapter (connection handler)
→ LSPQueueManager (priority scheduling)
→ WorkerDispatcher (route to coordinator or worker)
→ Response
The LSPQueueManager implements a five-level priority scheduler with starvation relief. Priority scheduling and worker dispatch are orthogonal concerns: the queue controls when a request runs; the dispatcher controls where.
Three request types always execute on the coordinator thread:
| Type | Reason |
|---|---|
completion |
Requires stateful session context |
signatureHelp |
Coupled to the completion session |
rename |
Requires cross-file coordination |
All other types — hover, definition, diagnostics, documentOpen/Change/Save/Close, references, implementation, codeLens, documentSymbol, foldingRange — are dispatchable to workers when the topology is active.
When apex.experimental.workers.enabled is true, the language server spawns an internal worker topology using @effect/platform Worker primitives. The LSP client is unaware of workers; they are entirely internal to the server process.
graph TB
subgraph "VS Code Extension Host"
Client["LSP Client"]
end
subgraph "Coordinator Thread"
LCS["LCSAdapter\nLSP Connection Handler"]
Queue["LSPQueueManager\nPriority Scheduler"]
Dispatch["WorkerDispatcher\nRoute by request type"]
Mediator["AssistanceMediator\nCross-worker IPC router"]
RLProxy["ResourceLoaderProxy\nStdlib IPC bridge"]
LCS --> Queue
Queue --> Dispatch
LCS --- Mediator
Mediator --- RLProxy
end
subgraph "Resource-Loader Worker"
RL["ResourceLoader\nProtobuf cache · ZIP archive"]
end
subgraph "Data-Owner Worker"
DO["SymbolManager\nCanonical workspace symbol graph\nDocument lifecycle · Batch ingestion"]
end
subgraph "Enrichment Worker Pool (×N)"
EW1["Enrichment Worker #1"]
EW2["Enrichment Worker #2"]
end
Client <-->|"LSP protocol"| LCS
Dispatch -->|"DispatchHover · DispatchDefinition\nDispatchDiagnostic"| EW1
Dispatch -->|"DispatchHover · DispatchDefinition\nDispatchDiagnostic"| EW2
Dispatch -->|"DispatchDocumentOpen/Change\nWorkspaceBatchIngest\nQueryGraphData"| DO
EW1 <-.->|"QuerySymbolSubset\nUpdateSymbolSubset\nresourceLoader:*"| Mediator
EW2 <-.->|"QuerySymbolSubset\nUpdateSymbolSubset\nresourceLoader:*"| Mediator
Mediator <-.->|"read · write-back"| DO
Mediator <-.->|"stdlib queries"| RLProxy
RLProxy <-->|"@effect/platform\nWorker protocol"| RL
| Role | Count | Responsibility |
|---|---|---|
| Coordinator | 1 (main thread) | LSP connection, queue scheduling, worker dispatch, cross-worker IPC mediation |
| Data-owner | 1 | Canonical SymbolManager; document lifecycle (open/change/save/close); workspace batch ingestion; QuerySymbolSubset; QueryGraphData |
| Enrichment pool | N (default 2, max cpus-2) |
Stateless handlers: hover, definition, diagnostics, references, implementation, documentSymbol, codeLens, foldingRange |
| Resource-loader | 1 | Stdlib loading from protobuf cache and ZIP archive via ResourceLoaderService |
Enrichment workers are stateless. For each request they:
- Load — send
QuerySymbolSubsetto data-owner via the coordinator'sAssistanceMediatorto fetch symbol data for the target file. - Process — run the LSP handler (e.g. hover, definition) locally using a transient
SymbolManager. - Resolve stdlib — send
resourceLoader:resolveClass/resourceLoader:getSymbolTablethrough the mediator to the resource-loader worker when a standard library type is encountered. - Write back — send
UpdateSymbolSubsetto data-owner if enrichment produced new symbol data, advancing the detail level for subsequent requests.
Worker messages use @effect/schema TaggedRequest classes defined in apex-lsp-shared/workerWireSchemas.ts. Each request/response pair is versioned (WIRE_PROTOCOL_VERSION), fully type-safe, and JSON round-trip safe (no structured-clone hazards).
WorkerTopologyTransport abstracts spawn/send/dispatch/shutdown behind opaque WorkerHandle/PoolHandle types. Production uses @effect/platform directly via makeNodeWorkerLayer. A MockWorkerTransport enables unit testing without real threads.
apex.experimental.workers.enabled boolean default: false
apex.experimental.workers.poolSize number default: 2 (min: 1, max: cpus-2)
Workers inherit the main process heap size (apex.environment.jsHeapSizeGB), debug flags (apex.debug, apex.debugPort), and profiling mode (apex.environment.profilingMode) automatically via WorkerExecArgvBuilder. Worker debug ports are auto-assigned and logged to the Output panel with role labels (e.g. [apex-worker-dataOwner]).
For full details see docs/adr-effect-worker-topology.md and the sequence diagrams in .session-files/diagrams/worker-architecture.md.
The Apex Language Server supports different operational modes optimized for different environments:
- Production Mode: Optimized for performance and stability in production environments
- Development Mode: Full feature set with enhanced debugging and development workflows
- Test Mode: Testing-specific features and configurations
The server mode can be configured through multiple methods:
- Environment Variable Override: Set
APEX_LS_MODE=productionorAPEX_LS_MODE=development - Extension Mode: Automatically determined based on VS Code extension mode
- NODE_ENV: Falls back to
NODE_ENVenvironment variable
For detailed information about server mode configuration and capabilities, see:
The apex-ls package provides a unified Apex Language Server that works across browser, Node.js, and web worker environments. It consolidates the functionality previously provided by separate browser and Node.js packages.
npm install @salesforce/apex-lsThe apex-lsp-testbed package provides a testbed for performance and qualitative analysis of different Apex language server implementations.
npm install @salesforce/apex-lsp-testbed- Node.js (latest LTS recommended)
- npm
# Clone the repository
git clone <repository-url>
cd apex-language-support
# Install dependencies
npm installTo build all packages, run the following command from the root of the repository:
# Build all packages
npm run compileOther useful commands for development include:
# Run all tests
npm run test
# Run all tests with coverage
npm run test:coverage
# Lint all packages
npm run lint
# Fix linting issues
npm run lint:fixTo build and package the VS Code extension (.vsix file), run the following command from the root of the repository:
# Build and package the VS Code extension
npm run package --workspace=apex-language-server-extensionThe packaged extension will be available in the packages/apex-lsp-vscode-extension directory.
This project includes comprehensive test coverage for all packages. Test coverage reports are generated using Jest and Istanbul.
# Run all tests with coverage
npm run test:coverage
# Generate a consolidated coverage report for the entire repository
npm run test:coverage:reportAfter running the test coverage commands, coverage reports are available:
- Package-level reports: Generated in each package's
coveragedirectory - Consolidated repository report: Generated in the root
coveragedirectory
The coverage reports include:
- HTML reports for interactive viewing (
coverage/lcov-report/index.html) - LCOV reports for CI integration
- Text summaries in the console
- JSON coverage data for further processing
Global coverage thresholds are set in the Jest configuration file:
- Statements: 50%
- Branches: 50%
- Functions: 50%
- Lines: 50%
These thresholds can be adjusted per package as needed.
This project uses GitHub Actions for continuous integration and automated releases. The workflows are designed to handle both VS Code extensions and NPM packages.
- Trigger: Push to
mainbranch or pull requests - Platforms: Ubuntu and Windows with Node.js 20.x and LTS versions
- Tasks: Linting, compilation, testing with coverage, and packaging
The nightly-extensions.yml workflow handles automated releases of VS Code extensions:
- Supported Extensions:
apex-lsp-vscode-extension(VS Code Marketplace and OpenVSX Registry)
- Triggers: Manual dispatch, workflow calls, or scheduled nightly builds
- Features:
- Smart change detection (only releases extensions with changes)
- Version bumping with even/odd minor version strategy for pre-releases
- Dry-run mode for testing release plans
- Support for multiple registries (VSCE, OVSX)
- Automated GitHub releases with VSIX artifacts
The release-npm.yml workflow handles automated releases of NPM packages:
- Supported Packages: All packages in the monorepo
- Triggers: Manual dispatch or workflow calls
- Features:
- Conventional commit-based version bumping
- Automated NPM publishing
- Change detection to avoid unnecessary releases
- Auto: Determined by conventional commit messages
- Manual: Patch, minor, or major bumps
- Pre-release: Uses odd minor versions (1.1.x, 1.3.x)
- Stable: Uses even minor versions (1.0.x, 1.2.x)
- Scheduled builds that create pre-release versions
- Use patch version bumps with nightly timestamps
- Automatically marked as pre-releases
To manually trigger a release:
-
VS Code Extensions:
# Go to Actions tab in GitHub # Select "Release VS Code Extensions" # Choose extensions, registries, and options # Set dry-run=true to test first
-
NPM Packages:
# Go to Actions tab in GitHub # Select "Release NPM Packages" # Choose packages and options
Both release workflows support dry-run mode for testing:
- Simulates the entire release process without making actual changes
- Shows what would be released and to which registries
- Displays version bump calculations
- Perfect for testing release configurations
The workflows require several tools and dependencies:
- Node.js 20.x: For building and packaging
- jq: For JSON processing (pre-installed on Ubuntu runners)
- GitHub CLI: For creating releases
- VSCE/OVSX: For publishing to marketplaces
Licensed under the BSD 3-Clause license. For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause