Skip to content

Commit 0eccc99

Browse files
committed
Show hover at variable assignment and fix incorrect name space in hover
1 parent 0287e43 commit 0eccc99

11 files changed

Lines changed: 987 additions & 993 deletions

File tree

src/completion/variable/raw_type_inference.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ impl AssignmentAccumulator {
170170
/// candidate source for
171171
/// [`try_chained_array_access_with_candidates`](crate::completion::source::helpers)
172172
/// when resolving array access chains.
173-
pub(in crate::completion) fn resolve_variable_assignment_raw_type(
173+
pub(crate) fn resolve_variable_assignment_raw_type(
174174
var_name: &str,
175175
content: &str,
176176
cursor_offset: u32,

src/completion/variable/resolution.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -784,17 +784,20 @@ fn walk_if_statement<'b>(
784784

785785
/// Handle `foreach` statements during variable assignment walking.
786786
///
787-
/// Only resolves the foreach value/key variable and recurses into the
788-
/// body when the cursor is actually inside the loop body (iteration
789-
/// variables are out of scope outside the loop).
787+
/// Resolves the foreach value/key variable and recurses into the body
788+
/// when the cursor is inside the loop body **or** on the foreach header
789+
/// (the `foreach ($expr as $val)` part). The header check is needed so
790+
/// that hover on the binding variable at its definition site can resolve
791+
/// the iteration type.
790792
fn walk_foreach_statement<'b>(
791793
foreach: &'b Foreach<'b>,
792794
ctx: &VarResolutionCtx<'_>,
793795
results: &mut Vec<ClassInfo>,
794796
conditional: bool,
795797
) {
796798
let body_span = foreach.body.span();
797-
if ctx.cursor_offset >= body_span.start.offset && ctx.cursor_offset <= body_span.end.offset {
799+
let header_start = foreach.foreach.span().start.offset;
800+
if ctx.cursor_offset >= header_start && ctx.cursor_offset <= body_span.end.offset {
798801
// ── Foreach value/key type from generic iterables ──
799802
// When the variable we're resolving is the foreach
800803
// *value* variable, try to infer its type from the

src/definition/resolve.rs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -165,16 +165,18 @@ impl Backend {
165165
// for assignments the definition's `effective_from` is past
166166
// the LHS token — the lookup would skip the definition and
167167
// find an earlier one instead of recognising "at definition".
168-
if let Some(def_kind) = self.lookup_var_def_kind_at(uri, name, cursor_offset) {
169-
// The cursor is on a variable at its definition site.
170-
// Try to resolve the type hint so the user can jump
171-
// from `HtmlString|string $content` to `HtmlString`.
172-
// For all definition kinds (parameter, catch, foreach,
173-
// property, assignment, etc.) the behaviour is the
174-
// same: attempt type-hint resolution and return None
175-
// when no navigable type hint exists.
176-
let _ = def_kind; // all kinds use the same path
177-
return self.resolve_type_hint_at_variable(uri, content, position, &var_name);
168+
if self
169+
.lookup_var_def_kind_at(uri, name, cursor_offset)
170+
.is_some()
171+
{
172+
// The cursor is on a variable at its definition site
173+
// (assignment LHS, parameter, foreach binding, catch
174+
// binding, etc.). GTD should not trigger here — the
175+
// user is already at the definition. Type hints next
176+
// to the variable (e.g. `Throwable` in `catch
177+
// (Throwable $it)`) are separate symbol spans that
178+
// the user can click directly.
179+
return None;
178180
}
179181

180182
if let Some(var_def) = self.lookup_var_definition(uri, name, cursor_offset) {
@@ -199,8 +201,7 @@ impl Backend {
199201
{
200202
return Some(location);
201203
}
202-
// Already at definition — try type-hint resolution.
203-
self.resolve_type_hint_at_variable(uri, content, position, &var_name)
204+
None
204205
}
205206

206207
SymbolKind::MemberAccess {

src/definition/variable/mod.rs

Lines changed: 8 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
/// - **Array destructuring**: `[$a, $b] = …` / `list($a, $b) = …`
2222
///
2323
/// When the cursor is already at the definition site (e.g. on a
24-
/// parameter), the module falls through to type-hint resolution:
25-
/// it extracts the type hint and jumps to the first class-like type
26-
/// in it (e.g. `HtmlString` in `HtmlString|string $content`).
24+
/// parameter or assignment LHS), GTD returns `None` — the user is
25+
/// already at the definition. Type hints next to the variable
26+
/// (e.g. `Throwable` in `catch (Throwable $it)`) are separate
27+
/// symbol spans that the user can click directly.
2728
///
2829
/// When the AST parse fails (malformed PHP, parser panic), the function
2930
/// returns `None` rather than falling back to text heuristics.
@@ -33,20 +34,14 @@
3334
/// - [`var_definition`]: AST walk that finds variable definition sites
3435
/// (assignments, parameters, foreach, catch, static/global,
3536
/// destructuring).
36-
/// - [`type_hint`]: AST walk that extracts the type hint string at a
37-
/// variable's definition site (parameter, property, closure/arrow
38-
/// function parameter).
3937
use tower_lsp::lsp_types::*;
4038

4139
use crate::Backend;
42-
use crate::composer;
4340
use crate::parser::with_parsed_program;
4441
use crate::util::{offset_to_position, position_to_offset};
4542

46-
mod type_hint;
4743
mod var_definition;
4844

49-
use type_hint::find_type_hint_at_definition;
5045
use var_definition::find_variable_definition_in_program;
5146

5247
// ═══════════════════════════════════════════════════════════════════════
@@ -60,8 +55,7 @@ pub(super) enum VarDefSearchResult {
6055
#[default]
6156
NotFound,
6257
/// The cursor is already sitting on the definition site (e.g. on a
63-
/// parameter declaration). The caller should fall through to
64-
/// type-hint resolution.
58+
/// parameter declaration). The caller should return `None`.
6559
AtDefinition,
6660
/// Found a prior definition at the given byte offset.
6761
/// `offset` is the start of the `$var` token, `end_offset` is the end.
@@ -92,8 +86,8 @@ impl Backend {
9286
///
9387
/// Returns:
9488
/// - `Some(Some(location))` — found a prior definition, jump there
95-
/// - `Some(None)` — cursor is at a definition site (fall through to type-hint)
96-
/// OR no definition found in the AST
89+
/// - `Some(None)` — cursor is at a definition site or no definition
90+
/// found in the AST
9791
/// - `None` — AST parse failed
9892
fn resolve_variable_definition_ast(
9993
content: &str,
@@ -118,8 +112,7 @@ impl Backend {
118112
Some(None)
119113
}
120114
VarDefSearchResult::AtDefinition => {
121-
// Cursor is at the definition — return Some(None) so the
122-
// caller falls through to type-hint resolution.
115+
// Cursor is at the definition — return Some(None).
123116
Some(None)
124117
}
125118
VarDefSearchResult::FoundAt { offset, end_offset } => {
@@ -136,118 +129,6 @@ impl Backend {
136129
}
137130
}
138131
}
139-
140-
// ─── Type-Hint Resolution at Variable Definition ────────────────────
141-
142-
/// When the cursor is on a variable that is already at its definition
143-
/// site (parameter, property, promoted property, catch variable),
144-
/// extract the type hint and jump to the first class-like type in it.
145-
///
146-
/// For example, given `public readonly HtmlString|string $content,`
147-
/// this returns the location of the `HtmlString` class definition.
148-
pub(super) fn resolve_type_hint_at_variable(
149-
&self,
150-
uri: &str,
151-
content: &str,
152-
position: Position,
153-
var_name: &str,
154-
) -> Option<Location> {
155-
self.resolve_type_hint_at_variable_ast(uri, content, position, var_name)
156-
}
157-
158-
/// AST-based type-hint resolution: extract the type hint from the AST
159-
/// node where the variable is defined (parameter, catch, property).
160-
fn resolve_type_hint_at_variable_ast(
161-
&self,
162-
uri: &str,
163-
content: &str,
164-
position: Position,
165-
var_name: &str,
166-
) -> Option<Location> {
167-
let cursor_offset = position_to_offset(content, position);
168-
169-
let type_hint_str: Option<String> = with_parsed_program(
170-
content,
171-
"resolve_type_hint_at_variable_ast",
172-
|program, _| find_type_hint_at_definition(program, var_name, cursor_offset),
173-
);
174-
175-
let type_hint = type_hint_str?;
176-
self.resolve_type_hint_string_to_location(uri, content, &type_hint)
177-
}
178-
179-
/// Given a type-hint string (e.g. `HtmlString|string`, `?Foo`),
180-
/// resolve it to the definition location of the first class-like type.
181-
fn resolve_type_hint_string_to_location(
182-
&self,
183-
uri: &str,
184-
content: &str,
185-
type_hint: &str,
186-
) -> Option<Location> {
187-
let scalars = [
188-
"string", "int", "float", "bool", "array", "callable", "iterable", "object", "mixed",
189-
"void", "never", "null", "false", "true", "self", "static", "parent",
190-
];
191-
192-
let class_name = type_hint
193-
.split(['|', '&'])
194-
.map(|t| t.trim_start_matches('?'))
195-
.find(|t| !t.is_empty() && !scalars.contains(&t.to_lowercase().as_str()))?;
196-
197-
let ctx = self.file_context(uri);
198-
199-
let fqn = Self::resolve_to_fqn(class_name, &ctx.use_map, &ctx.namespace);
200-
201-
let mut candidates = vec![fqn];
202-
if class_name.contains('\\') && !candidates.contains(&class_name.to_string()) {
203-
candidates.push(class_name.to_string());
204-
}
205-
206-
// Try same-file first.
207-
for fqn in &candidates {
208-
if let Some(location) = self.find_definition_in_ast_map(fqn, content, uri) {
209-
return Some(location);
210-
}
211-
}
212-
213-
// Try Composer classmap: direct FQN → file path lookup.
214-
// This covers vendor classes that haven't been loaded into ast_map
215-
// yet (cold Ctrl+Click on a type hint never used in completion).
216-
for fqn in &candidates {
217-
if let Ok(cmap) = self.classmap.lock()
218-
&& let Some(file_path) = cmap.get(fqn.as_str()).cloned()
219-
{
220-
drop(cmap);
221-
if let Some(location) = self.resolve_class_in_file(&file_path, fqn) {
222-
return Some(location);
223-
}
224-
}
225-
}
226-
227-
// Try PSR-4 resolution as a last resort.
228-
// PSR-4 mappings only cover user code (from composer.json).
229-
// Vendor classes are resolved by the classmap above.
230-
let workspace_root = self
231-
.workspace_root
232-
.lock()
233-
.ok()
234-
.and_then(|guard| guard.clone());
235-
236-
if let Some(workspace_root) = workspace_root
237-
&& let Ok(mappings) = self.psr4_mappings.lock()
238-
{
239-
for fqn in &candidates {
240-
if let Some(file_path) =
241-
composer::resolve_class_path(&mappings, &workspace_root, fqn)
242-
&& let Some(location) = self.resolve_class_in_file(&file_path, fqn)
243-
{
244-
return Some(location);
245-
}
246-
}
247-
}
248-
249-
None
250-
}
251132
}
252133

253134
// ═══════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)