phpantom_lsp 0.7.0

Fast PHP language server with deep type intelligence. Generics, Laravel, PHPStan annotations. Ready in an instant.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
# PHPantom — LSP Features

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) |

---

## F2. Partial result streaming via `$/progress`

**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.

### Streaming between existing phases

`find_implementors` already runs five sequential phases (see
`docs/ARCHITECTURE.md` § Go-to-Implementation):

1. **Phase 1 — ast_map** (already-parsed classes in memory) — essentially
   free. Flush results immediately.
2. **Phase 2 — class_index** (FQN → URI entries not yet in ast_map) —
   loads individual files. Flush after each batch.
3. **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).
4. **Phase 4 — embedded stubs** (string pre-filter → lazy parse) — flush
   after stubs are checked.
5. **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.

### Prioritising user code within Phase 3

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.

### Granularity options

- **Per-phase batches** (simplest) — one `$/progress` notification 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.

### Applicable requests

| 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 |

### Implementation sketch

1. Check whether the client sent a `partialResultToken` in the request
   params.
2. If yes, create a `$/progress` sender. After each scan phase (or
   per-file, depending on granularity), send a
   `ProgressParams { token, value: [items...] }` notification.
3. Return `null` as the final response.
4. If no token was provided, fall back to the current behaviour: collect
   everything, return once.

---

## F3. Incremental text sync

**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:**

1. **Change the capability** — set `text_document_sync` to
   `TextDocumentSyncKind::INCREMENTAL` in `ServerCapabilities`.

2. **Apply diffs** — in the `did_change` handler, apply each
   `TextDocumentContentChangeEvent` to the stored file content string.
   The events contain a `range` (start/end position) and `text`
   (replacement). Convert positions to byte offsets and splice.

3. **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.

---

## F4. Return type and closure parameter type inlay hints

**Impact: Medium · Effort: Medium**

PHPantom's inlay hints currently show **parameter names** and
**by-reference indicators** at call sites. Two additional hint kinds
would bring PHPantom to parity with Devsense and ahead of Intelephense:

### Return type hints

Show an inferred return type hint after the closing parenthesis of
functions, methods, closures, and arrow functions that lack an explicit
return type declaration:

```php
function doubled($x)  // → : int
{
    return $x * 2;
}

$fn = fn($x) => $x * 2;  // → : int
```

The hint should only appear when the return type can be inferred from
the function body (or from the callable context for closures). Functions
that already have a native return type hint or a `@return` docblock
should not receive a hint.

Ideally, clicking the hint (or double-clicking, depending on editor
support) should insert the return type declaration as a text edit.

### Closure / arrow function parameter type hints

Show an inferred type hint after untyped closure and arrow function
parameters when the type can be inferred from the callable context:

```php
$users->map(fn($u) => $u->name);
//            ^ : User

$filtered = array_filter($items, function ($item) { ... });
//                                         ^ : Item
```

The hint should only appear when the parameter has no native type hint
and the type is inferred from the enclosing callable signature (e.g.
a `Closure(User): bool` parameter type, or a `@param` on the receiving
function). Parameters that already have a type hint should not receive
a hint.

### What to avoid

- **Variable type hints at assignment sites.** Phpactor shows these
  (e.g. `$x` `: string` after every assignment). This is noisy in
  practice and clutters the editor. Do not add this kind.
- **End-of-block labels.** Phpactor shows `// class Foo` or
  `// method bar` at closing braces. This is an editor feature (most
  editors already show sticky scroll or breadcrumbs) and would add
  visual noise. Do not add this kind.

---

## F5. Call hierarchy

**Impact: Medium · Effort: Medium**

Implement `callHierarchy/incomingCalls` and
`callHierarchy/outgoingCalls` to answer "who calls this function?" and
"what does this function call?"

### Incoming calls (who calls this)

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.

### Outgoing calls (what does this call)

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.

### Prepare

`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.

### Dependencies

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.

## F6. Machine-readable CLI output formats

**Impact: Medium · Effort: Low**

Add a `--format` flag to `analyze` and `fix` that controls the output
format. The default remains the current human-readable table.

### Formats

- **`github`** — Emit
  [workflow commands]https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message
  (`::warning file=...::message`) so diagnostics appear as inline
  annotations on pull request diffs. This is the highest-priority
  format because GitHub Actions is the most common CI environment for
  PHP projects.
- **`json`** — One JSON object per diagnostic (or a top-level array).
  Enables integration with custom dashboards, editor plugins, and
  other tooling that wants to consume PHPantom output programmatically.

### Implementation

The output logic in `analyse.rs` and `fix.rs` currently writes
directly to stderr/stdout with ANSI formatting. Extract the rendering
behind a trait or enum so each format can be selected at the call
site. The `--no-colour` flag becomes redundant for non-table formats
but should continue to work for the default table output.

---

## F7. Evaluatable expression support (DAP integration)

**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.

### Supported expression kinds

- **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.

### Why this is cheap

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.

### What this enables

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.

---

## F8. Test ↔ implementation navigation via `@covers`

**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.

### Why not path-based mapping

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.

### From test → subject

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.

### From subject → test

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.php` in 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 `@covers`
  annotations 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.

### Exposure

- **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.

### Dependencies

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).

---

## F9. Namespace renaming

**Impact: Medium · Effort: Medium**

Extend the rename handler (`textDocument/rename`) to support renaming
PHP namespaces. When the user triggers a rename on a namespace segment
in a `namespace` declaration or a `use` statement, PHPantom should
produce a `WorkspaceEdit` that updates:

1. The `namespace` declaration in every file that belongs to the
   renamed namespace (and its children).
2. All `use` statements across the project that reference classes,
   functions, or constants under the renamed namespace.
3. Fully-qualified name references (`\Old\Namespace\Class`) in
   docblocks, type hints, and string literals (class-string context).
4. PSR-4 directory structure: include `RenameFile` resource operations
   in the `WorkspaceEdit` to move files and directories so the
   filesystem stays consistent with the new namespace. Intelephense
   already does this: renaming a namespace emits `RenameFile` ops
   that move files to match the new PSR-4 path. PHPantom should do
   the same by default when a PSR-4 mapping exists in
   `composer.json`.

### Scope

Renaming applies to a single namespace segment. For example, renaming
`Bar` in `App\Bar\Service` affects `App\Bar\*` but not `App\Baz\*`.
Renaming the root segment (`App`) would rename everything under
`App\*`.

### Bidirectional support

Phpactor supports the inverse direction: moving a class file to a new
directory and updating the namespace declaration and all references to
match. PHPStorm also supports "Move class with full reference update."
PHPantom should eventually support both directions:

- **Rename namespace → move files** (this task). The user renames a
  namespace segment and PHPantom moves the corresponding directory.
- **Move file → fix namespace** (future, separate task). The user
  moves a file in the file manager and PHPantom updates the
  `namespace` declaration and all `use` statements to match the new
  PSR-4 path. Phpactor exposes this as a "Fix namespace / class name"
  code action. This direction can be tracked separately once F9
  ships.

### Edge cases

- Namespace aliases (`use App\Old as X`) should preserve the alias
  and only update the FQN portion.
- Group use declarations (`use App\Old\{Foo, Bar}`) need the shared
  prefix updated.
- Files with multiple namespace declarations (rare but legal in PHP)
  should only rename the matching namespace block.
- Inline FQNs in `@param`, `@return`, `@var`, `@throws`, `@see`,
  `@extends`, `@implements`, and `@template` docblock tags.

### Implementation

1. Detect that the cursor is on a namespace name token (in a
   `namespace` declaration or `use` statement).
2. Resolve the full namespace being renamed and the segment being
   changed.
3. Use the existing cross-file scanning infrastructure (classmap +
   PSR-4 walk) to find all files that reference the namespace.
4. Generate text edits for each affected location.
5. Look up the PSR-4 autoload mapping in `composer.json` to determine
   the corresponding directory. If a mapping exists, include
   `RenameFile` resource operations in the `WorkspaceEdit` to move
   the directory (and all files beneath it) to the new path.

---

## F10. Linked editing ranges

**Impact: Medium · Effort: Low**

Implement the `textDocument/linkedEditingRange` LSP request so that
renaming one occurrence of a paired token simultaneously updates the
other. The primary use case in PHP is matching opening and closing
tags in HTML-embedded PHP, but the most immediately useful case for
pure PHP is variable renaming within a scope.

### Use cases

1. **Variables within a scope.** Place the cursor on `$userName` and
   every other occurrence of `$userName` in the same function or
   method body enters linked editing mode. Typing a new name updates
   all occurrences simultaneously without triggering a full rename
   request. This is faster and more fluid than `textDocument/rename`
   for local renames.

2. **Matching PHP open/close tags.** When editing `<?php ... ?>` in
   mixed HTML/PHP files, linked editing could pair the tags. Lower
   priority since most PHP files use only an opening tag.

### Implementation

1. **Register the capability.** Advertise
   `linkedEditingRangeProvider: true` in the server capabilities
   during `initialize`.

2. **Handle the request.** When the client sends
   `textDocument/linkedEditingRange`, determine what the cursor is
   on:
   - **Variable name:** use the scope collector to find all
     occurrences of that variable in the enclosing scope. Return
     their ranges. Set `wordPattern` to `\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*`
     so the editor knows to include the `$` prefix in the linked
     edit region.
   - If the cursor is not on a supported token, return `null`.

3. **Leverage existing infrastructure.** The scope collector
   (`scope_collector/mod.rs`) already tracks variable definition and
   read sites with byte offsets. The document highlight handler
   (`highlight/mod.rs`) already finds all occurrences of a variable
   in a scope and returns their ranges. The linked editing handler
   is essentially the same logic but returning a
   `LinkedEditingRanges` response instead of `DocumentHighlight[]`.

### Constraints

- All returned ranges must be on the same line or the client may
  reject them (some clients impose this restriction, though the spec
  does not). In practice, variable occurrences span multiple lines,
  and modern clients (VS Code, Zed) handle multi-line linked editing
  correctly.
- The response must not include ranges that overlap or that would
  produce invalid syntax when edited together. Since all ranges are
  the same variable name, this is naturally satisfied.

---

## F11. VS Code extension

| 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.

### Approach

Fork the [vscode-intelephense](https://github.com/bmewburn/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.

### Scope

1. **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.
2. **Settings surface.** Expose PHPantom's `.phpantom.toml` settings
   as VS Code settings (PHP version, diagnostics toggles, indexing
   strategy).
3. **Status bar.** Show indexing progress and server status.
4. **Marketplace listing.** Icon, description, screenshots,
   categories, keywords.
5. **CI.** GitHub Actions workflow to build, test, and publish the
   extension on release.

### Code signing

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`, and
  `notarytool` in the release CI workflow.
- **Windows:** Authenticode certificate (or Azure Trusted Signing)
  and `signtool` in the release CI workflow.

---

## F12. IntelliJ / PHPStorm plugin

| Field      | Value                    |
| ---------- | ------------------------ |
| **Impact** | High                     |
| **Effort** | Medium (2-5 days)        |

Create an IntelliJ plugin that depends on
[LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) and
bundles PHPantom. Publish it to the JetBrains Marketplace. Works in
all IntelliJ-based IDEs (PHPStorm, IntelliJ IDEA, WebStorm, etc.).

### Approach

Fork [clojure-lsp-intellij](https://github.com/clojure-lsp/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 `PHP` language and file type via
  `com.redhat.devtools.lsp4ij.languageMapping`.
- Bundle or auto-download the PHPantom binary.
- Add a settings page for the binary path and any PHPantom-specific
  options.

### Scope

1. **`plugin.xml` registration.** Server definition, language
   mapping, file type mapping (`.php`, `.phtml`, `.inc`).
2. **Binary management.** Auto-download from GitHub Releases on
   first run, with a manual path override in settings.
3. **Settings UI.** Binary path, PHP version override, diagnostic
   toggles.
4. **JetBrains Marketplace listing.** Icon, description, plugin
   compatibility range (2024.2+, matching lsp4ij's requirement).
5. **CI.** GitHub Actions workflow using `gradlew buildPlugin` and
   `gradlew publishPlugin`.

### Why not use the built-in IntelliJ LSP API

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.

---

## F13. Homebrew formula

| 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`.

### Approach

Submit a PR to [homebrew-core](https://github.com/Homebrew/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).

### Formula contents

- **Homepage:** `https://github.com/AJenbo/phpantom_lsp`
- **Source:** GitHub Releases tarball or `cargo install` from crates.io.
- **Binary:** `phpantom_lsp`
- **Test block:** `system bin/"phpantom_lsp", "--version"`

### Why this matters

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).

---

## F14. Helix upstream PR

| Field      | Value                    |
| ---------- | ------------------------ |
| **Impact** | Low-Medium               |
| **Effort** | Low (≤ 1 day)            |

**Depends on:** F13 (Homebrew formula).

Submit a PR to the [Helix editor](https://github.com/helix-editor/helix)
adding `phpantom_lsp` as a language server option in the default
`languages.toml`.

### Change

Add a `phpantom` server definition and include it in the `php`
language entry (alongside `intelephense`):

```toml
[language-server.phpantom]
command = "phpantom_lsp"

# In the [[language]] entry for php, add "phpantom" to language-servers.
```

### Prerequisites

- 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.