Skip to content

Commit 2ee6563

Browse files
feat(csslsrs): get document symbols (#10)
* feat(csslsrs): get document symbols * fix encoding * hide warnings * it's looking good * support nested selector * rust & vscode implementation * wasm basic implementation * trim ranges * support deprecated tags * CSS variables — why this don't work ? * fix: use git versions * nit: use latest versions instead of git * @media details * benches * e2e test * Update packages/language-server-tests-benchmarks/src/benchmarks/css/document_symbols.bench.ts Co-authored-by: Erika <[email protected]> * fix review * Update packages/language-server-tests-benchmarks/src/benchmarks/css/document_symbols.bench.ts Co-authored-by: Erika <[email protected]> --------- Co-authored-by: Princesseuh <[email protected]>
1 parent d0572f4 commit 2ee6563

File tree

18 files changed

+1251
-158
lines changed

18 files changed

+1251
-158
lines changed

Cargo.lock

Lines changed: 138 additions & 146 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/csslsrs/Cargo.toml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,21 @@ crate-type = ["cdylib", "rlib"]
1313
wasm = ["wasm-bindgen", "serde-wasm-bindgen", "console_error_panic_hook"]
1414

1515
[dependencies]
16-
biome_css_parser = "0.5"
16+
biome_css_parser = "0.5.8"
1717
lsp-types = "0.97"
1818
serde = { version = "1.0", features = ["derive"] }
19-
serde_json = "1.0.132"
19+
serde_json = "1.0"
20+
biome_css_syntax = "0.5.8"
21+
biome_rowan = "0.5.8"
22+
rustc-hash = "2.1.0"
23+
palette = { git = "https://github.com/Ogeon/palette/", rev = "234309cdd2f96ac04f034f963c018b98065dcfd1", version = "0.7.6" }
2024
console_error_panic_hook = { version = "0.1.7", optional = true }
2125
serde-wasm-bindgen = { version = "0.6", optional = true }
2226
wasm-bindgen = { version = "0.2", optional = true }
23-
biome_css_syntax = "0.5.7"
24-
biome_rowan = "0.5.7"
25-
rustc-hash = "2.0.0"
26-
palette = { git = "https://github.com/Ogeon/palette/", rev = "234309cdd2f96ac04f034f963c018b98065dcfd1", version = "0.7.6" }
2727

2828
[dev-dependencies]
2929
criterion = { package = "codspeed-criterion-compat", version = "2.7.2" }
30+
pretty_assertions = "1.4.1"
3031

3132
[[bench]]
3233
name = "bench_main"

crates/csslsrs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ A CSS Language Service made with Rust.
1616
| Path completion ||| - |
1717
| Definition ||| - |
1818
| References ||| - |
19-
| Document Symbols | || - |
19+
| Document Symbols | || Supports more symbols |
2020
| Document Highlights ||| - |
2121
| Code Actions ||| - |
2222
| Code Lens ||| - |
@@ -27,4 +27,4 @@ A CSS Language Service made with Rust.
2727
| Selection Range ||| - |
2828
| Validation ||| - |
2929
| Custom data ||| - |
30-
| Super-set of CSS ||| - |
30+
| Super-set of CSS ||| - |

crates/csslsrs/benches/bench_main.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ mod features;
55
criterion_main!(
66
features::folding::benches,
77
features::color::benches,
8-
features::hover::benches
8+
features::hover::benches,
9+
features::document_symbols::benches
910
);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use criterion::{criterion_group, Criterion};
2+
use csslsrs::service::LanguageService;
3+
use lsp_types::{TextDocumentItem, Uri};
4+
use std::{hint::black_box, str::FromStr};
5+
6+
static TEST_CASE: &str = r#"
7+
body {
8+
background-color: #fff;
9+
}
10+
11+
a {
12+
color: red;
13+
}
14+
15+
h1.foo {
16+
color: rgba(0, 0, 0, 0.5);
17+
}
18+
19+
h1 > span {
20+
color: linear-gradient(to right, red, #fff);
21+
}
22+
"#;
23+
24+
fn get_document_symbols_benchmark(c: &mut Criterion) {
25+
let mut ls = LanguageService::default();
26+
27+
let document = TextDocumentItem {
28+
uri: Uri::from_str("file:///test.css").unwrap(),
29+
language_id: "css".to_string(),
30+
version: 0,
31+
text: TEST_CASE.to_string(),
32+
};
33+
34+
ls.upsert_document(document.clone());
35+
36+
c.bench_function("get_document_symbols", |b| {
37+
b.iter(|| ls.get_document_symbols(black_box(document.clone())))
38+
});
39+
}
40+
41+
criterion_group!(benches, get_document_symbols_benchmark);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod color;
2+
pub mod document_symbols;
23
pub mod folding;
34
pub mod hover;
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
use crate::{
2+
converters::{
3+
line_index::LineIndex,
4+
to_proto::{position, range},
5+
PositionEncoding,
6+
},
7+
css_data::{CssCustomData, Status},
8+
service::LanguageService,
9+
};
10+
use biome_css_syntax::{CssLanguage, CssSyntaxKind};
11+
use biome_rowan::{AstNode, SyntaxNode, TextSize};
12+
use lsp_types::{DocumentSymbol, Range, SymbolKind, SymbolTag, TextDocumentItem};
13+
14+
// From a given CSS node, recursively extract document symbols based on their kind in the CSS Syntax Tree.
15+
fn extract_document_symbols(
16+
node: &SyntaxNode<CssLanguage>,
17+
line_index: &LineIndex,
18+
encoding: PositionEncoding,
19+
custom_data: &Vec<&CssCustomData>,
20+
) -> Vec<DocumentSymbol> {
21+
let mut symbols = Vec::new();
22+
for child in node.children() {
23+
let symbol: Option<DocumentSymbol> = match child.kind() {
24+
// Handle CSS at-rules, e.g. `@media`, `@keyframes`, etc.
25+
CssSyntaxKind::CSS_AT_RULE => child.first_child().and_then(|at_rule| {
26+
at_rule.first_token().map(|token| {
27+
create_symbol(
28+
String::from("@") + token.text_trimmed(),
29+
// For some at-rules, we want to include more details, e.g. `@media` should include the media query list.
30+
at_rule
31+
.first_child()
32+
.filter(|child| child.kind() == CssSyntaxKind::CSS_MEDIA_QUERY_LIST)
33+
.map(|child| child.text_trimmed().to_string()),
34+
SymbolKind::NAMESPACE,
35+
range(line_index, child.text_trimmed_range(), encoding).unwrap(),
36+
Range::new(
37+
position(
38+
line_index,
39+
// We need to include the `@` symbol in the selection range.
40+
token.text_trimmed_range().start() - TextSize::from(1),
41+
encoding,
42+
)
43+
.unwrap(),
44+
position(line_index, token.text_trimmed_range().end(), encoding)
45+
.unwrap(),
46+
),
47+
false,
48+
)
49+
})
50+
}),
51+
CssSyntaxKind::CSS_GENERIC_PROPERTY => child.children().find_map(|c| {
52+
match c.kind() {
53+
// Handle CSS variables, e.g. `--foo`, `--bar`, etc.
54+
CssSyntaxKind::CSS_DASHED_IDENTIFIER => c.first_token().map(|property_node| {
55+
create_symbol(
56+
property_node.text_trimmed().to_string(),
57+
None,
58+
SymbolKind::VARIABLE,
59+
range(line_index, child.text_trimmed_range(), encoding).unwrap(),
60+
range(line_index, property_node.text_trimmed_range(), encoding)
61+
.unwrap(),
62+
false,
63+
)
64+
}),
65+
// Handle CSS properties, e.g. `color`, `font-size`, etc.
66+
CssSyntaxKind::CSS_IDENTIFIER => c.first_token().map(|property_node| {
67+
create_symbol(
68+
property_node.text_trimmed().to_string(),
69+
None,
70+
SymbolKind::PROPERTY,
71+
range(line_index, child.text_trimmed_range(), encoding).unwrap(),
72+
range(line_index, property_node.text_trimmed_range(), encoding)
73+
.unwrap(),
74+
is_property_deprecated(property_node.text_trimmed(), custom_data),
75+
)
76+
}),
77+
_ => None,
78+
}
79+
}),
80+
// Handle CSS selectors, e.g. `.foo`, `#bar`, `div > span`, etc. Even when nested.
81+
CssSyntaxKind::CSS_QUALIFIED_RULE | CssSyntaxKind::CSS_NESTED_QUALIFIED_RULE => child
82+
.children()
83+
.find(|c| {
84+
c.kind() == CssSyntaxKind::CSS_SELECTOR_LIST
85+
|| c.kind() == CssSyntaxKind::CSS_SUB_SELECTOR_LIST
86+
|| c.kind() == CssSyntaxKind::CSS_RELATIVE_SELECTOR_LIST
87+
})
88+
.map(|selector| {
89+
create_symbol(
90+
selector.text_trimmed().to_string(),
91+
None,
92+
SymbolKind::CLASS,
93+
range(line_index, child.text_trimmed_range(), encoding).unwrap(),
94+
range(line_index, selector.text_trimmed_range(), encoding).unwrap(),
95+
false,
96+
)
97+
}),
98+
_ => None,
99+
};
100+
101+
// If we have a symbol, search for nested children symbols.
102+
if let Some(mut sym) = symbol {
103+
let children_symbols =
104+
extract_document_symbols(&child, line_index, encoding, custom_data);
105+
if !children_symbols.is_empty() {
106+
sym.children = Some(children_symbols);
107+
}
108+
symbols.push(sym);
109+
} else {
110+
// Even if we don't have a symbol, we still want to search for nested symbols.
111+
let nested_symbols =
112+
extract_document_symbols(&child, line_index, encoding, custom_data);
113+
symbols.extend(nested_symbols);
114+
}
115+
}
116+
117+
symbols
118+
}
119+
120+
// Create a LSP `DocumentSymbol` based on the given parameters.
121+
fn create_symbol(
122+
name: String,
123+
detail: Option<String>,
124+
kind: SymbolKind,
125+
range: lsp_types::Range,
126+
selection_range: lsp_types::Range,
127+
is_deprecated: bool,
128+
) -> DocumentSymbol {
129+
// TMP: deprecated is deprecated, but—for now—we still need to intialize it to None, and hide the warning.
130+
#[allow(deprecated)]
131+
DocumentSymbol {
132+
name,
133+
detail,
134+
kind,
135+
tags: is_deprecated.then(|| vec![SymbolTag::DEPRECATED]),
136+
deprecated: None,
137+
range,
138+
selection_range,
139+
children: None,
140+
}
141+
}
142+
143+
// Use the custom CSS data to determine if a given CSS property is deprecated.
144+
fn is_property_deprecated(property: &str, custom_data: &[&CssCustomData]) -> bool {
145+
custom_data.iter().any(|data| {
146+
data.properties.as_ref().map_or(false, |properties| {
147+
properties
148+
.iter()
149+
.any(|prop| prop.name == property && matches!(prop.status, Some(Status::Obsolete)))
150+
})
151+
})
152+
}
153+
154+
impl LanguageService {
155+
/// Extracts document symbols from the given CSS document.
156+
pub fn get_document_symbols(
157+
&mut self,
158+
document: TextDocumentItem,
159+
) -> Option<Vec<DocumentSymbol>> {
160+
let store_entry = self.store.get(&document.uri);
161+
162+
match store_entry {
163+
Some(store_entry) => Some(extract_document_symbols(
164+
store_entry.css_tree.tree().syntax(),
165+
&store_entry.line_index,
166+
self.options.encoding,
167+
&self.get_css_custom_data(),
168+
)),
169+
None => None,
170+
}
171+
}
172+
}
173+
174+
#[cfg(feature = "wasm")]
175+
mod wasm_bindings {
176+
use std::str::FromStr;
177+
178+
use super::extract_document_symbols;
179+
use crate::service::wasm_bindings::WASMLanguageService;
180+
use biome_rowan::AstNode;
181+
use lsp_types::Uri;
182+
use serde_wasm_bindgen;
183+
use wasm_bindgen::prelude::*;
184+
185+
#[wasm_bindgen(typescript_custom_section)]
186+
const TS_APPEND_CONTENT: &'static str = r#"
187+
export async function get_document_symbols(documentUri: string): import("vscode-languageserver-types").DocumentSymbol[];"#;
188+
189+
#[wasm_bindgen]
190+
impl WASMLanguageService {
191+
#[wasm_bindgen(skip_typescript, js_name = getDocumentSymbols)]
192+
pub fn get_document_symbols(&self, document_uri: String) -> JsValue {
193+
let store_document = self.store.get(&Uri::from_str(&document_uri).unwrap());
194+
195+
let symbols = match store_document {
196+
Some(store_document) => extract_document_symbols(
197+
store_document.css_tree.tree().syntax(),
198+
&store_document.line_index,
199+
self.options.encoding,
200+
&self.get_css_custom_data(),
201+
),
202+
None => Vec::new(),
203+
};
204+
205+
serde_wasm_bindgen::to_value(&symbols).unwrap()
206+
}
207+
}
208+
}

crates/csslsrs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod store;
88
pub mod features {
99
pub mod color_parser;
1010
pub mod colors;
11+
pub mod document_symbols;
1112
pub mod folding;
1213
pub mod hover;
1314
}

crates/csslsrs/src/service.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ pub mod wasm_bindings {
187187
getDocumentColors: typeof get_document_colors;
188188
getColorPresentations: typeof get_color_presentations;
189189
getFoldingRanges: typeof get_folding_ranges;
190+
getDocumentSymbols: typeof get_document_symbols;
190191
free(): void;
191192
}
192193
"#;

0 commit comments

Comments
 (0)