Skip to content

Commit d01eae0

Browse files
siduxAJenbo
authored andcommitted
Fix type hierarchy registration and subtype expansion
Signed-off-by: Anders Jenbo <anders@jenbo.dk>
1 parent 8a288cd commit d01eae0

4 files changed

Lines changed: 120 additions & 5 deletions

File tree

docs/CHANGELOG.md

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

2828
### Fixed
2929

30+
- **Type Hierarchy works in more clients.** The `textDocument/prepareTypeHierarchy` capability was registered without registration options, so some clients (notably Zed) did not reliably expose the Type Hierarchy action. The dynamic registration now carries proper `TypeHierarchyRegistrationOptions` with a PHP document selector, so those clients recognise that the feature applies to PHP files. Contributed by @sidux in https://github.com/PHPantom-dev/phpantom_lsp/pull/179.
3031
- **Extract method generates correct code for more selections.** A variable that the selection reads before it first assigns (for example a parameter the extracted code both consults and updates) is now passed in as an argument as well as returned, instead of being left undefined inside the new method. And an early `return` whose value references a variable defined inside the selection is now kept inside the extracted method and propagated to the caller, instead of being copied to the call site where that variable does not exist.
3132
- **The editor stays responsive during fast typing in large files.** Editors send a burst of requests on every keystroke (completion, a documentation lookup for each suggestion, diagnostics, code lens, semantic highlighting, and more). The server processed only a few at a time, so during continuous typing the burst backed up until the server stopped answering anything at all, including the completion the user was waiting on, and it only recovered after a restart. Now the burst is processed concurrently and every expensive request runs off the main loop: diagnostics (which re-analyze the whole file on each edit) compute in the background instead of on the request that asked for them, so a diagnostic pull returns immediately and never blocks the threads that deliver completion and hover, and repeated whole-file requests (semantic highlighting, code lens, the document outline, folding, document links) are collapsed so a fast typist's superseded requests no longer pile up and monopolize the CPU. Completion and other requests keep coming back while you type.
3233
- **Typing in a large file no longer pegs the CPU and stalls completion.** Semantic highlighting recomputed every token's position by rescanning the file from the beginning, so a large file took many seconds at full CPU to highlight. Editors request highlighting on every keystroke, so this ran continuously while typing and starved completion, hover, and other requests until they appeared to hang. Highlighting a large file is now effectively instant, and the same speedup applies to the document outline and code folding, which used the same per-position rescan.

src/server.rs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,7 @@ impl LanguageServer for Backend {
397397
.supports_type_hierarchy_dynamic_registration
398398
.load(Ordering::Acquire)
399399
{
400-
registrations.push(Registration {
401-
id: "type-hierarchy".to_string(),
402-
method: "textDocument/prepareTypeHierarchy".to_string(),
403-
register_options: None,
404-
});
400+
registrations.push(type_hierarchy_registration());
405401
}
406402

407403
// Register file watchers for staleness detection. The client
@@ -1588,6 +1584,27 @@ impl LanguageServer for Backend {
15881584
}
15891585
}
15901586

1587+
fn type_hierarchy_registration() -> Registration {
1588+
Registration {
1589+
id: "type-hierarchy".to_string(),
1590+
method: "textDocument/prepareTypeHierarchy".to_string(),
1591+
register_options: Some(
1592+
serde_json::to_value(TypeHierarchyRegistrationOptions {
1593+
text_document_registration_options: TextDocumentRegistrationOptions {
1594+
document_selector: Some(vec![DocumentFilter {
1595+
language: Some("php".to_string()),
1596+
scheme: None,
1597+
pattern: None,
1598+
}]),
1599+
},
1600+
type_hierarchy_options: TypeHierarchyOptions::default(),
1601+
static_registration_options: StaticRegistrationOptions::default(),
1602+
})
1603+
.expect("type hierarchy registration options serialize"),
1604+
),
1605+
}
1606+
}
1607+
15911608
/// Convert a `Vec<Location>` into a `GotoDefinitionResponse`.
15921609
///
15931610
/// Returns `Scalar` for a single location, `Array` for multiple, and
@@ -1603,6 +1620,26 @@ fn wrap_locations(locations: Vec<Location>) -> Option<GotoDefinitionResponse> {
16031620
}
16041621
}
16051622

1623+
#[cfg(test)]
1624+
mod tests {
1625+
use super::*;
1626+
1627+
#[test]
1628+
fn type_hierarchy_registration_includes_php_document_selector() {
1629+
let registration = type_hierarchy_registration();
1630+
1631+
assert_eq!(registration.id, "type-hierarchy");
1632+
assert_eq!(registration.method, "textDocument/prepareTypeHierarchy");
1633+
1634+
let options = registration
1635+
.register_options
1636+
.expect("type hierarchy registration should include options");
1637+
assert_eq!(options["documentSelector"][0]["language"], "php");
1638+
assert!(options["documentSelector"][0].get("scheme").is_none());
1639+
assert!(options["documentSelector"][0].get("pattern").is_none());
1640+
}
1641+
}
1642+
16061643
// ─── Self-scan helpers ──────────────────────────────────────────────────────
16071644

16081645
impl Backend {

src/type_hierarchy.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ impl Backend {
139139

140140
// direct_only = true so only immediate children are returned;
141141
// the client walks the tree one level at a time.
142+
//
143+
// We can't shortcut to the reverse inheritance index (`gti_index`)
144+
// alone: it is populated only from files that have been fully
145+
// parsed, so a parent may have one child already indexed while
146+
// other children live in files discovered on disk but not yet
147+
// parsed. `find_implementors` uses `gti_index` as its first phase
148+
// and then scans for the rest, so it returns the complete set.
142149
let implementors = self.find_implementors(short, &fqn, &class_loader, true, true);
143150

144151
let mut result = Vec::new();

tests/integration/type_hierarchy.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,3 +1060,73 @@ async fn supertypes_items_have_nonzero_selection_range() {
10601060
);
10611061
}
10621062
}
1063+
1064+
// ─── Subtypes: completeness when some children already indexed ──────────────
1065+
1066+
/// Subtypes must list every direct child even when one child file is
1067+
/// already parsed (so it lives in the reverse inheritance index) while
1068+
/// another child file has only been discovered on disk and not yet
1069+
/// parsed. Returning only the indexed children silently hides the rest.
1070+
#[tokio::test]
1071+
async fn subtypes_includes_unopened_children_when_some_are_indexed() {
1072+
let composer = r#"{
1073+
"autoload": {
1074+
"psr-4": {
1075+
"App\\": "src/"
1076+
}
1077+
}
1078+
}"#;
1079+
let files = &[
1080+
(
1081+
"src/Repository.php",
1082+
"<?php\nnamespace App;\ninterface Repository {\n}\n",
1083+
),
1084+
(
1085+
"src/UserRepository.php",
1086+
"<?php\nnamespace App;\nclass UserRepository implements Repository {\n}\n",
1087+
),
1088+
(
1089+
"src/AdminRepository.php",
1090+
"<?php\nnamespace App;\nclass AdminRepository implements Repository {\n}\n",
1091+
),
1092+
];
1093+
1094+
let (backend, dir) = create_psr4_workspace(composer, files);
1095+
1096+
// Open the interface and ONE implementor. Opening UserRepository
1097+
// parses it and records App\UserRepository as a child of
1098+
// App\Repository in the reverse inheritance index — but
1099+
// AdminRepository stays on disk, discovered only by a wider scan.
1100+
let iface_uri = Url::from_file_path(dir.path().join("src/Repository.php")).unwrap();
1101+
open(
1102+
&backend,
1103+
&iface_uri,
1104+
"<?php\nnamespace App;\ninterface Repository {\n}\n",
1105+
)
1106+
.await;
1107+
let user_uri = Url::from_file_path(dir.path().join("src/UserRepository.php")).unwrap();
1108+
open(
1109+
&backend,
1110+
&user_uri,
1111+
"<?php\nnamespace App;\nclass UserRepository implements Repository {\n}\n",
1112+
)
1113+
.await;
1114+
1115+
// Prepare on the `Repository` interface name (line 2, on the name).
1116+
let items = prepare_at(&backend, &iface_uri, 2, 12).await;
1117+
assert_eq!(items.len(), 1, "Should prepare on Repository interface");
1118+
assert_eq!(item_fqn(&items[0]), "App\\Repository");
1119+
1120+
let subs = subtypes_of(&backend, &items[0]).await;
1121+
let names = item_names(&subs);
1122+
assert!(
1123+
names.contains(&"UserRepository".to_string()),
1124+
"subtypes should include the already-indexed UserRepository, got: {:?}",
1125+
names
1126+
);
1127+
assert!(
1128+
names.contains(&"AdminRepository".to_string()),
1129+
"subtypes should include the unopened AdminRepository, got: {:?}",
1130+
names
1131+
);
1132+
}

0 commit comments

Comments
 (0)