Skip to content

Commit ce5cdfc

Browse files
authored
fix(hover): at rules support & refactor (#20)
1 parent 3546cf3 commit ce5cdfc

File tree

2 files changed

+108
-112
lines changed

2 files changed

+108
-112
lines changed

crates/csslsrs/src/features/hover.rs

Lines changed: 89 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,110 @@
1-
use crate::css_data::{
2-
AtDirectiveEntry, CssCustomData, MarkupDescriptionOrString, PropertyEntry, Reference, Status,
3-
};
4-
use biome_css_syntax::{CssLanguage, CssSyntaxKind};
5-
use biome_rowan::{AstNode, SyntaxNode};
6-
use lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position, TextDocumentItem};
7-
use std::fmt::Write;
8-
91
use crate::{
10-
converters::{from_proto::offset, line_index::LineIndex, to_proto::range, PositionEncoding},
2+
converters::{
3+
from_proto::offset,
4+
line_index::LineIndex,
5+
to_proto::{self, range},
6+
PositionEncoding,
7+
},
8+
css_data::{
9+
AtDirectiveEntry, CssCustomData, MarkupDescriptionOrString, PropertyEntry, Reference,
10+
Status,
11+
},
1112
service::LanguageService,
1213
};
14+
use biome_css_syntax::{CssLanguage, CssSyntaxKind};
15+
use biome_rowan::{AstNode, SyntaxNode, TextSize};
16+
use lsp_types::{
17+
Hover, HoverContents, MarkupContent, MarkupKind, Position, Range, TextDocumentItem,
18+
};
19+
use std::fmt::Write;
1320

1421
/// Extracts hover information for the given CSS node and position.
1522
fn extract_hover_information(
1623
node: &SyntaxNode<CssLanguage>,
1724
position: Position,
1825
line_index: &LineIndex,
1926
encoding: PositionEncoding,
20-
css_data: &Vec<&CssCustomData>,
27+
css_data: &[&CssCustomData],
2128
) -> Option<Hover> {
2229
let offset = offset(line_index, position, encoding).ok()?;
2330
let token = node.token_at_offset(offset).right_biased()?;
24-
let mut selector_node = None;
25-
for ancestor in token.ancestors() {
26-
match ancestor.kind() {
27-
// These nodes represent the full selector, including combinators
31+
// Since the token is a leaf node, we need to find a more meaningful parent to provide hover informations.
32+
token
33+
.ancestors()
34+
.find_map(|ancestor| match ancestor.kind() {
35+
// Handle CSS selectors, e.g. `.class`, `#id`, `element`, `element.class`, etc.
2836
CssSyntaxKind::CSS_COMPLEX_SELECTOR | CssSyntaxKind::CSS_SELECTOR_LIST => {
29-
selector_node = Some(ancestor.clone());
30-
break; // We've found the full selector
37+
let name = &ancestor.text_trimmed().to_string();
38+
let content = format_selector_entry(name, Some(calculate_specificity(name)));
39+
Some(Hover {
40+
contents: HoverContents::Markup(MarkupContent {
41+
kind: MarkupKind::Markdown,
42+
value: content,
43+
}),
44+
range: range(line_index, ancestor.text_trimmed_range(), encoding).ok(),
45+
})
3146
}
32-
// Update selector_node if it's not already set
33-
CssSyntaxKind::CSS_COMPOUND_SELECTOR => {
34-
if selector_node.is_none() {
35-
selector_node = Some(ancestor.clone());
36-
}
37-
}
38-
CssSyntaxKind::CSS_IDENTIFIER => {
39-
// Handle identifiers like properties or at-rules
40-
if let Some(hover_content) =
41-
get_css_hover_content(ancestor.kind(), token.text().trim(), css_data)
42-
{
43-
return Some(Hover {
44-
contents: HoverContents::Markup(MarkupContent {
45-
kind: MarkupKind::Markdown,
46-
value: hover_content,
47-
}),
48-
range: range(line_index, ancestor.text_trimmed_range(), encoding).ok(),
49-
});
50-
}
47+
// Handle CSS properties, e.g. `color`, `font-size`, etc.
48+
CssSyntaxKind::CSS_GENERIC_PROPERTY => {
49+
// We can assume that the token is the IDENT token with the property name.
50+
let name = token.text_trimmed().to_string();
51+
css_data.iter().find_map(|data| {
52+
data.properties
53+
.as_ref()?
54+
.iter()
55+
.find(|prop| prop.name == name)
56+
.map(format_property_entry)
57+
.map(|content| Hover {
58+
contents: HoverContents::Markup(MarkupContent {
59+
kind: MarkupKind::Markdown,
60+
value: content,
61+
}),
62+
range: range(line_index, token.text_trimmed_range(), encoding).ok(),
63+
})
64+
})
5165
}
52-
_ => {
53-
// Not part of a selector; continue traversing
66+
// Handle CSS at-rules, e.g. `@media`, `@keyframes`, etc.
67+
CssSyntaxKind::CSS_AT_RULE => {
68+
// We can't assume that the token is the KW token (with the at-rule name) since it could be the AT token.
69+
let at_rule_token = ancestor.first_child()?.first_token()?;
70+
css_data.iter().find_map(|data| {
71+
data.at_directives
72+
.as_ref()?
73+
.iter()
74+
.find(|at_rule| {
75+
// CSS Custom Data uses `@` prefix for at-rules, so we need to add it back.
76+
at_rule.name == format!("@{}", at_rule_token.text_trimmed())
77+
})
78+
.map(format_at_rule_entry)
79+
.map(|content| Hover {
80+
contents: HoverContents::Markup(MarkupContent {
81+
kind: MarkupKind::Markdown,
82+
value: content,
83+
}),
84+
range: Some(Range::new(
85+
to_proto::position(
86+
line_index,
87+
// We need to include the `@` symbol in the selection range.
88+
at_rule_token.text_trimmed_range().start() - TextSize::from(1),
89+
encoding,
90+
)
91+
.unwrap(),
92+
to_proto::position(
93+
line_index,
94+
at_rule_token.text_trimmed_range().end(),
95+
encoding,
96+
)
97+
.unwrap(),
98+
)),
99+
})
100+
})
54101
}
55-
}
56-
}
57-
58-
// Use the identified selector node for hover content
59-
if let Some(selector_node) = selector_node {
60-
if let Some(hover_content) = get_css_hover_content(
61-
selector_node.kind(),
62-
selector_node.text().to_string().trim(),
63-
css_data,
64-
) {
65-
return Some(Hover {
66-
contents: HoverContents::Markup(MarkupContent {
67-
kind: MarkupKind::Markdown,
68-
value: hover_content,
69-
}),
70-
range: range(line_index, selector_node.text_trimmed_range(), encoding).ok(),
71-
});
72-
}
73-
}
74-
75-
None
76-
}
77-
78-
/// Generates hover content for a given CSS entity using the provided CSS custom data.
79-
fn get_css_hover_content(
80-
kind: CssSyntaxKind,
81-
name: &str,
82-
css_data: &Vec<&CssCustomData>,
83-
) -> Option<String> {
84-
match kind {
85-
// Handle CSS properties like "color", "font-size", etc.
86-
CssSyntaxKind::CSS_IDENTIFIER => {
87-
for data in css_data {
88-
if let Some(property) = data
89-
.properties
90-
.as_ref()
91-
.iter()
92-
.flat_map(|props| props.iter())
93-
.find(|prop| prop.name == name)
94-
{
95-
return Some(format_css_property_entry(property));
96-
}
97-
}
98-
None
99-
}
100-
// Handle at-rules like @media, @supports, etc.
101-
CssSyntaxKind::CSS_AT_RULE => {
102-
eprintln!("Looking for at-rule: {}", name);
103-
for data in css_data {
104-
if let Some(at_directive) = data
105-
.at_directives
106-
.as_ref()
107-
.iter()
108-
.flat_map(|ats| ats.iter())
109-
.find(|at| at.name == name)
110-
{
111-
return Some(format_css_at_rule_entry(at_directive));
112-
}
113-
}
114-
None
115-
}
116-
// Handle CSS selectors like ".class", "#id", "element", etc.
117-
CssSyntaxKind::CSS_SELECTOR_LIST
118-
| CssSyntaxKind::CSS_COMPLEX_SELECTOR
119-
| CssSyntaxKind::CSS_COMPOUND_SELECTOR => Some(format_css_selector_entry(
120-
name,
121-
Some(calculate_specificity(name)),
122-
)),
123-
_ => None,
124-
}
102+
_ => None,
103+
})
125104
}
126105

127106
/// Formats the CSS property entry into a hover content string.
128-
fn format_css_property_entry(property: &PropertyEntry) -> String {
107+
fn format_property_entry(property: &PropertyEntry) -> String {
129108
let mut content = String::new();
130109
write_status(&mut content, &property.status);
131110
write_description(&mut content, &property.description);
@@ -136,7 +115,7 @@ fn format_css_property_entry(property: &PropertyEntry) -> String {
136115
}
137116

138117
/// Formats the CSS at-rule entry into a hover content string.
139-
fn format_css_at_rule_entry(at_property: &AtDirectiveEntry) -> String {
118+
fn format_at_rule_entry(at_property: &AtDirectiveEntry) -> String {
140119
let mut content = String::new();
141120
write_status(&mut content, &at_property.status);
142121
write_description(&mut content, &at_property.description);
@@ -145,7 +124,7 @@ fn format_css_at_rule_entry(at_property: &AtDirectiveEntry) -> String {
145124
}
146125

147126
/// Formats the CSS selector entry into a hover content string.
148-
fn format_css_selector_entry(name: &str, specificity: Option<(u32, u32, u32)>) -> String {
127+
fn format_selector_entry(name: &str, specificity: Option<(u32, u32, u32)>) -> String {
149128
let mut content = String::new();
150129
// TODO: this is a placeholder, we should render an HTML preview of the selector
151130
writeln!(content, "**{}**\n", escape_markdown(name)).unwrap();

crates/csslsrs/tests/hover.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use csslsrs::service::LanguageService;
22
use lsp_types::{
33
Hover, HoverContents, MarkupContent, MarkupKind, Position, Range, TextDocumentItem, Uri,
44
};
5+
use pretty_assertions::assert_eq;
56
use std::str::FromStr;
67

78
/// Utility function to convert an offset to a position
@@ -311,14 +312,30 @@ fn test_hover_with_escaped_colon() {
311312
assert_hover(css_text, expected_hover);
312313
}
313314

314-
#[ignore]
315315
#[test]
316316
fn test_hover_at_rule() {
317+
let css_text = "@med|ia screen and (min-width: 900px) {}";
318+
let expected_hover = Hover {
319+
contents: HoverContents::Markup(MarkupContent {
320+
kind: MarkupKind::Markdown,
321+
value: "Defines a stylesheet for a particular media type.\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@media), [Can I Use](https://caniuse.com/?search=@media)\n\n".to_string(),
322+
}),
323+
range: Some(Range {
324+
start: Position { line: 0, character: 0 },
325+
end: Position { line: 0, character: 6 },
326+
}),
327+
};
328+
329+
assert_hover(css_text, expected_hover);
330+
}
331+
332+
#[test]
333+
fn test_hover_at_rule_on_at_token() {
317334
let css_text = "|@media screen and (min-width: 900px) {}";
318335
let expected_hover = Hover {
319336
contents: HoverContents::Markup(MarkupContent {
320337
kind: MarkupKind::Markdown,
321-
value: "**@media**\n\n[At Rule Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 0, 0)\n\n".to_string(),
338+
value: "Defines a stylesheet for a particular media type.\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@media), [Can I Use](https://caniuse.com/?search=@media)\n\n".to_string(),
322339
}),
323340
range: Some(Range {
324341
start: Position { line: 0, character: 0 },

0 commit comments

Comments
 (0)