Skip to content

Commit 87d19f4

Browse files
committed
Show hover for Blade echo delimiters as e() function
1 parent 8415aa4 commit 87d19f4

4 files changed

Lines changed: 100 additions & 10 deletions

File tree

docs/CHANGELOG.md

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

1919
- **Blade hover positions.** Hover ranges in `.blade.php` files are now translated back to original Blade coordinates, so the editor highlights the correct symbol under the cursor.
20+
- **Blade echo delimiter hover.** Hovering on `{{` or `}}` in Blade templates now shows hover for the `e()` function instead of incorrectly showing hover for the expression inside the echo.
2021
- **Standalone `@var` completion.** Variables typed only via a standalone `/** @var Type $var */` docblock (with no preceding assignment) now resolve for member completion and go-to-definition. This fixes completion inside Blade templates that use `@php /** @var \App\Models\Foo $var */ @endphp`.
2122
- **Foreach loop variable resolution in Blade.** Loop variables (e.g. `$user` in `@foreach($users->active() as $user)`) now resolve their element type correctly when the iterable is typed via a standalone `@var` docblock. Template parameter bounds (e.g. `@template TModel of BlogAuthor`) are substituted through the inheritance chain.
2223

docs/todo/bugs.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@ pipeline so it produces correct data. Downstream consumers
88
to second-guess upstream output.
99

1010

11-
## B17. Blade `{{` hover shows inner expression instead of `e()`
12-
13-
Hovering on the `{{` delimiter in a Blade template should show
14-
hover info for the implicit `e()` (htmlspecialchars) call that
15-
Blade compiles to. Currently it shows hover for the expression
16-
inside the echo (e.g. `config(...)`) because the position mapping
17-
offsets into the virtual PHP content rather than recognising the
18-
delimiter itself.
19-
20-
2111
## B16. PDOStatement fetch mode-dependent return types
2212

2313
**Blocked on:** [phpstorm-stubs#1882](https://github.com/JetBrains/phpstorm-stubs/pull/1882)

src/blade/mod.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,91 @@ fn extract_string_literal(s: &str) -> Option<&str> {
115115
None
116116
}
117117
}
118+
119+
use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position, Range};
120+
121+
impl crate::Backend {
122+
/// If the cursor is on a `{{`, `}}`, `{!!`, or `!!}` Blade echo delimiter,
123+
/// return a hover describing the implicit `e()` call (for escaped echo)
124+
/// or raw output (for unescaped echo).
125+
pub(crate) fn blade_echo_delimiter_hover(
126+
&self,
127+
uri: &str,
128+
position: Position,
129+
) -> Option<Hover> {
130+
let content = self.get_file_content(uri)?;
131+
let line = content.lines().nth(position.line as usize)?;
132+
let col = position.character as usize;
133+
134+
// Check if cursor is on `{{` (escaped echo open)
135+
if col < line.len()
136+
&& line.get(col..col + 2) == Some("{{")
137+
&& line.get(col..col + 3) != Some("{!!")
138+
{
139+
return Some(self.blade_e_hover(position, 2));
140+
}
141+
// Also match if cursor is on the second `{` of `{{`
142+
if col > 0
143+
&& line.get(col - 1..col + 1) == Some("{{")
144+
&& (col < 2 || line.get(col - 1..col + 2) != Some("{!!"))
145+
{
146+
return Some(self.blade_e_hover(
147+
Position {
148+
line: position.line,
149+
character: (col - 1) as u32,
150+
},
151+
2,
152+
));
153+
}
154+
// `}}` closing delimiter
155+
if col < line.len()
156+
&& line.get(col..col + 2) == Some("}}")
157+
&& (col == 0 || line.as_bytes().get(col - 1) != Some(&b'!'))
158+
{
159+
return Some(self.blade_e_hover(position, 2));
160+
}
161+
if col > 0
162+
&& line.get(col - 1..col + 1) == Some("}}")
163+
&& (col < 2 || line.as_bytes().get(col - 2) != Some(&b'!'))
164+
{
165+
return Some(self.blade_e_hover(
166+
Position {
167+
line: position.line,
168+
character: (col - 1) as u32,
169+
},
170+
2,
171+
));
172+
}
173+
174+
None
175+
}
176+
177+
/// Build hover content for `{{ }}` (escaped echo via `e()`).
178+
fn blade_e_hover(&self, start: Position, len: u32) -> Hover {
179+
// Try to resolve the actual `e()` function from the project/stubs.
180+
let empty_use_map = std::collections::HashMap::new();
181+
let loader = self.function_loader_with(&empty_use_map, &None);
182+
let content = if let Some(func) = loader("e") {
183+
crate::hover::hover_for_function(&func, None)
184+
.contents
185+
} else {
186+
HoverContents::Markup(MarkupContent {
187+
kind: MarkupKind::Markdown,
188+
value: "Blade escaped echo. Output is passed through `e()` (`htmlspecialchars`).\n\n\
189+
```php\n<?php\nfunction e(mixed $value, bool $doubleEncode = true): string;\n```"
190+
.to_string(),
191+
})
192+
};
193+
Hover {
194+
contents: content,
195+
range: Some(Range {
196+
start,
197+
end: Position {
198+
line: start.line,
199+
character: start.character + len,
200+
},
201+
}),
202+
}
203+
}
204+
205+
}

src/server.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,17 @@ impl LanguageServer for Backend {
640640
let backend = self.clone_for_blocking();
641641
let uri_clone = uri.clone();
642642
tokio::task::spawn_blocking(move || {
643+
// For Blade files, check if the cursor is on a `{{` or `{!!` echo
644+
// delimiter. If so, return hover for `e()` (escaped echo) or a
645+
// raw-echo explanation, rather than falling through to the virtual
646+
// PHP content where the position maps into boilerplate.
647+
if crate::blade::is_blade_file(&uri_clone)
648+
&& let Some(hover) =
649+
backend.blade_echo_delimiter_hover(&uri_clone, position)
650+
{
651+
return Ok(Some(hover));
652+
}
653+
643654
backend.handle_with_position("hover", &uri_clone, position, |content, pos| {
644655
let mut hover = backend.handle_hover(&uri_clone, content, pos)?;
645656
if crate::blade::is_blade_file(&uri_clone)

0 commit comments

Comments
 (0)