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
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
# PHPantom — Blade

This document is the implementation plan for Laravel Blade template
support in PHPantom. For Eloquent model support see `laravel.md`.
For general architecture see `ARCHITECTURE.md`.

---

## Philosophy

- **No application booting.** Consistent with `laravel.md`. We
  never run PHP or boot a Laravel application.
- **No call-site scanning.** We do not scan controllers, mailers, or
  other PHP files for `view()` calls to infer template variable types.
  Variable types come from explicit `@var` PHPDoc in `@php` blocks
  (compatible with Bladestan's `@bladestan-signature`), `@props`
  directives, or component class constructors.
- **Discovery is just directory walks.** Scanning `resources/views/`
  and `app/View/Components/` (plus `app/Livewire/`) at init time is
  the full extent of external Blade file discovery. Paths are converted
  to view names and component names via string transforms.
- **PSR-4 is for class source lookup, not discovery.** Once we know an
  FQN (e.g. `App\View\Components\Alert`), we use the existing
  `find_or_load_class` pipeline to read its source. We do not use
  PSR-4 to discover component names.
- **Graceful degradation.** Unknown directives become comments. Failed
  component resolution produces comments. The user always gets partial
  completions rather than a broken file. The preprocessor must never
  produce invalid PHP.

---

## Overview

Blade templates (`.blade.php`) mix HTML, Blade directives, component
tags (`<x-alert>`, `<livewire:counter>`), and embedded PHP. The
mago-syntax parser only understands pure PHP. The strategy:

1. Preprocess `.blade.php` files into valid PHP.
2. Feed the virtual PHP through the existing pipeline (parser,
   resolver, completion, definition).
3. Map LSP response positions back to the original Blade file via a
   source map.

---

## Phase 1: Blade-to-PHP Preprocessor

This phase delivers the core value: completion and go-to-definition
for PHP expressions inside Blade templates.

### 1. New module `src/blade/`

Create the following files:

- `src/blade/mod.rs` — module declarations, `is_blade_file()` helper,
  public `preprocess()` entry point.
- `src/blade/preprocessor.rs` — the main Blade-to-PHP transformation
  engine.
- `src/blade/directives.rs` — directive pattern matching and their PHP
  translations.
- `src/blade/source_map.rs``BladeSourceMap`, `ColumnAdjustment`,
  and position translation functions.

Register `pub mod blade;` in `src/lib.rs`.

### 2. Preprocessor design

The preprocessor operates line-by-line and produces exactly one output
line per input line. This is the single most important invariant: it
makes line mapping a trivial identity function so only column
adjustments are needed.

The virtual PHP document starts with an implicit `<?php` prologue
(occupying a synthetic first line if the Blade file doesn't start with
one). Each Blade line is then transformed:

- **Blade directives** become single-line PHP statements.
- **Echo expressions** (`{{ }}`, `{!! !!}`) are replaced inline.
- **HTML-only lines** become single-line PHP comments (`// HTML`).
- **Component tags** are handled in Phase 2.

#### 2a. Echo expressions

| Blade | Virtual PHP |
|---|---|
| `{{ $expr }}` | `echo e($expr);` |
| `{!! $expr !!}` | `echo ($expr);` |
| `{{-- comment --}}` | `/* comment */` |

When echo expressions appear mid-line (surrounded by HTML), the
surrounding HTML becomes a comment on the same line and the echo
expression is emitted as PHP on that line. Multiple echo expressions
on the same line are joined with `;` on the same output line.

The `@{{ expr }}` escape syntax (used for JS frameworks) strips the
leading `@` and emits a comment.

#### 2b. Control directives

| Blade | Virtual PHP |
|---|---|
| `@if ($cond)` | `if ($cond):` |
| `@elseif ($cond)` | `elseif ($cond):` |
| `@else` | `else:` |
| `@endif` | `endif;` |
| `@foreach ($arr as $v)` | `foreach ($arr as $v):` |
| `@endforeach` | `endforeach;` |
| `@for (...)` | `for (...):` |
| `@endfor` | `endfor;` |
| `@while (...)` | `while (...):` |
| `@endwhile` | `endwhile;` |
| `@forelse ($arr as $v)` | `foreach ($arr as $v):` |
| `@empty` | `endforeach; if (false):` |
| `@endforelse` | `endif;` |
| `@switch($v)` | `switch($v):` |
| `@case(...)` | `case (...):` |
| `@break` | `break;` |
| `@default` | `default:` |
| `@endswitch` | `endswitch;` |
| `@unless (...)` | `if (!( ... )):` |
| `@endunless` | `endif;` |
| `@isset($v)` | `if (isset($v)):` |
| `@endisset` | `endif;` |
| `@empty($v)` (directive) | `if (empty($v)):` |
| `@endempty` | `endif;` |

#### 2c. Directives that expose implicit variables

| Blade | Virtual PHP |
|---|---|
| `@session('key')` | `if (true): $value = '';` |
| `@endsession` | `endif;` |
| `@context('key')` | `if (true): $value = '';` |
| `@endcontext` | `endif;` |
| `@error('field')` | `if (true): $message = '';` |
| `@enderror` | `endif;` |

#### 2d. Stub directives

These are parsed but produce no semantic PHP effect:

- `@auth`/`@endauth`, `@guest`/`@endguest`, `@env(...)`/`@endenv`,
  `@production`/`@endproduction`, `@once`/`@endonce`  `if (true):` / `endif;`
- `@csrf`, `@method(...)`, `@push`/`@endpush`, `@prepend`/`@endprepend`,
  `@stack(...)`, `@yield(...)`, `@section(...)`/`@endsection`/`@show`,
  `@extends(...)`, `@include(...)` and variants, `@includeIf(...)`,
  `@includeWhen(...)`, `@includeUnless(...)`, `@includeFirst(...)`,
  `@each(...)``/* @directive */`

#### 2e. PHP blocks

| Blade | Virtual PHP |
|---|---|
| `@php ... @endphp` | Raw PHP content (unwrap the delimiters) |
| `@use('App\Models\Flight')` | `use App\Models\Flight;` |
| `@use('App\Models\Flight', 'F')` | `use App\Models\Flight as F;` |
| `@use('App\Models\{A, B}')` | `use App\Models\{A, B};` |

#### 2f. Service injection

`@inject('metrics', 'App\Services\MetricsService')` →
`$metrics = new \App\Services\MetricsService();`

This gives the variable the correct type for completion.

#### 2g. Inline attribute directives

`@class([...])`, `@style([...])`, `@checked(...)`, `@selected(...)`,
`@disabled(...)`, `@readonly(...)`, `@required(...)` → `echo (...);`
so their PHP expressions get parsed.

#### 2h. Verbatim regions

`@verbatim ... @endverbatim` content becomes PHP comments (it
contains JS template syntax that would confuse the parser).

#### 2i. Unknown directives

Any `@directive(...)` not recognized becomes `/* @directive(...) */`.

### 3. Source map

Because the preprocessor maintains one output line per input line, the
`BladeSourceMap` only stores per-line column adjustments:

```rust
pub struct BladeSourceMap {
    /// Per-line column adjustment regions.
    /// Lines with no adjustments have an empty vec (identity mapping).
    adjustments: Vec<Vec<ColumnAdjustment>>,
}

pub struct ColumnAdjustment {
    pub blade_col_start: u32,
    pub blade_col_end: u32,
    pub php_col_start: u32,
    pub php_col_end: u32,
}
```

Functions on `BladeSourceMap`:

- `blade_to_php(line, col) -> (line, col)` — translate a position
  in the original Blade file to the virtual PHP.
- `php_to_blade(line, col) -> (line, col)` — translate a position
  in the virtual PHP back to the original Blade file.

### 4. New fields on `Backend`

```rust
/// Virtual PHP content generated from Blade files.
/// Maps file URI -> preprocessed PHP source.
pub(crate) blade_virtual_content: Arc<Mutex<HashMap<String, String>>>,

/// Source maps from virtual PHP back to original Blade positions.
/// Maps file URI -> BladeSourceMap.
pub(crate) blade_source_maps: Arc<Mutex<HashMap<String, BladeSourceMap>>>,
```

Add these to `Backend::defaults()` with empty `HashMap`s.

### 5. Wire into the LSP pipeline

#### 5a. `is_blade_file()` helper

A public function in `src/blade/mod.rs`:

```rust
pub fn is_blade_file(uri: &str) -> bool {
    uri.ends_with(".blade.php")
}
```

The server should also check `languageId == "blade"` in `did_open`
when available (some editors send this for Blade files).

#### 5b. `did_open` / `did_change` in `server.rs`

After storing the file content in `open_files`, check
`is_blade_file(&uri)`. If true:

1. Run `blade::preprocess(&content)` which returns
   `(virtual_php: String, source_map: BladeSourceMap)`.
2. Store the virtual PHP in `blade_virtual_content`.
3. Store the source map in `blade_source_maps`.
4. Call `self.update_ast(&uri, &virtual_php)` (not the original
   content).

If the file is not Blade, the existing path is unchanged.

#### 5c. `completion` in `handler.rs`

At the top of `handle_completion`, after reading `content` from
`open_files`, check if this is a Blade file. If so:

1. Read the virtual PHP from `blade_virtual_content` (this is what
   gets parsed and resolved against).
2. Read the source map from `blade_source_maps`.
3. Translate the cursor `position` from Blade coordinates to virtual
   PHP coordinates using `source_map.blade_to_php()`.
4. Run the normal completion pipeline against the virtual PHP content
   with the translated position.
5. Translate result positions in `CompletionItem` text edits back to
   Blade coordinates using `source_map.php_to_blade()`.

#### 5d. `goto_definition` in `server.rs`

Same pattern: translate the incoming position to virtual PHP
coordinates before resolution, translate the result location back
if it points into the same Blade file.

#### 5e. `did_close` in `server.rs`

Clean up `blade_virtual_content` and `blade_source_maps` entries
for the closed file.

### 6. Implicit variables

The preprocessor injects these declarations at the top of the virtual
PHP (after the `<?php` prologue):

```php
/** @var \Illuminate\Support\ViewErrorBag $errors */
$errors = new \Illuminate\Support\ViewErrorBag();
/** @var \Illuminate\View\Factory $__env */
$__env = new \Illuminate\View\Factory();
```

Inside `@foreach` blocks, inject a `$loop` variable declaration
immediately after the `foreach` opening:

```php
/** @var object{index: int, iteration: int, remaining: int, count: int, first: bool, last: bool, even: bool, odd: bool, depth: int, parent: ?object} $loop */
$loop = (object)[];
```

### 7. Explicit type declarations via `@var` PHPDoc

Users declare variable types in their Blade templates by writing:

```blade
@php
/**
 * @bladestan-signature
 * @var string $name
 * @var \App\Models\User $user
 */
@endphp
```

The `@php ... @endphp` block becomes raw PHP. The `@var` tags are
standard PHPDoc that mago-syntax already parses. The
`@bladestan-signature` marker is just a comment we ignore. This works
out of the box with the preprocessor and is compatible with the
Bladestan ecosystem. No special handling needed.

### 8. Tests

Create `tests/blade_preprocessor.rs` with unit tests:

- Echo expressions: `{{ $var }}`, `{!! $html !!}`, `{{-- comment --}}`
- Each control directive (if/else/endif, foreach, for, while, switch,
  forelse, unless, isset, empty)
- Directives with implicit vars (@error, @session, @context)
- Stub directives (@auth, @guest, @csrf, etc.)
- PHP blocks (@php/@endphp, @use)
- Service injection (@inject)
- Inline attribute directives (@class, @checked, etc.)
- Verbatim regions
- Unknown directives
- Mixed lines (HTML with embedded `{{ }}`)
- Line count preservation (critical invariant)

Create `tests/completion_blade.rs` with integration tests:

- `$this->` inside a `@php` block
- `$var->` where `$var` is declared via `@var` PHPDoc
- `$user->` inside a `@foreach` with typed collection
- `$loop->` inside a `@foreach`
- `$errors->` (implicit variable)
- `$value` inside `@session` block
- `$message` inside `@error` block

---

## Phase 2: Component Support

### 9. Template and component file discovery

At `initialized` time (alongside PSR-4 and classmap loading), scan
the filesystem to build three maps.

New file: `src/blade/discovery.rs`

#### 9a. View name map

Recursively scan `resources/views/` for `*.blade.php` files. Build
a map of dot-notation view names to file paths:

- `resources/views/users/index.blade.php` → `"users.index"`
- `resources/views/components/alert.blade.php` → `"components.alert"`

Store as:

```rust
/// View dot-name -> file path.
pub(crate) blade_views: Arc<Mutex<HashMap<String, PathBuf>>>,
```

#### 9b. Class-based component map

Recursively scan `app/View/Components/` for `*.php` files. Convert
file paths to kebab-case component names and FQNs:

- `app/View/Components/Alert.php` → name `"alert"`,
  FQN `"App\\View\\Components\\Alert"`
- `app/View/Components/Forms/Input.php` → name `"forms.input"`,
  FQN `"App\\View\\Components\\Forms\\Input"`

Index components (where directory name matches file name) should be
registered both ways:

- `app/View/Components/Card/Card.php` → name `"card"` (index) and
  `"card.card"` (explicit)

Store as:

```rust
/// Component kebab-name -> FQN.
pub(crate) blade_components: Arc<Mutex<HashMap<String, String>>>,
```

#### 9c. Livewire component map

Recursively scan `app/Livewire/` for `*.php` files. Convert file
paths to dot-notation component names and FQNs:

- `app/Livewire/Counter.php` → name `"counter"`,
  FQN `"App\\Livewire\\Counter"`
- `app/Livewire/Admin/Users.php` → name `"admin.users"`,
  FQN `"App\\Livewire\\Admin\\Users"`

Store as:

```rust
/// Livewire component name -> FQN.
pub(crate) livewire_components: Arc<Mutex<HashMap<String, String>>>,
```

#### 9d. Workspace root dependency

All three scans depend on `workspace_root`. Run them in `initialized`
after the existing Composer parsing, gated on
`workspace_root.is_some()`.

### 10. `<x-component>` tag parsing in preprocessor

New file: `src/blade/components.rs`

The preprocessor detects `<x-name ...>` and `</x-name>` tags and
converts them to PHP.

#### 10a. Opening tags

Parse `<x-component-name attr="val" :attr="$expr" ...>` or
`<x-component-name ... />` (self-closing).

1. Extract the component name (everything between `<x-` and the first
   whitespace or `>`/`/>`).
2. Look up the name in `blade_components`. If found, resolve the FQN.
3. Extract attributes:
   - `attr="literal"` → named arg with string value
   - `:attr="$expr"` → named arg with PHP expression value
   - `::attr="expr"` → ignored (Alpine.js passthrough)
   - Bare `attr` → named arg with `true`
   - `:$var` (short syntax) → named arg `var: $var`
4. Convert attribute names from kebab-case to camelCase for the
   constructor call.
5. Emit `$component = new \FQN(camelAttr: value, ...);`

If the component is not found in `blade_components`, check if it's an
anonymous component (exists in `blade_views` under `components.`
prefix). For anonymous components, emit a comment but still expose
`$attributes` and `$slot`.

For `<x-dynamic-component :component="$name" ...>`, emit
`echo $name;` so the expression gets parsed, but do not try to
resolve a target component.

#### 10b. Closing tags

`</x-name>` becomes a comment: `/* /x-name */`

#### 10c. Named slots

`<x-slot:title>` → `$title = new \Illuminate\Support\HtmlString('');`
`</x-slot>` → comment

#### 10d. Implicit component variables

When inside a component tag region (between opening and closing tags),
inject:

```php
/** @var \Illuminate\View\ComponentAttributeBag $attributes */
$attributes = new \Illuminate\View\ComponentAttributeBag([]);
/** @var \Illuminate\Support\HtmlString $slot */
$slot = new \Illuminate\Support\HtmlString('');
```

### 11. `<livewire:component>` tag parsing

Parse `<livewire:name :attr="$expr" ...>` or
`<livewire:name ... />`.

1. Extract the component name (everything between `<livewire:` and
   the first whitespace or `>`/`/>`).
2. Look up in `livewire_components`. If found, resolve the FQN.
3. Extract attributes (same rules as `<x-...>`).
4. Emit `$component = new \FQN();` followed by property assignments
   for each attribute: `$component->attrName = $expr;`.

Livewire attribute names use camelCase on the class, so apply the
same kebab-to-camelCase conversion.

### 12. `@props` and `@aware`

#### 12a. `@props`

`@props(['type' => 'info', 'message'])` becomes:

```php
$type = 'info';
$message = null;
/** @var \Illuminate\View\ComponentAttributeBag $attributes */
$attributes = new \Illuminate\View\ComponentAttributeBag([]);
/** @var \Illuminate\Support\HtmlString $slot */
$slot = new \Illuminate\Support\HtmlString('');
```

The preprocessor parses the array literal in the `@props()`
argument to extract variable names and default values. Variables
listed without a key-value pair (just `'message'`) get a `null`
default.

#### 12b. `@aware`

`@aware(['color' => 'gray'])` → `$color = 'gray';`

Same parsing as `@props` but without the `$attributes`/`$slot`
injection.

### 13. Component and view name completion

#### 13a. `<x-` completion

When the user types `<x-` in a Blade file, offer completions from:

- `blade_components` map (class-based components, kebab-case names)
- Anonymous component templates: entries in `blade_views` whose key
  starts with `"components."`, with the prefix stripped and dots
  preserved (e.g. `"components.forms.input"``"forms.input"`)

Detection: check if the characters before the cursor match
`<x-` (possibly with a partial name typed). This is a Blade-level
context check done before the normal PHP completion pipeline.

Items should use `CompletionItemKind::Module` or `::Class` depending
on whether they're anonymous or class-backed.

#### 13b. `<livewire:` completion

Same pattern. When the user types `<livewire:`, offer completions
from the `livewire_components` map.

#### 13c. `@include('` and `@extends('` view name completion

When the cursor is inside the string argument to `@include`,
`@includeIf`, `@includeWhen`, `@includeUnless`, `@includeFirst`,
`@extends`, `@each`, or a `view()` function call, offer completions
from the `blade_views` map (dot-notation view names).

Detection: look for `@include('`, `@extends('`, or `view('` before
the cursor and check that the cursor is inside the quotes. The
trigger characters `'` and `"` are already registered.

#### 13d. Component attribute completion

When the cursor is inside a `<x-component ` tag (after the component
name, before `>` or `/>`), resolve the component class and offer its
constructor parameter names as kebab-case attribute completions.

Offer both plain and `:` prefixed variants:
- `message` (string literal)
- `:message` (PHP expression)

For Livewire components, offer the class's public property names as
attribute completions.

### 14. Tests

Create `tests/blade_components.rs`:

- `<x-alert>` resolves to `App\View\Components\Alert`
- `<x-forms.input>` resolves to `App\View\Components\Forms\Input`
- `<x-card>` resolves to index component
  `App\View\Components\Card\Card`
- `<livewire:counter>` resolves to `App\Livewire\Counter`
- Anonymous component detection
- `<x-dynamic-component>` does not crash
- Attribute parsing: string, expression, Alpine passthrough, bare,
  short syntax

Extend `tests/completion_blade.rs`:

- `<x-` triggers component name completions
- `<livewire:` triggers Livewire component name completions
- `@include('` triggers view name completions
- `<x-alert ` triggers attribute completions
- `$component->` after component instantiation
- `$attributes->` in component templates

---

## Phase 3: Cross-File View Intelligence

### 15. Go-to-definition for view names and components

#### 15a. View name go-to-definition

Inside `@include('users.index')`, `@extends('layouts.app')`, or
`view('welcome')`:

1. Extract the view name string at the cursor position.
2. Look up in `blade_views`.
3. Return a `Location` pointing to the resolved file.

#### 15b. Component tag go-to-definition

On `<x-alert>`:

1. Extract the component name.
2. Look up in `blade_components` to get the FQN.
3. Use `find_or_load_class` + `class_index` / `classmap` to find the
   source file.
4. Return a `Location` pointing to the class definition.

On `<livewire:counter>`:

1. Same pattern using `livewire_components`.

### 16. Signature merging for `@extends`

When template A contains `@extends('layouts.app')`:

1. Resolve `layouts.app` via `blade_views` to a file path.
2. Read or preprocess that file.
3. Extract `@var` declarations from its `@php` blocks.
4. Merge those declarations into template A's virtual PHP prologue,
   following the Bladestan covariance model:
   - Variables only in child: use child type.
   - Variables only in parent: use parent type.
   - Variables in both: child may narrow but not widen.
   - Walk the chain recursively if the parent also `@extends`.

This gives child templates access to the parent's declared
variables without the user redeclaring them.

### 17. Component class to template variable typing

For class-based components, when editing the component's Blade
template:

1. Determine which component class backs this template. Convention:
   `resources/views/components/alert.blade.php` is backed by
   `App\View\Components\Alert`.
2. Load the class via `find_or_load_class`.
3. Read public properties and constructor parameter types.
4. Inject those as `@var` declarations in the virtual PHP prologue
   (unless the template already has explicit `@var` or `@props`).

### 18. Tests

Create `tests/definition_blade.rs`:

- Go-to-definition on `@include('users.index')` → view file
- Go-to-definition on `@extends('layouts.app')` → layout file
- Go-to-definition on `<x-alert>` → component class
- Go-to-definition on `<livewire:counter>` → Livewire class

Extend `tests/completion_blade.rs`:

- Variables from parent layout available in child via `@extends`
- Component class constructor types available in template

---

## Phase 4: Blade Directive Completion

### 19. Directive name completion

When the user types `@` in a Blade file (outside `{{ }}`, `@php`
blocks, and string literals), offer completions for all known Blade
directives with snippet templates.

Each completion inserts a snippet with tab stops:

```
@if ($1)
    $0
@endif
```

```
@foreach ($1 as $2)
    $0
@endforeach
```

```
@include('$1')
```

```
@props([$1])
```

```
@inject('$1', '$2')
```

```
@php
$0
@endphp
```

Detection: The `@` trigger character is already registered. In
`handle_completion`, check `is_blade_file` and that the cursor is in
an HTML/directive context (not inside `{{ }}`, not inside a `@php`
block, not inside a string literal).

### 20. Tests

Extend `tests/completion_blade.rs`:

- `@` triggers directive name completions
- `@if` partial triggers filtered directive completions
- No directive completion inside `{{ }}` or `@php` blocks

---

## Implementation Sequence

The items below are ordered for incremental delivery. Each step
produces a testable, shippable improvement.

### Step 1: Preprocessor skeleton (items 1-3)

Create `src/blade/mod.rs`, `preprocessor.rs`, `directives.rs`,
`source_map.rs`. Implement the `preprocess()` function that handles
echo expressions and all control/stub directives. Write the
`BladeSourceMap` with `blade_to_php()` and `php_to_blade()`.

**Deliverable:** A pure function
`preprocess(blade_source: &str) -> (String, BladeSourceMap)` with
comprehensive unit tests proving line count preservation and correct
PHP output.

### Step 2: Wire into LSP (items 4-5)

Add `blade_virtual_content` and `blade_source_maps` to `Backend`.
Modify `did_open`, `did_change`, `did_close`, `completion`, and
`goto_definition` in `server.rs` to route Blade files through the
preprocessor and translate positions.

**Deliverable:** Completion works for `$var->` inside `{{ }}` and
`@php` blocks in `.blade.php` files.

### Step 3: Implicit variables (item 6)

Inject `$errors`, `$__env`, and `$loop` declarations. Handle
`@session`/`@error`/`@context` implicit `$value`/`$message`.

**Deliverable:** `$errors->`, `$loop->first`, and `$value` inside
`@session` blocks all produce completions.

### Step 4: Discovery (item 9)

Implement `src/blade/discovery.rs`. Scan `resources/views/`,
`app/View/Components/`, `app/Livewire/` at init time. Add the three
new maps to `Backend`.

**Deliverable:** Maps are populated and logged at startup.

### Step 5: Component tag parsing (items 10-12)

Implement `src/blade/components.rs`. Parse `<x-...>` and
`<livewire:...>` tags. Handle `@props`, `@aware`, named slots.

**Deliverable:** `$component->` after `<x-alert>` produces
completions from the Alert class. `$attributes->` works in component
templates.

### Step 6: Name completions (item 13)

Implement `<x-`, `<livewire:`, `@include('`, and component attribute
completions.

**Deliverable:** Typing `<x-` shows available components. Typing
`@include('` shows available views. Typing attributes inside
`<x-alert ` shows constructor parameter names.

### Step 7: Directive completion (item 19)

Implement `@` directive name completion with snippets.

**Deliverable:** Typing `@` in a Blade file shows all known
directives with snippet templates.

### Step 8: Cross-file intelligence (items 15-17)

Implement go-to-definition for view names and component tags.
Implement `@extends` signature merging. Implement component class to
template variable typing.

**Deliverable:** Ctrl-click on `@include('users.index')` jumps to
the file. Parent layout variables are available in child templates.

---

## Editor Integration Notes

### File extension detection

The server activates Blade preprocessing when:
- The URI ends with `.blade.php`, OR
- The `languageId` in `did_open` is `"blade"`.

### Zed extension

The Zed extension (`zed-extension/extension.toml`) currently
registers `languages = ["PHP"]`. To support Blade files, it will
need an additional language registration. This may require Zed to
have a Blade language definition (grammar, file associations), or
the extension can register `.blade.php` as a PHP variant. This is
an editor-side concern and may need a separate Zed extension or an
update to the existing one.

### Other editors

- **VS Code:** Extensions like Laravel Blade Snippets set
  `languageId` to `"blade"`. PHPantom's VS Code integration would
  need to register for both `"php"` and `"blade"` language IDs.
- **Neovim:** `lspconfig` can be configured to send `.blade.php`
  files to PHPantom with the correct `languageId`.