Skip to content

Commit

Permalink
feat(csslsrs): get document symbols (#10)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
goulvenclech and Princesseuh authored Dec 19, 2024
1 parent d0572f4 commit 2ee6563
Show file tree
Hide file tree
Showing 18 changed files with 1,251 additions and 158 deletions.
284 changes: 138 additions & 146 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions crates/csslsrs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,21 @@ crate-type = ["cdylib", "rlib"]
wasm = ["wasm-bindgen", "serde-wasm-bindgen", "console_error_panic_hook"]

[dependencies]
biome_css_parser = "0.5"
biome_css_parser = "0.5.8"
lsp-types = "0.97"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.132"
serde_json = "1.0"
biome_css_syntax = "0.5.8"
biome_rowan = "0.5.8"
rustc-hash = "2.1.0"
palette = { git = "https://github.com/Ogeon/palette/", rev = "234309cdd2f96ac04f034f963c018b98065dcfd1", version = "0.7.6" }
console_error_panic_hook = { version = "0.1.7", optional = true }
serde-wasm-bindgen = { version = "0.6", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
biome_css_syntax = "0.5.7"
biome_rowan = "0.5.7"
rustc-hash = "2.0.0"
palette = { git = "https://github.com/Ogeon/palette/", rev = "234309cdd2f96ac04f034f963c018b98065dcfd1", version = "0.7.6" }

[dev-dependencies]
criterion = { package = "codspeed-criterion-compat", version = "2.7.2" }
pretty_assertions = "1.4.1"

[[bench]]
name = "bench_main"
Expand Down
4 changes: 2 additions & 2 deletions crates/csslsrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ A CSS Language Service made with Rust.
| Path completion ||| - |
| Definition ||| - |
| References ||| - |
| Document Symbols | || - |
| Document Symbols | || Supports more symbols |
| Document Highlights ||| - |
| Code Actions ||| - |
| Code Lens ||| - |
Expand All @@ -27,4 +27,4 @@ A CSS Language Service made with Rust.
| Selection Range ||| - |
| Validation ||| - |
| Custom data ||| - |
| Super-set of CSS ||| - |
| Super-set of CSS ||| - |
3 changes: 2 additions & 1 deletion crates/csslsrs/benches/bench_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ mod features;
criterion_main!(
features::folding::benches,
features::color::benches,
features::hover::benches
features::hover::benches,
features::document_symbols::benches
);
41 changes: 41 additions & 0 deletions crates/csslsrs/benches/features/document_symbols.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use criterion::{criterion_group, Criterion};
use csslsrs::service::LanguageService;
use lsp_types::{TextDocumentItem, Uri};
use std::{hint::black_box, str::FromStr};

static TEST_CASE: &str = r#"
body {
background-color: #fff;
}
a {
color: red;
}
h1.foo {
color: rgba(0, 0, 0, 0.5);
}
h1 > span {
color: linear-gradient(to right, red, #fff);
}
"#;

fn get_document_symbols_benchmark(c: &mut Criterion) {
let mut ls = LanguageService::default();

let document = TextDocumentItem {
uri: Uri::from_str("file:///test.css").unwrap(),
language_id: "css".to_string(),
version: 0,
text: TEST_CASE.to_string(),
};

ls.upsert_document(document.clone());

c.bench_function("get_document_symbols", |b| {
b.iter(|| ls.get_document_symbols(black_box(document.clone())))
});
}

criterion_group!(benches, get_document_symbols_benchmark);
1 change: 1 addition & 0 deletions crates/csslsrs/benches/features/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod color;
pub mod document_symbols;
pub mod folding;
pub mod hover;
208 changes: 208 additions & 0 deletions crates/csslsrs/src/features/document_symbols.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
use crate::{
converters::{
line_index::LineIndex,
to_proto::{position, range},
PositionEncoding,
},
css_data::{CssCustomData, Status},
service::LanguageService,
};
use biome_css_syntax::{CssLanguage, CssSyntaxKind};
use biome_rowan::{AstNode, SyntaxNode, TextSize};
use lsp_types::{DocumentSymbol, Range, SymbolKind, SymbolTag, TextDocumentItem};

// From a given CSS node, recursively extract document symbols based on their kind in the CSS Syntax Tree.
fn extract_document_symbols(
node: &SyntaxNode<CssLanguage>,
line_index: &LineIndex,
encoding: PositionEncoding,
custom_data: &Vec<&CssCustomData>,
) -> Vec<DocumentSymbol> {
let mut symbols = Vec::new();
for child in node.children() {
let symbol: Option<DocumentSymbol> = match child.kind() {
// Handle CSS at-rules, e.g. `@media`, `@keyframes`, etc.
CssSyntaxKind::CSS_AT_RULE => child.first_child().and_then(|at_rule| {
at_rule.first_token().map(|token| {
create_symbol(
String::from("@") + token.text_trimmed(),
// For some at-rules, we want to include more details, e.g. `@media` should include the media query list.
at_rule
.first_child()
.filter(|child| child.kind() == CssSyntaxKind::CSS_MEDIA_QUERY_LIST)
.map(|child| child.text_trimmed().to_string()),
SymbolKind::NAMESPACE,
range(line_index, child.text_trimmed_range(), encoding).unwrap(),
Range::new(
position(
line_index,
// We need to include the `@` symbol in the selection range.
token.text_trimmed_range().start() - TextSize::from(1),
encoding,
)
.unwrap(),
position(line_index, token.text_trimmed_range().end(), encoding)
.unwrap(),
),
false,
)
})
}),
CssSyntaxKind::CSS_GENERIC_PROPERTY => child.children().find_map(|c| {
match c.kind() {
// Handle CSS variables, e.g. `--foo`, `--bar`, etc.
CssSyntaxKind::CSS_DASHED_IDENTIFIER => c.first_token().map(|property_node| {
create_symbol(
property_node.text_trimmed().to_string(),
None,
SymbolKind::VARIABLE,
range(line_index, child.text_trimmed_range(), encoding).unwrap(),
range(line_index, property_node.text_trimmed_range(), encoding)
.unwrap(),
false,
)
}),
// Handle CSS properties, e.g. `color`, `font-size`, etc.
CssSyntaxKind::CSS_IDENTIFIER => c.first_token().map(|property_node| {
create_symbol(
property_node.text_trimmed().to_string(),
None,
SymbolKind::PROPERTY,
range(line_index, child.text_trimmed_range(), encoding).unwrap(),
range(line_index, property_node.text_trimmed_range(), encoding)
.unwrap(),
is_property_deprecated(property_node.text_trimmed(), custom_data),
)
}),
_ => None,
}
}),
// Handle CSS selectors, e.g. `.foo`, `#bar`, `div > span`, etc. Even when nested.
CssSyntaxKind::CSS_QUALIFIED_RULE | CssSyntaxKind::CSS_NESTED_QUALIFIED_RULE => child
.children()
.find(|c| {
c.kind() == CssSyntaxKind::CSS_SELECTOR_LIST
|| c.kind() == CssSyntaxKind::CSS_SUB_SELECTOR_LIST
|| c.kind() == CssSyntaxKind::CSS_RELATIVE_SELECTOR_LIST
})
.map(|selector| {
create_symbol(
selector.text_trimmed().to_string(),
None,
SymbolKind::CLASS,
range(line_index, child.text_trimmed_range(), encoding).unwrap(),
range(line_index, selector.text_trimmed_range(), encoding).unwrap(),
false,
)
}),
_ => None,
};

// If we have a symbol, search for nested children symbols.
if let Some(mut sym) = symbol {
let children_symbols =
extract_document_symbols(&child, line_index, encoding, custom_data);
if !children_symbols.is_empty() {
sym.children = Some(children_symbols);
}
symbols.push(sym);
} else {
// Even if we don't have a symbol, we still want to search for nested symbols.
let nested_symbols =
extract_document_symbols(&child, line_index, encoding, custom_data);
symbols.extend(nested_symbols);
}
}

symbols
}

// Create a LSP `DocumentSymbol` based on the given parameters.
fn create_symbol(
name: String,
detail: Option<String>,
kind: SymbolKind,
range: lsp_types::Range,
selection_range: lsp_types::Range,
is_deprecated: bool,
) -> DocumentSymbol {
// TMP: deprecated is deprecated, but—for now—we still need to intialize it to None, and hide the warning.
#[allow(deprecated)]
DocumentSymbol {
name,
detail,
kind,
tags: is_deprecated.then(|| vec![SymbolTag::DEPRECATED]),
deprecated: None,
range,
selection_range,
children: None,
}
}

// Use the custom CSS data to determine if a given CSS property is deprecated.
fn is_property_deprecated(property: &str, custom_data: &[&CssCustomData]) -> bool {
custom_data.iter().any(|data| {
data.properties.as_ref().map_or(false, |properties| {
properties
.iter()
.any(|prop| prop.name == property && matches!(prop.status, Some(Status::Obsolete)))
})
})
}

impl LanguageService {
/// Extracts document symbols from the given CSS document.
pub fn get_document_symbols(
&mut self,
document: TextDocumentItem,
) -> Option<Vec<DocumentSymbol>> {
let store_entry = self.store.get(&document.uri);

match store_entry {
Some(store_entry) => Some(extract_document_symbols(
store_entry.css_tree.tree().syntax(),
&store_entry.line_index,
self.options.encoding,
&self.get_css_custom_data(),
)),
None => None,
}
}
}

#[cfg(feature = "wasm")]
mod wasm_bindings {
use std::str::FromStr;

use super::extract_document_symbols;
use crate::service::wasm_bindings::WASMLanguageService;
use biome_rowan::AstNode;
use lsp_types::Uri;
use serde_wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = r#"
export async function get_document_symbols(documentUri: string): import("vscode-languageserver-types").DocumentSymbol[];"#;

#[wasm_bindgen]
impl WASMLanguageService {
#[wasm_bindgen(skip_typescript, js_name = getDocumentSymbols)]
pub fn get_document_symbols(&self, document_uri: String) -> JsValue {
let store_document = self.store.get(&Uri::from_str(&document_uri).unwrap());

let symbols = match store_document {
Some(store_document) => extract_document_symbols(
store_document.css_tree.tree().syntax(),
&store_document.line_index,
self.options.encoding,
&self.get_css_custom_data(),
),
None => Vec::new(),
};

serde_wasm_bindgen::to_value(&symbols).unwrap()
}
}
}
1 change: 1 addition & 0 deletions crates/csslsrs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod store;
pub mod features {
pub mod color_parser;
pub mod colors;
pub mod document_symbols;
pub mod folding;
pub mod hover;
}
Expand Down
1 change: 1 addition & 0 deletions crates/csslsrs/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ pub mod wasm_bindings {
getDocumentColors: typeof get_document_colors;
getColorPresentations: typeof get_color_presentations;
getFoldingRanges: typeof get_folding_ranges;
getDocumentSymbols: typeof get_document_symbols;
free(): void;
}
"#;
Expand Down
Loading

0 comments on commit 2ee6563

Please sign in to comment.