Items are ordered by impact (descending), then effort (ascending) within the same impact tier.
| Label | Scale |
|---|---|
| Impact | Critical, High, Medium-High, Medium, Low-Medium, Low |
| Effort | Low (≤ 1 day), Medium (2-5 days), Medium-High (1-2 weeks), High (2-4 weeks), Very High (> 1 month) |
Impact: Medium · Effort: Medium-High
The LSP spec (3.17) allows requests that return arrays — such as
textDocument/implementation, textDocument/references,
workspace/symbol, and even textDocument/completion — to stream
incremental batches of results via $/progress notifications when both
sides negotiate a partialResultToken. The final RPC response then
carries null (all items were already sent through progress).
This would let PHPantom deliver the first useful results almost instantly instead of blocking until every source has been scanned.
find_implementors already runs five sequential phases (see
docs/ARCHITECTURE.md § Go-to-Implementation):
- Phase 1 — ast_map (already-parsed classes in memory) — essentially free. Flush results immediately.
- Phase 2 — class_index (FQN → URI entries not yet in ast_map) — loads individual files. Flush after each batch.
- Phase 3 — classmap files (Composer classmap, user + vendor mixed) — iterates unique file paths, applies string pre-filter, parses matches. This is the widest phase and the best candidate for within-phase streaming (see below).
- Phase 4 — embedded stubs (string pre-filter → lazy parse) — flush after stubs are checked.
- Phase 5 — PSR-4 directory walk (user code only, catches files not in the classmap) — disk I/O + parse per file, good candidate for per-file streaming.
Each phase boundary is a natural point to flush a $/progress batch,
so the editor starts populating the results list while heavier phases
are still running.
Phase 3 iterates the Composer classmap, which contains both user and
vendor entries. Currently they are processed in arbitrary order. A
simple optimisation: partition classmap file paths into user paths
(under PSR-4 roots from composer.json autoload / autoload-dev)
and vendor paths (everything else, typically under vendor/), then
process user paths first. This way the results most relevant to the
developer arrive before vendor matches, even within a single phase.
- Per-phase batches (simplest) — one
$/progressnotification at each of the five phase boundaries listed above. - Per-file streaming — within Phases 3 and 5, emit results as each file is parsed from disk instead of waiting for the entire phase to finish. Phase 3 can iterate hundreds of classmap files and Phase 5 recursively walks PSR-4 directories, so per-file flushing would significantly improve perceived latency for large projects.
- Adaptive batching — collect results for a short window (e.g. 50 ms) then flush, balancing notification overhead against latency.
| Request | Benefit |
|---|---|
textDocument/implementation |
Already scans five phases; each phase's matches can be streamed |
textDocument/references |
Will need full-project scanning; streaming is essential |
workspace/symbol |
Searches every known class/function; early batches feel instant |
textDocument/completion |
Less critical (usually fast), but long chains through vendor code could benefit |
- Check whether the client sent a
partialResultTokenin the request params. - If yes, create a
$/progresssender. After each scan phase (or per-file, depending on granularity), send aProgressParams { token, value: [items...] }notification. - Return
nullas the final response. - If no token was provided, fall back to the current behaviour: collect everything, return once.
Impact: Low-Medium · Effort: Medium
PHPantom uses TextDocumentSyncKind::FULL, meaning every
textDocument/didChange notification sends the entire file content.
Switching to TextDocumentSyncKind::INCREMENTAL means the client sends
only the changed range (line/column start, line/column end, replacement
text), reducing IPC bandwidth for large files.
The practical benefit is bounded: Mago requires a full re-parse of the file regardless of how the change was received, so the saving is purely in the data transferred over the IPC channel. For files under ~1000 lines this is negligible. For very large files (5000+ lines, common in legacy PHP), sending 200KB on every keystroke can become noticeable.
Implementation:
-
Change the capability — set
text_document_synctoTextDocumentSyncKind::INCREMENTALinServerCapabilities. -
Apply diffs — in the
did_changehandler, apply eachTextDocumentContentChangeEventto the stored file content string. The events contain arange(start/end position) andtext(replacement). Convert positions to byte offsets and splice. -
Re-parse — after applying all change events, re-parse the full file with Mago as today. No incremental parsing needed initially.
Relationship with partial result streaming (F2): These two features address different performance axes. Incremental text sync reduces the cost of inbound data (client to server per keystroke). Partial result streaming (F2) reduces the perceived latency of outbound results (server to client for large result sets). They are independent and can be implemented in either order, but if both are planned, incremental text sync is lower priority because full-file sync is rarely the bottleneck in practice. Partial result streaming has a more immediate user-visible impact for go-to-implementation, find references, and workspace symbols on large codebases.
Impact: Medium · Effort: Medium
Implement callHierarchy/incomingCalls and
callHierarchy/outgoingCalls to answer "who calls this function?" and
"what does this function call?"
Given a function or method, find all call sites across the project. This is conceptually similar to Find References but filtered to call expressions and structured as a tree (each caller is itself a callable with a location).
The existing Find References infrastructure
(find_references_in_file, cross-file scanning) provides the core
search. The call hierarchy handler wraps the results into
CallHierarchyIncomingCall items, grouping by containing function.
Given a function or method, walk its AST body and collect all call
expressions (function calls, method calls, static calls, new
expressions). Resolve each callee to its declaration location.
This is a single-file AST walk with cross-file resolution for each callee, similar to what go-to-definition already does.
callHierarchy/prepare returns a CallHierarchyItem for the symbol
at the cursor. This is straightforward: resolve the symbol, return its
name, kind, URI, range, and selection range.
Call hierarchy benefits significantly from a full project index. Without an index, incoming calls can only be found via the existing classmap + PSR-4 scan approach (same as Find References). With a full index (X4), the lookup becomes a simple index query.
Consider implementing after X4 (full background indexing) ships, or accept the same scan-based latency that Find References currently has.
Impact: Low-Medium · Effort: Low
Implement textDocument/evaluatableExpression so debuggers (Xdebug
via DAP) can evaluate expressions under the cursor during a debug
session. Given a cursor position, the handler returns the expression
text and range that the debugger should evaluate in the running PHP
process.
- Variables:
$var— return the variable name and its span. - Property access:
$obj->prop,$this->prop— return the full member access expression. - Array access:
$arr[0],$arr['key']— return the full subscript expression including brackets. - Static property access:
Foo::$bar— return the full expression. - Parameters: function/method parameters at declaration sites.
The symbol map already identifies all of these constructs with precise
byte ranges. The handler is a thin layer: look up the SymbolSpan at
the cursor position, check that it's a variable, member access, or
subscript expression, and return the source text and range. No type
resolution needed.
When a user is debugging PHP with Xdebug and hovers over $user->name
in their editor, the editor asks the LSP "what expression is here?"
and forwards it to the debug adapter for evaluation. Without this
handler, the editor falls back to selecting the word under the cursor,
which gives name instead of $user->name — useless for the
debugger.
Impact: Low · Effort: Medium
Provide bidirectional navigation between a test class and the class it
tests, using PHPUnit's @covers / @coversClass / #[CoversClass]
annotations as the linking mechanism.
Pattern-based approaches (e.g. src/Foo.php → tests/FooTest.php)
assume a project follows a specific directory convention. Many projects
don't: tests may live under tests/Feature/, tests/Functional/,
or in a completely separate directory structure. The @covers tag is
an explicit, project-layout-independent link that works for any
structure.
When the cursor is in a test class, look for:
@covers \App\Service\UserService(docblock on class or method)@coversClass(\App\Service\UserService::class)(PHPUnit 10+)#[CoversClass(UserService::class)](PHP 8 attribute, PHPUnit 10+)
Resolve the referenced class name via the standard class loader and navigate to its definition. This can be exposed as a code lens ("Go to subject") or a code action, or both.
Given a class, find test classes that reference it in @covers /
@coversClass / #[CoversClass]. This requires scanning test files
for the annotation. Two approaches:
- Lazy scan: When the user invokes "find tests" on a class, scan
files matching
*Test.phpin the project for@covers/#[CoversClass]referencing the current class FQN. This is O(n) in test file count but test directories are typically small. - Indexed: If full background indexing (X4) ships, index
@coversannotations during the indexing pass and look them up in O(1).
The lazy approach is fine for most projects. Test directories rarely
exceed a few hundred files, and a simple memchr-based string
pre-filter on the class name before parsing keeps it fast.
- Code lens on test classes: "Subject: UserService" (clickable, navigates to the subject class).
- Code lens on subject classes: "Tests: UserServiceTest" (clickable, navigates to the test).
- Code action: "Go to test" / "Go to subject" when the cursor is on the class name.
No hard dependencies. Works with the existing class loader for the test → subject direction. The subject → test direction benefits from but does not require full indexing (X4).
| Field | Value |
|---|---|
| Impact | High |
| Effort | Medium (2-5 days) |
Create a VS Code extension that bundles PHPantom and publishes it to the VS Code Marketplace.
Fork the vscode-intelephense
client extension (MIT-licensed). Intelephense is the #1 PHP extension
in the VS Code Marketplace, so its package.json represents what
PHP developers expect from an extension: the settings schema,
activation events, file associations, categories, and contribution
points are battle-tested. Starting from this base means we do not
accidentally omit something users take for granted.
Strip the proprietary Intelephense server dependency (intelephense
npm package) and replace it with PHPantom binary management. The
extension is a thin TypeScript wrapper around vscode-languageclient
that spawns phpantom_lsp over stdio.
Cleanup process: After forking, compare the result against a
fresh VS Code extension scaffold (yo code generator) to identify
and remove Intelephense-specific legacy that does not apply to
PHPantom (licence key commands, telemetry integration, Node.js
runtime configuration, premium feature gating). The goal is a clean
extension that inherits the right UX expectations without carrying
over implementation baggage.
- Binary distribution. Bundle or auto-download the correct pre-built binary for each platform (linux-x64, linux-arm64, darwin-x64, darwin-arm64, win-x64). Use GitHub Releases as the download source.
- Settings surface. Expose PHPantom's
.phpantom.tomlsettings as VS Code settings (PHP version, diagnostics toggles, indexing strategy). - Status bar. Show indexing progress and server status.
- Marketplace listing. Icon, description, screenshots, categories, keywords.
- CI. GitHub Actions workflow to build, test, and publish the extension on release.
macOS and Windows builds must be signed so the OS stops flagging PHPantom as malware. This is a prerequisite for the VS Code extension (users will not trust an extension that triggers Gatekeeper or SmartScreen warnings).
- macOS: Apple Developer ID certificate,
codesign, andnotarytoolin the release CI workflow. - Windows: Authenticode certificate (or Azure Trusted Signing)
and
signtoolin the release CI workflow.
| Field | Value |
|---|---|
| Impact | High |
| Effort | Medium (2-5 days) |
Create an IntelliJ plugin that depends on LSP4IJ and bundles PHPantom. Publish it to the JetBrains Marketplace. Works in all IntelliJ-based IDEs (PHPStorm, IntelliJ IDEA, WebStorm, etc.).
Fork clojure-lsp-intellij
(MIT-licensed). It is a Kotlin/Gradle plugin that registers a
language server via lsp4ij's com.redhat.devtools.lsp4ij.server
extension point. Strip the Clojure-specific parts and replace them
with PHPantom:
- Register PHPantom as the language server in
plugin.xml. - Map the
PHPlanguage and file type viacom.redhat.devtools.lsp4ij.languageMapping. - Bundle or auto-download the PHPantom binary.
- Add a settings page for the binary path and any PHPantom-specific options.
plugin.xmlregistration. Server definition, language mapping, file type mapping (.php,.phtml,.inc).- Binary management. Auto-download from GitHub Releases on first run, with a manual path override in settings.
- Settings UI. Binary path, PHP version override, diagnostic toggles.
- JetBrains Marketplace listing. Icon, description, plugin compatibility range (2024.2+, matching lsp4ij's requirement).
- CI. GitHub Actions workflow using
gradlew buildPluginandgradlew publishPlugin.
IntelliJ's native LSP support (since 2023.2) is only available in Ultimate editions and is still limited in capability. LSP4IJ is free, works in all editions (including Community), and supports a broader set of LSP features. Using lsp4ij also means the plugin works in IntelliJ IDEA (for PHP projects opened there) and other JetBrains IDEs, not just PHPStorm.
| Field | Value |
|---|---|
| Impact | Medium |
| Effort | Low (≤ 1 day) |
Create a Homebrew formula for PHPantom so users on macOS and Linux
can install it with brew install phpantom_lsp.
Submit a PR to homebrew-core
with a formula that downloads the pre-built binary from GitHub
Releases for the current platform. Alternatively, the formula can
build from source using cargo install if the Homebrew reviewers
prefer source builds (common for Rust projects).
- Homepage:
https://github.com/AJenbo/phpantom_lsp - Source: GitHub Releases tarball or
cargo installfrom crates.io. - Binary:
phpantom_lsp - Test block:
system bin/"phpantom_lsp", "--version"
A Homebrew formula is a prerequisite for upstream PRs to editors like Helix, which prefer that language servers be installable via a package manager. It also simplifies the VS Code extension's binary management on macOS (detect Homebrew-installed binary before downloading).
| Field | Value |
|---|---|
| Impact | Low-Medium |
| Effort | Low (≤ 1 day) |
Depends on: F13 (Homebrew formula).
Submit a PR to the Helix editor
adding phpantom_lsp as a language server option in the default
languages.toml.
Add a phpantom server definition and include it in the php
language entry (alongside intelephense):
[language-server.phpantom]
command = "phpantom_lsp"
# In the [[language]] entry for php, add "phpantom" to language-servers.- F13 (Homebrew formula) should be merged so Helix maintainers can
point users at
brew install phpantom_lsp. - Helix maintainers may want a brief README section documenting the server and its feature set.
Impact: Low-Medium · Effort: Low
Implement textDocument/declaration to jump from a concrete method to
its abstract or interface prototype, complementing the existing
go-to-definition (which jumps to the concrete implementation) and
go-to-implementation (which jumps from an interface to concrete classes).
When the cursor is on a method call or method name:
- Search for an interface or abstract class that declares a method with the same name and is in the inheritance chain of the resolved class.
- If found, jump to the interface/abstract method declaration.
- If no abstract prototype exists, fall back to the same result as go-to-definition.
The existing resolve_implementation already does reverse lookups
(concrete → prototype) via resolve_reverse_implementation. The
declaration handler can reuse this: for MemberAccess and
MemberDeclaration symbols, call the reverse-implementation resolver
first. For class-level symbols, declaration and definition are the
same.
Register declaration_provider in server.rs and wire it to a thin
handler that delegates to the existing infrastructure.
Impact: Low · Effort: Low
Extend the existing on-type formatting handler (currently triggered on
\n for docblock generation) to also trigger on }, automatically
de-indenting the closing brace to match its opening {.
When the user types }:
- From the
}position, scan backward through the document text to find the matching{(tracking brace depth, skipping strings and comments). - Read the indentation of the line containing the matching
{. - If the
}line has more indentation than the{line, return aTextEditthat replaces the leading whitespace on the}line with the{line's indentation.
This is a pure text-based operation — no AST needed. Register } as
an additional on_type_formatting_trigger_character alongside the
existing \n.
Impact: Medium · Effort: Medium-High
Move a class file to a new location and update all references across
the project (namespace declaration, use statements, FQN references).
PHPantom currently supports file rename on class rename but not the
full move-with-reference-update workflow.
The operation needs to:
- Accept a source file and a destination path.
- Compute the new namespace from the destination path using the PSR-4 autoload map.
- Update the namespace declaration in the moved file.
- Find all references to the class across the project (use statements, FQN occurrences, docblock type strings).
- Rewrite each reference to use the new FQN, or update the
usestatement and leave short names unchanged.
References:
- Phpactor:
MoveClassrefactoring in the class-mover package.
Impact: Medium · Effort: Low
When a class's namespace or name does not match its file path per PSR-4 mapping, offer a code action (or command) to fix the namespace and/or class name. The inverse direction (rename file on class rename) is already supported.
The code action should:
- Resolve the expected namespace and class name from the file path
using the PSR-4 autoload map in
composer.json. - If the current namespace differs, offer "Fix namespace to
App\Models\Foo". - If the class name differs from the filename, offer "Fix class name
to
Foo".
References:
- Phpactor:
FixNamespaceClassNamecode action.