Skip to content

Commit 775146a

Browse files
committed
Add linked editing ranges for variables within definition regions
1 parent 5774950 commit 775146a

14 files changed

Lines changed: 939 additions & 82 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ PHPantom focuses on deep type intelligence. Here's how it compares:
2020
| Common LSP features<sup>1</sup> ||||||
2121
| Workspace symbols | 🚧 |||||
2222
| Semantic tokens ||||||
23-
| Linked editing | |||||
23+
| Linked editing | |||||
2424
| Extras<sup>2</sup> || 💰 | 🚧 | 🚧 ||
2525
| **Diagnostics** | | | | | |
2626
| PHPStan integration |||| 🚧 | 🚧 |

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **Linked editing ranges.** Place the cursor on a variable and all occurrences within its definition region (from one assignment to the next) enter linked editing mode. Typing a new name updates every occurrence simultaneously without affecting reassigned uses of the same variable name. Ranges exclude the leading `$` sigil so that typing before a variable (e.g. wrapping it in a function call) does not propagate to other occurrences.
1213
- **Invalid class-like kind diagnostics.** Flags class-like names used in positions where their kind is guaranteed to fail at runtime: `new` on abstract classes, interfaces, traits, or enums; `extends` on a final class, interface, or trait; `implements` with a non-interface; trait `use` with a non-trait; `instanceof` with a trait (always false); `catch` with a non-Throwable type or trait; and traits in native type-hint positions. Severity follows PHP semantics: unconditional fatal errors are Error, runtime-conditional failures are Warning.
1314
- **Nested array shape inference from multi-level key assignments.** Assignments like `$b['a']['b'] = 'x'` now produce a nested array shape type (`array{a: array{b: string}}`), enabling array key completion and hover for arrays built incrementally with nested keys. Previously only single-level key assignments were tracked.
1415

docs/todo.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,10 @@ within the same impact tier.
2121

2222
# Scheduled Sprints
2323

24-
## Sprint 4 — Refactoring toolkit & type inference
25-
26-
| # | Item | Impact | Effort |
27-
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------ |
28-
29-
30-
| | **Release 0.7.0** | | |
31-
3224
## Sprint 5 — Polish for office adoption
3325

3426
| # | Item | Impact | Effort |
3527
| --- | ---------------------------------------------------------------------------------------------------------------------------- | ----------- | ------ |
36-
| F10 | [Linked editing ranges](todo/lsp-features.md#f10-linked-editing-ranges) | Medium | Low |
3728
| A36 | [Import all missing classes](todo/actions.md#a36-import-all-missing-classes) (bulk import) | Medium | Low |
3829
| F6 | [Machine-readable CLI output formats](todo/lsp-features.md#f6-machine-readable-cli-output-formats) (`--format github\|json`) | Medium | Low |
3930
| D4 | [Unused variable diagnostic](todo/diagnostics.md#d4-unused-variable-diagnostic) | Medium | Medium |

docs/todo/lsp-features.md

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -443,66 +443,6 @@ PHPantom should eventually support both directions:
443443

444444
---
445445

446-
## F10. Linked editing ranges
447-
448-
**Impact: Medium · Effort: Low**
449-
450-
Implement the `textDocument/linkedEditingRange` LSP request so that
451-
renaming one occurrence of a paired token simultaneously updates the
452-
other. The primary use case in PHP is matching opening and closing
453-
tags in HTML-embedded PHP, but the most immediately useful case for
454-
pure PHP is variable renaming within a scope.
455-
456-
### Use cases
457-
458-
1. **Variables within a scope.** Place the cursor on `$userName` and
459-
every other occurrence of `$userName` in the same function or
460-
method body enters linked editing mode. Typing a new name updates
461-
all occurrences simultaneously without triggering a full rename
462-
request. This is faster and more fluid than `textDocument/rename`
463-
for local renames.
464-
465-
2. **Matching PHP open/close tags.** When editing `<?php ... ?>` in
466-
mixed HTML/PHP files, linked editing could pair the tags. Lower
467-
priority since most PHP files use only an opening tag.
468-
469-
### Implementation
470-
471-
1. **Register the capability.** Advertise
472-
`linkedEditingRangeProvider: true` in the server capabilities
473-
during `initialize`.
474-
475-
2. **Handle the request.** When the client sends
476-
`textDocument/linkedEditingRange`, determine what the cursor is
477-
on:
478-
- **Variable name:** use the scope collector to find all
479-
occurrences of that variable in the enclosing scope. Return
480-
their ranges. Set `wordPattern` to `\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*`
481-
so the editor knows to include the `$` prefix in the linked
482-
edit region.
483-
- If the cursor is not on a supported token, return `null`.
484-
485-
3. **Leverage existing infrastructure.** The scope collector
486-
(`scope_collector/mod.rs`) already tracks variable definition and
487-
read sites with byte offsets. The document highlight handler
488-
(`highlight/mod.rs`) already finds all occurrences of a variable
489-
in a scope and returns their ranges. The linked editing handler
490-
is essentially the same logic but returning a
491-
`LinkedEditingRanges` response instead of `DocumentHighlight[]`.
492-
493-
### Constraints
494-
495-
- All returned ranges must be on the same line or the client may
496-
reject them (some clients impose this restriction, though the spec
497-
does not). In practice, variable occurrences span multiple lines,
498-
and modern clients (VS Code, Zed) handle multi-line linked editing
499-
correctly.
500-
- The response must not include ranges that overlap or that would
501-
produce invalid syntax when edited together. Since all ranges are
502-
the same variable name, this is naturally satisfied.
503-
504-
---
505-
506446
## F11. VS Code extension
507447

508448
| Field | Value |

example.php

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,18 @@
1919
namespace Demo {
2020

2121
use Attribute;
22+
use Bug10298\PropAttr;
23+
use Bug5607\Cl;
2224
use Closure;
2325
use Demo\ValidationException;
2426
use Demo\NotFoundException;
27+
use EloquentBuilder\OnlyUsers;
2528
use Exception;
29+
use Override;
30+
use PHPStan\DependencyInjection\GenerateFactory;
31+
use PHPStan\Reflection\ClassReflection;
32+
use ReadonlyPropertyAssignPhpDoc\C;
33+
use Stringable;
2634
use Demo\UserProfile as Profile;
2735

2836
// ═══════════════════════════════════════════════════════════════════════════
@@ -3354,16 +3362,7 @@ private function acceptTrait(JsonSerializer $x): JsonSerializer
33543362
}
33553363

33563364
// These also produce diagnostics but would crash at class-load time,
3357-
// so they are shown as comments only:
3358-
//
3359-
// class Bad1 extends ScaffoldingDrawable {} // Error: interface in extends
3360-
// class Bad2 implements Pen {} // Error: class in implements
3361-
// class Bad3 { use ScaffoldingDrawable; } // Error: interface in trait use
3362-
// class Bad4 extends ScaffoldingAbstractShape {} // OK: abstract classes CAN be extended
3363-
}
3364-
3365-
3366-
// ═══════════════════════════════════════════════════════════════════════════
3365+
// so they are═══════════════════════════════════════════════════════════════════════════
33673366
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
33683367
// ┃ SCAFFOLDING — Supporting definitions below this line. ┃
33693368

src/definition/resolve.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ impl Backend {
128128
let maps = self.symbol_maps.read();
129129
let map = maps.get(uri)?;
130130
let def = map.var_def_at(var_name, cursor_offset)?;
131-
if matches!(def.kind, VarDefKind::Assignment) {
131+
if matches!(
132+
def.kind,
133+
VarDefKind::Assignment | VarDefKind::CompoundAssignment
134+
) {
132135
Some(def.effective_from)
133136
} else {
134137
None

src/hover/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ impl Backend {
507507
&& !matches!(
508508
def_kind,
509509
VarDefKind::Assignment
510+
| VarDefKind::CompoundAssignment
510511
| VarDefKind::Parameter
511512
| VarDefKind::Foreach
512513
| VarDefKind::Catch

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ mod highlight;
114114
mod hover;
115115
pub(crate) mod inheritance;
116116
mod inlay_hints;
117+
mod linked_editing;
117118
pub(crate) mod names;
118119
mod parser;
119120
pub(crate) mod phar;

0 commit comments

Comments
 (0)