Rust PHP Language Server (LSP 3.17) with a VS Code extension.
php-lsp targets PHP 7.4-8.4 projects and provides indexed PHP language intelligence: diagnostics, hover, completion, navigation, references, rename, formatting integration, semantic tokens, hierarchy views, and built-in phpstorm-stubs support.
- Syntax diagnostics with incremental tree-sitter parsing.
- Semantic diagnostics for unknown classes, functions, imports, members, and duplicate workspace symbols.
- Member diagnostics for visibility, static/instance misuse, missing methods, missing properties, and missing class constants.
- Basic type compatibility checks for assignments, returns, arguments, properties, and member calls.
- Best-effort PHPDoc template metadata, PHPStan/Psalm type aliases and imported aliases, and inherited generic member type substitution for common repository and collection patterns, including foreach values from PHPDoc-generic collection returns.
- Call-site inference for PHPStan/Psalm conditional return types and
class-string<T>factory/service-locator patterns in hover, completion chains, local variable type inlay hints, and DoctrinegetRepository<T>()/ repositoryfind*hovers. - Shape-aware inference for PHPDoc
array{...}/object{...}and literal array shapes in completion and key navigation. - Closure and arrow-function parameter inference from
callable(...)signatures, including generic map/filter-style collection callbacks andarray_map-style helpers. - Framework-aware static providers for common Laravel string keys, Symfony Twig template names, and Symfony route names without booting the application.
- Blade-like and Symfony/Twig template documents use virtual PHP plus source maps for conservative hover, completion, definition, inlay hints, diagnostics, and semantic tokens in supported template expressions and control blocks.
- Override signature and PHP-version compatibility diagnostics.
- Optional PHPStan and Psalm diagnostics through configured external commands.
- Per-category diagnostic severity controls for unknown symbols, unused code, duplicate symbols, members, type compatibility, override signatures, and PHP-version checks.
- Test-friendly diagnostics for common PHPUnit patterns, including assertion helpers, test doubles, trait-based test helpers, anonymous classes, and closure/destructuring variable scopes.
- Hover for symbols, source-like PHP signatures, linked FQN/source metadata, class and method-level relation links, template/generic bindings, Symfony/Doctrine framework roles, indexed PHP 8 attributes, complete parameter lists, types, variables, PHPDoc summaries/descriptions, deprecation, and PHPDoc virtual members, plus call-site-specialized generic return sections with clickable class links where the target can be resolved.
- Completion for classes, interfaces, traits, enums, functions, constants,
methods, properties, variables, namespaces, keywords, snippets, PHPDoc virtual
members, shape keys/properties, framework string keys, template paths, and
auto-import edits; incomplete one-line
$object->expressions remain usable for completion while tree-sitter diagnostics still report the incomplete PHP. - Completion resolve enriches PHPDoc virtual member completions.
- Signature help for functions, methods, constructors, and active parameter tracking.
- Inlay hints for argument labels, inferred PHPDoc parameter/return types, useful inferred local variable types, and end-of-scope labels for methods and large blocks.
- Semantic tokens with full, delta, and range requests.
- Go to definition for indexed symbols, local variables,
$this, constructors, PHPDoc virtual members, PHPDoc/literal shape keys, static framework string keys, template paths, Symfony Twig route keys, and lazy vendor fallback. - Go to declaration for imports, with definition fallback.
- Go to type definition for inferred variables, members, function returns, and indexed symbol types.
- Go to implementation for interface/trait/base types and methods.
- Find references through indexed per-file references and same-scope local variable references.
- Document highlight for local variables and non-local symbols.
- Selection ranges based on the parsed AST.
- Linked editing for namespace/use alias edits.
- Document links for statically resolvable
include/requirepaths.
- Nested document symbols for namespaces, types, and members, including signatures and deprecation tags.
- Ranked workspace symbol search over the indexed workspace.
- Call hierarchy for functions, methods, constructors, incoming calls, and outgoing calls.
- Type hierarchy for classes, interfaces, traits, enums, supertypes, and subtypes.
- Rename for classes, functions, methods, properties, constants, and local variables.
- Prepare rename rejects unsupported or built-in targets before editing.
- Quick fixes to import unresolved classes/functions, remove unused imports, apply diagnostic replacement metadata, and optionally map PHPStan/Psalm findings to local fixes.
- Source action to organize imports.
- Quick fix to implement missing interface, abstract parent, and abstract trait methods while preserving PHPDoc, analyzer tags, attributes, visibility, static, params, defaults, and native-safe return types.
- Refactor actions to generate constructors and property getters/setters from indexed properties.
- Refactor actions to change member visibility and promote simple constructor assignments to constructor property promotion.
- Refactor action to synchronize PHPDoc
@paramand@returntags from function/method signatures while preserving richer analyzer-specific tags. - Refactor action to add return types from PHPDoc when supported by the target PHP version.
- Refactor actions to extract selected expressions to local variables, extract class-scope literals to constants, and inline simple same-block local variables.
- Heavy refactor edits use
codeAction/resolveso initial code-action requests stay lightweight. - Document formatting, range formatting, and on-type formatting through
auto-detected or configured external formatters (
pint,php-cs-fixer,phpcbf, or a custom command).
- Status bar popup with indexing status, file/percentage progress, symbol count, stubs information, active diagnostics/analyzers, formatter, include paths, and server binary details.
- Code lenses with reference counts.
- Folding ranges for PHP structures, comments, arrays, and blocks.
- Document formatting and range formatting through auto-detected or configured external tools.
- On-type indentation edits for newline, semicolon, and closing brace.
- Running
php-lspwithout a subcommand starts the LSP server on stdio. php-lsp init-configcreates a starter.php-lsp.tomlfile.php-lsp analyze [PATH]runs the same parser, workspace index, and built-in diagnostics pipeline from the command line.analyzesupports--project-root <DIR>,--severity <all|hint|info|warning|error>, and--format <table|json|github>.- Analyze output is available as a local table, stable JSON for scripts, or GitHub workflow annotations.
php-lsp fix [PATH] --dry-runpreviews safe native fixes without writing files.fixsupports repeated--rulevalues forunused-imports,organize-imports, andadd-return-type, plus--format <table|json>.
- Initialization options and runtime configuration updates through
workspace/didChangeConfiguration. - Composer autoload support for PSR-4, PSR-0, classmap, and files entries.
- Additional include and exclude paths from extension configuration.
- Built-in phpstorm-stubs bundle with configurable extension stubs.
- Lazy
vendor/indexing. - Multi-root workspace support.
- Watched PHP file changes and LSP file-operation notifications.
- Create/change/delete PHP file events reindex or remove symbols from the workspace index.
- Rename file notifications move indexed file state from old URI to new URI.
- Production validation was refreshed on 2026-05-28 after the IDE intelligence
milestone. It measures a primary 10k-file Symfony workspace and two
additional Laravel-like workspaces. Remaining GA work is tracked in
docs/production-risk-register.mdanddocs/production-baseline.md. - Workspace, stub, and lazy vendor file symbols are cached in separate disk namespaces with mtime, size, and content-hash validation. Lazy vendor files are persisted after the requested class is verified in the index; Composer vendor metadata is cached in memory with an LRU for lazy vendor symbols. The primary large-workspace warm cache target is met; installed-vendor first-hit behavior remains a watch item.
references,rename, and reference-count code lenses use indexed per-file references, but still iterate workspace reference sets and can be expensive on very large repositories.- Workspace indexing parses files through a bounded CPU-aware task queue; the
primary large-workspace indexing baseline is measured in
docs/production-baseline.md. - Heavy references/rename requests, background indexing, and external analyzers have cancellation coverage; some other heavy requests remain benchmark watch items.
- Rapid
didChangebursts still refresh parser/index state on each accepted edit, while diagnostics are debounced and version-checked. - Built-in stubs are configurable and version-filtered for supported phpstorm-stubs version-gating metadata. New metadata forms may require parser updates.
- Cross-file local variable analysis is intentionally limited; variable references and rename are local-scope oriented.
- Type inference includes common PHPDoc generic inheritance bindings,
class-string<T>call-site bindings, conditional return fallbacks, and Doctrine repository call-site bindings, class/file-scoped PHPStan/Psalm type aliases, callback parameter inference,Generator<TKey,TValue>foreach key/value inference, and best-effort PHPDoc/literal shapes, but it is still shallow compared with mature PHP static analyzers. - Built-in semantic diagnostics depend on indexed project and vendor symbols. If Composer/vendor metadata is absent, external framework classes can be reported as unknown; dynamic framework APIs such as some Eloquent relation members are best-effort.
- Template support is conservative. Blade-like and Twig documents are not full
template-engine implementations; diagnostics are best-effort and published
only for a small allowlist of exact source-mapped expression errors plus
conservative Twig delimiter/block syntax errors; generated virtual PHP,
incomplete/magic properties, and uncertain ranges stay suppressed. Complex
Twig expressions such as filters, tests,
in, functions, macros, ternaries, null coalescing, and dynamic/bracket attribute access are explicit best-effort backlog items; their full expression semantics are skipped rather than mapped to misleading PHP, while simple member chains and root variables inside those expressions can still be source-mapped for hover/completion/definition. Type-preserving Twig filters such assliceandfilterkeep the base collection available for foreach hover/completion/definition/inlay inference. Twig object completion also offers getter-derived property-style labels such asidforgetId(). Definition can still navigate static Twig template-path literals that exist undertemplates/, including HTML attribute values, and Symfonypath()/url()route keys backed by PHP 8#[Route(name: ...)]attributes. Twigforeachover Doctrine entity collections exposed through property-style access can infer item hover/completion/definition/inlay types from indexed ORMtargetEntityproperty metadata andadd*/remove*collection mutator signatures. Twig attribute access over PHPDoc and inferred array shapes can expose shape-key hover, completion, source-backed definition, and inlay types for patterns such asrow.messageLog, nestedconfig_params.sftp.port, and local{% set message_log = row.messageLog %}variables. Shape-key definitions point at the PHPDoc shape key or literal array key when the static context scanner has that source range. Twig context variables are inferred from staticrender(..., [...])call sites and literal-template helpers whose next argument is a context array, includingnew Class(), arrays of new objects, typed controller parameter variables, nullable locals assigned conditionally before render, indexed$this->service->method()return types, iterable repository method results, literal nested array shapes,$items[] = [...]append-built shapes, commonarray_values/array_filter/array_map/explode/preg_splitlist pipelines,compact('name')variables, Doctrine magicfind*/findOneBy*repository results, and Knp-style paginator variables backed by Doctrine repository/query-builder sources. One-level Twig{% include ... with {...} %}calls can pass those inferred caller variables into component templates, so included partials can resolve foreach item hover/completion/definition and inlay hints without hardcoded template names. Custom Doctrine repositories can be resolved from indexed@extends ServiceEntityRepository<Entity>PHPDoc or ORMrepositoryClassattributes without synchronous request-time source reads; render keys whose value type cannot be inferred are still seeded asmixedto avoid false undefined-variable diagnostics. Open Twig documents refresh those inferred context types after relevant PHP controller/render changes and workspace reindex events. - Diagnostics are optimized for editor feedback: file changes publish fast in-process diagnostics, while full diagnostics and optional external analyzer runs are used on open/save and reconfiguration.
- External PHPStan/Psalm diagnostics require those tools to be installed and configured by the workspace.
- Formatting is delegated to external tools; php-lsp auto-detects common Composer dev tools but does not implement or advertise a native PHP formatter provider.
The VS Code extension contributes these settings under phpLsp.*:
Runtime environment:
| Environment variable | Default | Description |
|---|---|---|
PHP_LSP_WORKER_THREAD_STACK_SIZE |
8388608 |
Tokio worker-thread stack size in bytes. Values below 1 MiB are ignored. Raising this can help unusually deep framework/type/template workloads, but the default is intended for normal editor use. |
| Setting | Default | Description |
|---|---|---|
phpLsp.enable |
true |
Enable the language server. |
phpLsp.phpVersion |
8.2 |
Target PHP version for diagnostics and version-aware refactors (7.4-8.4). |
phpLsp.serverPath |
"" |
Custom server binary path. Empty uses the bundled binary, then falls back to php-lsp from PATH if the bundled binary is missing. |
phpLsp.includePaths |
[] |
Additional relative or absolute directories/files to include in workspace indexing. |
phpLsp.excludePaths |
[] |
Relative or absolute directories/files to exclude from workspace indexing. |
phpLsp.stubs.extensions |
All available stubs | PHP stub extension set to index from the bundled stubs. Leave unset to discover all extension directories; set [] to disable stubs. |
phpLsp.composer.enabled |
true |
Enable composer.json autoload indexing. |
phpLsp.indexVendor |
true |
Index vendor/ lazily. |
phpLsp.diagnostics.mode |
basic-semantic |
off, syntax-only, or basic-semantic. |
phpLsp.diagnostics.severity |
Category warnings | Per-category severity for unknownSymbols, unused, duplicateSymbols, members, typeCompatibility, overrideSignatures, and phpVersion; values are off, error, warning, information, or hint. |
phpLsp.diagnostics.memberTypeNodeBudget |
512 |
Relevant AST-node budget for expensive member/type diagnostics per file. Set 0 to disable the cap. |
phpLsp.diagnostics.partialAnalysisDiagnostic |
true |
Publish an informational diagnostic when member/type diagnostics are skipped by the budget. |
phpLsp.allowProjectCommands |
false |
Trust executable analyzer and formatter settings from .php-lsp.toml. Keep disabled for untrusted workspaces. |
phpLsp.formatting.provider |
auto |
auto, none, pint, php-cs-fixer, phpcbf, or custom. |
phpLsp.formatting.command |
"" |
Custom formatter command; use {file} for the temporary PHP file. |
phpLsp.formatting.timeoutMs |
30000 |
External formatter timeout per request. |
phpLsp.phpstan.enabled |
false |
Enable PHPStan diagnostics. |
phpLsp.phpstan.command |
vendor/bin/phpstan ... {file} |
PHPStan command that prints JSON output. |
phpLsp.phpstan.timeoutMs |
30000 |
PHPStan timeout per file. |
phpLsp.psalm.enabled |
false |
Enable Psalm diagnostics. |
phpLsp.psalm.command |
vendor/bin/psalm ... {file} |
Psalm command that prints JSON output. |
phpLsp.psalm.timeoutMs |
30000 |
Psalm timeout per file. |
phpLsp.analyzerCodeActions.enabled |
false |
Enable opt-in quick fixes for PHPStan and Psalm diagnostics when diagnostic metadata is available. |
phpLsp.trace.server |
off |
LSP transport trace: off, messages, or verbose. |
phpLsp.logLevel |
info |
Server log level: error, warn, info, debug, or trace. |
Shared project defaults can also be stored in .php-lsp.toml. Use
php-lsp init-config to create a default file without overwriting an existing
one. Config precedence is built-in defaults, global config, project config, then
explicit VS Code settings. Executable analyzer and formatter settings from
project config are ignored unless phpLsp.allowProjectCommands is enabled in
VS Code or allowProjectCommands = true is set in global php-lsp config. See
Configuration.
Example external diagnostics setup:
{
"phpLsp.phpstan.enabled": true,
"phpLsp.phpstan.command": "vendor/bin/phpstan analyse --error-format=json --no-progress --no-interaction {file}",
"phpLsp.psalm.enabled": true,
"phpLsp.psalm.command": "vendor/bin/psalm --output-format=json --no-progress {file}",
"phpLsp.analyzerCodeActions.enabled": true
}When the same analyzer settings are stored in project .php-lsp.toml, php-lsp
requires explicit workspace-command trust before it executes them. Prefer VS
Code user/workspace settings or global php-lsp config for commands in
repositories you do not fully trust.
Example external formatting setup:
{
"phpLsp.formatting.provider": "php-cs-fixer"
}Formatter resolution order:
- Explicit
phpLsp.formatting.*settings, global php-lsp config, or trusted.php-lsp.toml[formatting]values. - Composer metadata auto-detection from
require-dev/require:laravel/pint,friendsofphp/php-cs-fixer, thensquizlabs/php_codesniffer. - No formatting provider when no explicit provider or supported Composer tool is available.
External formatter commands are timeout-bound and cancelled when the document changes, closes, or a newer formatting request supersedes the old one. Range formatting stays conservative: php-lsp formats only the selected fragment via a temporary file and never silently formats the whole document for a range request.
php-lsp analyze [PATH] --project-root <DIR> --severity warning --format table
php-lsp fix [PATH] --dry-run --project-root <DIR> --rule unused-imports --format jsonPATH can be a PHP file or directory. When it is omitted, php-lsp analyzes the
effective project root. CLI commands load the same global/project
.php-lsp.toml configuration used by the language server for PHP version,
diagnostic mode/severity, Composer discovery, and include/exclude paths.
Analyze exit codes:
| Code | Meaning |
|---|---|
0 |
No diagnostics at the requested severity. |
1 |
Execution or configuration error. |
2 |
Diagnostics were found. |
Output formats:
| Format | Use |
|---|---|
table |
Human-readable local output. |
json |
Stable machine-readable report with schemaVersion, summary, and diagnostics. |
github |
GitHub Actions workflow annotations. |
Fix dry-run mode:
php-lsp fixcurrently requires--dry-runand refuses to write files.- Without
--rule, it runs the preferred safe native fixers: unused imports and PHPDoc-derived return types that can be represented as native PHP return types for the configured PHP version. --rulecan be repeated. Supported values areunused-imports,organize-imports, andadd-return-type.- Exit code
0means no edits would be produced,1means execution or configuration error, and2means edits would be produced. - The fix command does not run project formatters.
- CI and local example scripts are documented in CLI And CI Usage.
The extension contributes these VS Code commands:
| Command palette title | Command ID | Behavior |
|---|---|---|
PHP: Show Language Server Status |
phpLsp.showStatus |
Opens the status quick pick with indexing, cache, stubs, diagnostics, formatter, analyzer, and server-binary details. |
PHP: Show Language Server Version |
phpLsp.showServerVersion |
Shows the initialized server name/version plus resolved binary, platform, stubs, cache roots, and last startup errors. |
PHP: Restart Language Server |
phpLsp.restartServer |
Restarts the client/server process and reuses the existing disk cache. |
PHP: Clear PHP LSP Cache and Restart |
phpLsp.clearCacheAndRestart |
Deletes cache directories for current workspace roots and discovered Composer roots, then restarts the server. |
- Architecture: server/client data flow, indexing, cache model, diagnostics pipeline, and runtime configuration behavior.
- Configuration:
.php-lsp.tomldiscovery, precedence, schema, and examples. - CLI and CI usage: GitHub Actions reporting and local CLI examples.
- LSP feature matrix: supported, partial, and unsupported LSP behavior.
- Performance guide: baseline methodology, profiling commands, cache interpretation, and production acceptance metrics.
- Production baseline: current measured validation and performance numbers.
- Production risk register: tracked production gaps and exit signals.
- Check
PHP: Show Language Server StatusorPHP: Show Language Server Versionfor the resolved server binary path, source, platform target, and last startup error. - If
phpLsp.serverPathis set, verify that it points to an executablephp-lspbinary. - If using the bundled binary, verify that your platform is one of
linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64, orwin32-arm64. - If the bundled binary is absent and
phpLsp.serverPathis empty, the client triesphp-lspfromPATHand logs the selected source in the LSP output channel. - Set
"phpLsp.logLevel": "debug"and"phpLsp.trace.server": "messages"for more output.
- Use
PHP: Show Language Server Statusto inspect indexed file count, cache path, stubs path, include/exclude paths, and analyzer settings. - Add generated directories to
phpLsp.excludePaths. - Keep
phpLsp.indexVendorenabled for lazy vendor lookup, but exclude very large generated vendor subtrees if they are not useful. - Use
PHP: Clear PHP LSP Cache and Restartwhen changing branches, Composer metadata, stubs, or project layout and the disk cache looks stale.
- Ensure Composer metadata and
vendor/composer/installed.jsonare available when you want built-in diagnostics to resolve external framework symbols. - Set
"phpLsp.diagnostics.mode": "syntax-only"to keep only parser syntax diagnostics, plus conservative Twig syntax diagnostics for Twig documents. - Set
"phpLsp.diagnostics.mode": "off"to disable built-in diagnostics. - If a large file reports partial analysis, raise
"phpLsp.diagnostics.memberTypeNodeBudget"or set it to0to run member/type diagnostics without that cap. - Prefer per-category severity controls when only one category is noisy:
{
"phpLsp.diagnostics.severity": {
"members": "off",
"unused": "hint"
}
}- Enable the analyzer explicitly with
phpLsp.phpstan.enabledorphpLsp.psalm.enabled. - If the analyzer is enabled only in
.php-lsp.toml, also enablephpLsp.allowProjectCommandsafter you trust the workspace. - Make sure the configured command works from the workspace root and prints JSON.
- Keep
{file}in the command template unless the tool should receive the file path appended at the end. - Increase
phpLsp.phpstan.timeoutMsorphpLsp.psalm.timeoutMsfor slow projects.
- With the default
autoprovider, make sure Composerrequire-devincludeslaravel/pint,friendsofphp/php-cs-fixer, orsquizlabs/php_codesniffer. - Set
phpLsp.formatting.providerexplicitly topint,php-cs-fixer,phpcbf, orcustomto bypass auto-detection; set it tononeto disable formatting. - If a formatter command or executable provider is configured only in
.php-lsp.toml, enablephpLsp.allowProjectCommandsafter you trust the workspace. - For
custom, configurephpLsp.formatting.commandand include{file}where the temporary PHP file path should be inserted. - Ensure the formatter executable is available from the workspace root.
- Increase
phpLsp.formatting.timeoutMsif the external formatter times out on large files.
- Server: Rust (tokio + tower-lsp-server + tree-sitter-php)
- Client: VS Code extension (TypeScript + vscode-languageclient)
- Transport: stdio (JSON-RPC 2.0)
- Rust 1.85+ (
rustup update stable) - Node.js 20+ and npm
- Git (for submodules)
make # build server + client + stubs → .vsix
make install # build + install extension into VS Code
make check # run stubs, Rust, and TypeScript checks
make check-stubs # verify source and bundled phpstorm-stubs integritymake uses the host Rust target detected from rustc -vV, builds a release
server binary into client/bin/<platform>/, bundles phpstorm-stubs into
client/stubs/, builds the TypeScript extension, and packages a .vsix.
Available targets:
| Command | Description |
|---|---|
make / make all / make package |
Full build: server + client + stubs → .vsix |
make install |
Build and install .vsix into VS Code |
make server |
Build a release Rust binary for the detected host platform and copy it to client/bin/<platform>/ |
make server-all |
Cross-compile server binaries for all configured targets |
make package-all |
Universal .vsix with all configured platform binaries |
make client |
npm ci + build extension JS |
make stubs |
Init submodule + bundle phpstorm-stubs |
make check-stubs |
Verify source and bundled phpstorm-stubs have enough PHP files and required core stubs |
make check |
Stubs integrity, lint, and tests |
make test |
Run Rust tests |
make lint |
cargo fmt --check, clippy, tsc --noEmit |
make fmt |
Auto-format Rust code |
make release |
Read VERSION, patch package/Cargo versions, commit, force-update the release tag, and push |
make clean |
Remove all build artefacts |
Stubs submodule (server/data/stubs) is pulled automatically on first build if not initialized.
scripts/bundle-stubs.sh, make check-stubs, CI, and release packaging fail
if source or bundled stubs are missing, too small, or lack required core files.
make server-all and make package-all use scripts/build-server.sh --all
for these VS Code platform directories:
linux-x64linux-arm64darwin-x64darwin-arm64win32-x64win32-arm64
Published Linux binaries are built from the GNU targets
(*-unknown-linux-gnu). Alpine/musl is not part of the universal VSIX release
target set.
make release requires a clean working tree, reads the semver value from
VERSION, updates client/package.json, client/package-lock.json,
server/Cargo.toml, and server/Cargo.lock, commits those version changes
when needed, creates or updates tag v<VERSION>, then pushes main and the
tag to GitHub. Build the universal package with make package-all before
publishing release artefacts. The GitHub release workflow also publishes the
packaged extension to VS Marketplace using the VSCE_PAT repository secret.
cd server
cargo build --releasecd client
npm ci
npm run build# 1. Build server binary for current platform → client/bin/<platform>/
./scripts/build-server.sh
# 2. Bundle phpstorm-stubs → client/stubs/
./scripts/bundle-stubs.sh
# Optional: verify source and bundled stubs before packaging
make check-stubs
# 3. Package VSIX
cd client
npx @vscode/vsce package --no-dependencies./scripts/build-server.sh x86_64-unknown-linux-gnu # specific target
./scripts/build-server.sh --all # configured targetsphp-lsp/
├── Makefile # Build automation
├── server/ # Rust LSP server (Cargo workspace)
│ ├── data/stubs/ # phpstorm-stubs (git submodule)
│ └── crates/
│ ├── php-lsp-server/ # Main binary
│ ├── php-lsp-parser/ # tree-sitter PHP wrapper
│ ├── php-lsp-index/ # Symbol index
│ ├── php-lsp-completion/ # Completion engine
│ └── php-lsp-types/ # Shared types
├── client/ # VS Code extension (TypeScript)
├── images/ # README and marketplace media
├── scripts/ # Build helpers (build-server.sh, bundle-stubs.sh)
└── test-fixtures/ # Test PHP projects
MIT