@@ -89,7 +89,9 @@ impl LanguageServer for Backend {
8989 SemanticTokenType::COMMENT,
9090 SemanticTokenType::PROPERTY,
9191 ],
92− token_modifiers: vec![],
92+ token_modifiers: vec![
93+ SemanticTokenModifier::new("deprecated"), // bit 0 = 1 → dimmed/strikethrough
94+ ],
9395 },
9496 full: Some(SemanticTokensFullOptions::Bool(true)),
9597 range: None,
@@ -700,6 +702,8 @@ fn directive_completions(range: Range) -> Vec<CompletionItem> {
700702 ("@base", "Set base URL for relative links", "@base "),
701703 ("@font-face", "Define a custom font face", "@font-face"),
702704 ("@json-ld", "Add JSON-LD structured data to head", "@json-ld"),
705+ ("@mixin", "Define a composable style group (use with ...$name)", "@mixin "),
706+ ("@assert", "Compile-time assertion for variable values", "@assert "),
703707 ]
704708 .iter()
705709 .map(|(name, detail, insert)| {
@@ -1328,19 +1332,39 @@ fn function_completions(text: &str, range: Range) -> Vec<CompletionItem> {
13281332 if let Some(rest) = trimmed.strip_prefix("@fn ") {
13291333 let parts: Vec<&str> = rest.split_whitespace().collect();
13301334 if let Some(name) = parts.first() {
1331− let params = parts[1..].join(" ");
1335+ let params: Vec<&str> = parts[1..].iter()
1336+ .map(|p| p.strip_prefix('$').unwrap_or(p))
1337+ .collect();
13321338 let detail = if params.is_empty() {
13331339 "Function".to_string()
13341340 } else {
1335− format!("Function({})", params)
1341+ format!("Function({})", params.join(", "))
13361342 };
1337− items.push(item(
1338− &format!("@{}", name),
1339− CompletionItemKind::FUNCTION,
1340− &detail,
1341− &format!("@{}", name),
1342− range,
1343− ));
1343+ // Generate snippet with tab stops for parameters
1344+ let insert_text = if params.is_empty() {
1345+ format!("@{}", name)
1346+ } else {
1347+ let param_snippets: Vec<String> = params.iter().enumerate().map(|(i, p)| {
1348+ let p_name = p.split('=').next().unwrap_or(p);
1349+ let default = p.split('=').nth(1).unwrap_or(p_name);
1350+ format!("{} ${{{}:{}}}", p_name, i + 1, default)
1351+ }).collect();
1352+ format!("@{} [{}]", name, param_snippets.join(", "))
1353+ };
1354+ let mut ci = CompletionItem {
1355+ label: format!("@{}", name),
1356+ kind: Some(CompletionItemKind::FUNCTION),
1357+ detail: Some(detail),
1358+ text_edit: Some(CompletionTextEdit::Edit(TextEdit {
1359+ range,
1360+ new_text: insert_text,
1361+ })),
1362+ ..Default::default()
1363+ };
1364+ if !params.is_empty() {
1365+ ci.insert_text_format = Some(tower_lsp::lsp_types::InsertTextFormat::SNIPPET);
1366+ }
1367+ items.push(ci);
13441368 }
13451369 }
13461370 }
@@ -2485,6 +2509,42 @@ fn code_actions(
24852509 }
24862510 }
24872511
2512+ // Quick-fix: remove unused @mixin
2513+ if msg.contains("unused mixin") {
2514+ let line = diag.range.start.line as usize;
2515+ let lines: Vec<&str> = text.lines().collect();
2516+ if let Some(source_line) = lines.get(line) {
2517+ let trimmed = source_line.trim_start();
2518+ if trimmed.starts_with("@mixin ") {
2519+ let mixin_name = trimmed.strip_prefix("@mixin ")
2520+ .and_then(|r| {
2521+ let r = r.trim();
2522+ r.find('[').map(|b| r[..b].trim()).or_else(|| r.split_whitespace().next())
2523+ })
2524+ .unwrap_or("?");
2525+ let edit = TextEdit {
2526+ range: Range::new(
2527+ Position::new(diag.range.start.line, 0),
2528+ Position::new(diag.range.start.line + 1, 0),
2529+ ),
2530+ new_text: String::new(),
2531+ };
2532+ let mut changes = HashMap::new();
2533+ changes.insert(uri.clone(), vec![edit]);
2534+ actions.push(CodeActionOrCommand::CodeAction(CodeAction {
2535+ title: format!("Remove unused mixin '{}'", mixin_name),
2536+ kind: Some(CodeActionKind::QUICKFIX),
2537+ diagnostics: Some(vec![diag.clone()]),
2538+ edit: Some(WorkspaceEdit {
2539+ changes: Some(changes),
2540+ ..Default::default()
2541+ }),
2542+ ..Default::default()
2543+ }));
2544+ }
2545+ }
2546+ }
2547+
24882548 // Quick-fix: add alt attribute to @image
24892549 if msg.contains("@image should have") && msg.contains("alt") {
24902550 let line = diag.range.start.line as usize;
@@ -2929,6 +2989,44 @@ fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
29292989 let mut prev_line: u32 = 0;
29302990 let mut prev_start: u32 = 0;
29312991
2992+ // Build set of unused variables by parsing diagnostics
2993+ let result = htmlang::parser::parse(text);
2994+ let mut unused_vars: std::collections::HashSet<String> = std::collections::HashSet::new();
2995+ for d in &result.diagnostics {
2996+ if d.message.contains("unused variable '$") {
2997+ if let Some(start) = d.message.find("'$") {
2998+ let rest = &d.message[start + 2..];
2999+ if let Some(end) = rest.find('\'') {
3000+ unused_vars.insert(rest[..end].to_string());
3001+ }
3002+ }
3003+ }
3004+ if d.message.contains("unused function '@") {
3005+ if let Some(start) = d.message.find("'@") {
3006+ let rest = &d.message[start + 2..];
3007+ if let Some(end) = rest.find('\'') {
3008+ unused_vars.insert(format!("@{}", &rest[..end]));
3009+ }
3010+ }
3011+ }
3012+ if d.message.contains("unused define '$") {
3013+ if let Some(start) = d.message.find("'$") {
3014+ let rest = &d.message[start + 2..];
3015+ if let Some(end) = rest.find('\'') {
3016+ unused_vars.insert(rest[..end].to_string());
3017+ }
3018+ }
3019+ }
3020+ if d.message.contains("unused mixin '") {
3021+ if let Some(start) = d.message.find("unused mixin '") {
3022+ let rest = &d.message[start + 14..];
3023+ if let Some(end) = rest.find('\'') {
3024+ unused_vars.insert(rest[..end].to_string());
3025+ }
3026+ }
3027+ }
3028+ }
3029+
29323030 for (line_idx, line) in text.lines().enumerate() {
29333031 let trimmed = line.trim();
29343032 let line_num = line_idx as u32;
@@ -2936,7 +3034,7 @@ fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
29363034 // Detect comments
29373035 if trimmed.starts_with("--") {
29383036 let col = (line.len() - trimmed.len()) as u32;
2939− push_token(&mut tokens, &mut prev_line, &mut prev_start, line_num, col, trimmed.len() as u32, 4);
3037+ push_token(&mut tokens, &mut prev_line, &mut prev_start, line_num, col, trimmed.len() as u32, 4, 0);
29403038 continue;
29413039 }
29423040
@@ -2957,13 +3055,20 @@ fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
29573055 | "@match" | "@case" | "@default" | "@slot" | "@children"
29583056 | "@warn" | "@debug" | "@lang" | "@favicon" | "@fragment"
29593057 | "@unless" | "@og" | "@breakpoint"
2960− | "@canonical" | "@base" | "@font-face" | "@json-ld" => 0, // keyword
3058+ | "@canonical" | "@base" | "@font-face" | "@json-ld"
3059+ | "@mixin" | "@assert" | "@theme" | "@deprecated" | "@extends"
3060+ | "@use" => 0, // keyword
29613061 _ => {
29623062 // Check if it's a user function call (starts with @ but not a builtin element)
29633063 if is_builtin_element(word) { 0 } else { 2 } // function
29643064 }
29653065 };
2966− push_token(&mut tokens, &mut prev_line, &mut prev_start, line_num, start as u32, (i - start) as u32, token_type);
3066+ // Mark unused @fn definitions with deprecated modifier (dimmed)
3067+ let modifier = if (trimmed.starts_with("@fn ") || trimmed.starts_with("@define ") || trimmed.starts_with("@mixin ")) && word != "@fn" && word != "@define" && word != "@mixin" {
3068+ let name_part = &word[1..]; // strip @
3069+ if unused_vars.contains(&format!("@{}", name_part)) { 1 } else { 0 }
3070+ } else { 0 };
3071+ push_token(&mut tokens, &mut prev_line, &mut prev_start, line_num, start as u32, (i - start) as u32, token_type, modifier);
29673072 } else if bytes[i] == b'$' {
29683073 let start = i;
29693074 i += 1;
@@ -2971,7 +3076,10 @@ fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
29713076 i += 1;
29723077 }
29733078 if i > start + 1 {
2974− push_token(&mut tokens, &mut prev_line, &mut prev_start, line_num, start as u32, (i - start) as u32, 1); // variable
3079+ // Check if this is an unused variable definition on a @let line
3080+ let var_name = &line[start + 1..i];
3081+ let modifier = if trimmed.starts_with("@let ") && unused_vars.contains(var_name) { 1 } else { 0 };
3082+ push_token(&mut tokens, &mut prev_line, &mut prev_start, line_num, start as u32, (i - start) as u32, 1, modifier); // variable
29753083 }
29763084 } else {
29773085 i += 1;
@@ -3005,6 +3113,7 @@ fn push_token(
30053113 start: u32,
30063114 length: u32,
30073115 token_type: u32,
3116+ token_modifiers_bitset: u32,
30083117) {
30093118 let delta_line = line - *prev_line;
30103119 let delta_start = if delta_line == 0 {
@@ -3017,7 +3126,7 @@ fn push_token(
30173126 delta_start,
30183127 length,
30193128 token_type,
3020− token_modifiers_bitset: 0,
3129+ token_modifiers_bitset,
30213130 });
30223131 *prev_line = line;
30233132 *prev_start = start;