use super::*;
#[test]
fn style_hover_uses_module_graph_property_value_narrowing() -> TestResult {
let mut state = LspShellState::default();
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"workspaceFolders": [
{
"uri": "file:///workspace-a",
"name": "workspace-a",
},
],
},
}),
);
for (uri, text) in [
(
"file:///workspace-a/src/App.module.scss",
"@use \"./theme\";\n.btn { color: red; }\n.btn { color: green; }",
),
(
"file:///workspace-a/src/_theme.scss",
".btn { color: blue; }\n.unrelated { color: black; }",
),
(
"file:///workspace-a/src/_other.scss",
".btn { color: orange; }",
),
] {
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": uri,
"languageId": "scss",
"version": 1,
"text": text,
},
},
}),
);
}
let hover_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 1,
"character": 2,
},
},
}),
);
let hover_text = hover_response
.as_ref()
.and_then(|value| value.pointer("/result/contents/value"))
.and_then(Value::as_str)
.ok_or_else(|| std::io::Error::other("style hover should render markdown value"))?;
assert!(
hover_text.contains("Cascade narrowed values:"),
"{hover_text}"
);
assert!(
hover_text.contains("`blue`"),
"reachable imported module value should participate: {hover_text}"
);
assert!(
!hover_text.contains("`orange`"),
"unreachable module value should not participate: {hover_text}"
);
Ok(())
}
#[test]
fn style_hover_narrows_same_selector_values_by_source_order() -> TestResult {
let mut state = LspShellState::default();
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"workspaceFolders": [
{
"uri": "file:///workspace-a",
"name": "workspace-a",
},
],
},
}),
);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
"languageId": "scss",
"version": 1,
"text": ".button { color: red; }\n.button { color: blue; }",
},
},
}),
);
let hover_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 1,
"character": 2,
},
},
}),
);
let hover_text = hover_response
.as_ref()
.and_then(|value| value.pointer("/result/contents/value"))
.and_then(Value::as_str)
.ok_or_else(|| std::io::Error::other("style hover should render markdown value"))?;
assert!(
hover_text.contains("Cascade narrowed values:"),
"{hover_text}"
);
assert!(
hover_text.contains("- `color`: `blue`"),
"latest same-selector declaration should be the narrowed value: {hover_text}"
);
assert!(
!hover_text.contains("- `color`: `red`, `blue`")
&& !hover_text.contains("- `color`: `blue`, `red`"),
"same-selector source order should avoid a finite set when the winner is known: {hover_text}"
);
Ok(())
}
#[test]
fn style_hover_prefers_positioned_condition_layer_branch() -> TestResult {
let mut state = LspShellState::default();
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"workspaceFolders": [
{
"uri": "file:///workspace-a",
"name": "workspace-a",
},
],
},
}),
);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
"languageId": "scss",
"version": 1,
"text": ".button { color: red; }\n@media (min-width: 40rem) {\n @layer theme {\n .button { color: blue; }\n }\n}",
},
},
}),
);
let hover_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 3,
"character": 6,
},
},
}),
);
let hover_text = hover_response
.as_ref()
.and_then(|value| value.pointer("/result/contents/value"))
.and_then(Value::as_str)
.ok_or_else(|| std::io::Error::other("style hover should render markdown value"))?;
assert!(
hover_text.contains("Cascade narrowed values:"),
"{hover_text}"
);
assert!(
hover_text.contains("- `color`: `blue`"),
"hovered media/layer branch should drive the narrowed value: {hover_text}"
);
assert!(
!hover_text.contains("- `color`: `red`"),
"base branch value should not be mixed into a positioned media/layer hover: {hover_text}"
);
Ok(())
}
#[test]
fn resolves_style_hover_candidates_from_opened_style_documents() -> TestResult {
let mut state = LspShellState::default();
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"workspaceFolders": [
{
"uri": "file:///workspace-a",
"name": "workspace-a",
},
],
},
}),
);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.tsx",
"languageId": "typescriptreact",
"version": 1,
"text": "import styles from \"./App.module.scss\";\nconst view = <div className={styles.root} />;",
},
},
}),
);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
"languageId": "scss",
"version": 1,
"text": ".root { color: var(--brand); }\n.theme { --brand: red; }\n@media (min-width: 40rem) { @layer theme { .root { color: blue; } } }",
},
},
}),
);
let response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 2,
"method": STYLE_HOVER_CANDIDATES_REQUEST,
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 0,
"character": 2,
},
},
}),
);
assert_eq!(
response
.as_ref()
.and_then(|value| value.pointer("/result/product")),
Some(&json!("omena-lsp-server.style-hover-candidates")),
);
assert_eq!(
response
.as_ref()
.and_then(|value| value.pointer("/result/candidateCount")),
Some(&json!(1)),
);
assert_eq!(
response
.as_ref()
.and_then(|value| value.pointer("/result/candidates/0/name")),
Some(&json!("root")),
);
assert_eq!(
response
.as_ref()
.and_then(|value| value.pointer("/result/candidates/0/range")),
Some(&json!({
"start": {
"line": 0,
"character": 1,
},
"end": {
"line": 0,
"character": 5,
},
})),
);
assert_eq!(
response
.as_ref()
.and_then(|value| value.pointer("/result/workspaceFolderUri")),
Some(&json!("file:///workspace-a")),
);
let custom_property_ref_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 3,
"method": STYLE_HOVER_CANDIDATES_REQUEST,
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 0,
"character": 21,
},
},
}),
);
assert_eq!(
custom_property_ref_response
.as_ref()
.and_then(|value| value.pointer("/result/candidates/0/kind")),
Some(&json!("customPropertyReference")),
);
assert_eq!(
custom_property_ref_response
.as_ref()
.and_then(|value| value.pointer("/result/candidates/0/range")),
Some(&json!({
"start": {
"line": 0,
"character": 19,
},
"end": {
"line": 0,
"character": 26,
},
})),
);
let custom_property_decl_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 4,
"method": STYLE_HOVER_CANDIDATES_REQUEST,
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 1,
"character": 11,
},
},
}),
);
assert_eq!(
custom_property_decl_response
.as_ref()
.and_then(|value| value.pointer("/result/candidates/0/kind")),
Some(&json!("customPropertyDeclaration")),
);
assert_eq!(
custom_property_decl_response
.as_ref()
.and_then(|value| value.pointer("/result/candidates/0/range")),
Some(&json!({
"start": {
"line": 1,
"character": 9,
},
"end": {
"line": 1,
"character": 16,
},
})),
);
let hover_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 5,
"method": "textDocument/hover",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 0,
"character": 2,
},
},
}),
);
assert_eq!(
hover_response
.as_ref()
.and_then(|value| value.pointer("/result/contents/kind")),
Some(&json!("markdown")),
);
assert_eq!(
hover_response
.as_ref()
.and_then(|value| value.pointer("/result/range")),
Some(&json!({
"start": {
"line": 0,
"character": 1,
},
"end": {
"line": 0,
"character": 5,
},
})),
);
let hover_text = hover_response
.as_ref()
.and_then(|value| value.pointer("/result/contents/value"))
.and_then(Value::as_str)
.ok_or_else(|| std::io::Error::other("style hover should render markdown value"))?;
assert!(
hover_text.contains("Cascade narrowed values:"),
"{hover_text}"
);
assert!(
hover_text.contains("- `color`: `var(--brand)`"),
"{hover_text}"
);
assert!(!hover_text.contains("@layer theme"), "{hover_text}");
assert!(!hover_text.contains("`blue`"), "{hover_text}");
let definition_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 6,
"method": "textDocument/definition",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 0,
"character": 21,
},
},
}),
);
assert_eq!(
definition_response
.as_ref()
.and_then(|value| value.pointer("/result/0/uri")),
Some(&json!("file:///workspace-a/src/App.module.scss")),
);
assert_eq!(
definition_response
.as_ref()
.and_then(|value| value.pointer("/result/0/range")),
Some(&json!({
"start": {
"line": 1,
"character": 9,
},
"end": {
"line": 1,
"character": 16,
},
})),
);
let references_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 7,
"method": "textDocument/references",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 0,
"character": 21,
},
"context": {
"includeDeclaration": true,
},
},
}),
);
assert_eq!(
references_response
.as_ref()
.and_then(|value| value.pointer("/result/0/range")),
Some(&json!({
"start": {
"line": 0,
"character": 19,
},
"end": {
"line": 0,
"character": 26,
},
})),
);
assert_eq!(
references_response
.as_ref()
.and_then(|value| value.pointer("/result/1/range")),
Some(&json!({
"start": {
"line": 1,
"character": 9,
},
"end": {
"line": 1,
"character": 16,
},
})),
);
let completion_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 8,
"method": "textDocument/completion",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 0,
"character": 20,
},
},
}),
);
assert_eq!(
completion_response
.as_ref()
.and_then(|value| value.pointer("/result/isIncomplete")),
Some(&json!(false)),
);
assert_eq!(
completion_response
.as_ref()
.and_then(|value| value.pointer("/result/items/0/label")),
Some(&json!("--brand")),
);
assert_eq!(
completion_response
.as_ref()
.and_then(|value| value.pointer("/result/items/0/data/rankingSource")),
Some(&json!("sameFileSourceOrderCascade")),
);
assert_eq!(
completion_response
.as_ref()
.and_then(|value| value.pointer("/result/items"))
.and_then(Value::as_array)
.map(Vec::len),
Some(1),
);
let selector_completion_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 9,
"method": "textDocument/completion",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 0,
"character": 0,
},
},
}),
);
let selector_completion_items = selector_completion_response
.as_ref()
.and_then(|value| value.pointer("/result/items"))
.and_then(Value::as_array)
.ok_or_else(|| std::io::Error::other("selector completion should contain items"))?;
let root_completion = selector_completion_items
.iter()
.find(|item| item.pointer("/label") == Some(&json!(".root")))
.ok_or_else(|| std::io::Error::other("root selector completion should be present"))?;
let root_completion_documentation = root_completion
.pointer("/documentation/value")
.and_then(Value::as_str)
.ok_or_else(|| {
std::io::Error::other("root selector completion should carry markdown documentation")
})?;
assert!(
root_completion_documentation.contains("Cascade narrowed values:"),
"{root_completion_documentation}"
);
assert!(
root_completion_documentation.contains("- `color`: `var(--brand)`"),
"{root_completion_documentation}"
);
assert!(
root_completion_documentation.contains("@layer theme"),
"{root_completion_documentation}"
);
assert!(
root_completion_documentation.contains("`blue`"),
"{root_completion_documentation}"
);
let prepare_rename_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 10,
"method": "textDocument/prepareRename",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 0,
"character": 2,
},
},
}),
);
assert_eq!(
prepare_rename_response
.as_ref()
.and_then(|value| value.pointer("/result/placeholder")),
Some(&json!("root")),
);
let rename_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 10,
"method": "textDocument/rename",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
"position": {
"line": 0,
"character": 21,
},
"newName": "--accent",
},
}),
);
assert_eq!(
rename_response.as_ref().and_then(|value| value
.pointer("/result/changes/file:~1~1~1workspace-a~1src~1App.module.scss/0/newText")),
Some(&json!("--accent")),
);
assert_eq!(
rename_response.as_ref().and_then(|value| value
.pointer("/result/changes/file:~1~1~1workspace-a~1src~1App.module.scss/1/newText")),
Some(&json!("--accent")),
);
let code_lens_response = handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 11,
"method": "textDocument/codeLens",
"params": {
"textDocument": {
"uri": "file:///workspace-a/src/App.module.scss",
},
},
}),
);
assert_eq!(
code_lens_response
.as_ref()
.and_then(|value| value.pointer("/result/0/command/title")),
Some(&json!("1 reference")),
);
assert_eq!(
code_lens_response
.as_ref()
.and_then(|value| value.pointer("/result/0/command/command")),
Some(&json!("editor.action.showReferences")),
);
assert_eq!(
code_lens_response
.as_ref()
.and_then(|value| value.pointer("/result/0/command/arguments/2/0/range")),
Some(&json!({
"start": {
"line": 1,
"character": 36,
},
"end": {
"line": 1,
"character": 40,
},
})),
);
Ok(())
}
#[test]
fn code_lens_routes_through_the_dispatched_query_lane() -> TestResult {
let mut state = LspShellState::default();
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///workspace-lens/src/App.module.scss",
"languageId": "scss",
"version": 1,
"text": ".root { color: red; }",
},
},
}),
);
handle_lsp_message(
&mut state,
json!({
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///workspace-lens/src/App.tsx",
"languageId": "typescriptreact",
"version": 1,
"text": "import styles from \"./App.module.scss\";\nconst view = <div className={styles.root} />;",
},
},
}),
);
let turn = crate::handle_lsp_message_scheduled_outputs_or_dispatch(
&mut state,
json!({
"jsonrpc": "2.0",
"id": 21,
"method": "textDocument/codeLens",
"params": {
"textDocument": {
"uri": "file:///workspace-lens/src/App.module.scss",
},
},
}),
);
let crate::LspLoopTurnV0::DispatchQuery(dispatch) = turn else {
return Err("codeLens must route through the dispatched query lane".into());
};
let response = crate::resolve_dispatched_query_response(&dispatch)
.ok_or("a dispatched codeLens request must produce a response")?;
assert_eq!(
response.pointer("/result/0/command/title"),
Some(&json!("1 reference")),
"the dispatched arm answers with the same code lenses as the synchronous arm"
);
assert!(
state.workspace_occurrence_index_memo_lock().is_some(),
"the worker-built occurrence index must land in the LOOP-shared memo"
);
Ok(())
}