@@ -67,7 +67,15 @@ impl LanguageServer for Backend {
6767 work_done_progress_options: Default::default(),
6868 })),
6969 document_symbol_provider: Some(OneOf::Left(true)),
70− code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
70+ code_action_provider: Some(CodeActionProviderCapability::Options(
71+ CodeActionOptions {
72+ code_action_kinds: Some(vec![
73+ CodeActionKind::QUICKFIX,
74+ CodeActionKind::REFACTOR_EXTRACT,
75+ ]),
76+ ..Default::default()
77+ },
78+ )),
7179 color_provider: Some(ColorProviderCapability::Simple(true)),
7280 folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
7381 semantic_tokens_provider: Some(
@@ -485,6 +493,11 @@ fn completions(text: &str, position: Position) -> Vec<CompletionItem> {
485493 }
486494 }
487495
496+ // Color value completions after color-related attributes
497+ if let Some(colors) = color_value_completions(before, edit_range) {
498+ return colors;
499+ }
500+
488501 return attr_completions(edit_range);
489502 }
490503
@@ -1100,6 +1113,94 @@ fn state_attr_completions(prefix: &str, range: Range) -> Vec<CompletionItem> {
11001113 .collect()
11011114}
11021115
1116+fn color_value_completions(before: &str, range: Range) -> Option<Vec<CompletionItem>> {
1117+ // Find the preceding attribute name before the cursor value position.
1118+ // Inside brackets, attributes are comma-separated. Look for the last attribute token
1119+ // before the current value position. Pattern: "attr value" or "attr " at end.
1120+ let bracket_content = before.rsplit('[').next()?;
1121+ // Split by commas to get the current segment
1122+ let segment = bracket_content.rsplit(',').next()?.trim();
1123+
1124+ // Check if the first word in this segment is a color-related attribute
1125+ let attr = segment.split_whitespace().next()?;
1126+
1127+ // Strip state prefix (e.g., "hover:background" -> "background")
1128+ let base_attr = if let Some(pos) = attr.rfind(':') {
1129+ &attr[pos + 1..]
1130+ } else {
1131+ attr
1132+ };
1133+
1134+ if !matches!(
1135+ base_attr,
1136+ "background" | "color" | "border" | "border-top" | "border-bottom"
1137+ | "border-left" | "border-right" | "accent-color" | "caret-color"
1138+ | "text-decoration-color" | "outline"
1139+ ) {
1140+ return None;
1141+ }
1142+
1143+ // Only show colors if we're in the value position (at least one space after the attr name)
1144+ let after_attr = &segment[attr.len()..];
1145+ if !after_attr.starts_with(' ') {
1146+ return None;
1147+ }
1148+
1149+ let colors: &[(&str, &str, &str)] = &[
1150+ ("white", "#ffffff", "White"),
1151+ ("black", "#000000", "Black"),
1152+ ("red", "#ef4444", "Red"),
1153+ ("orange", "#f97316", "Orange"),
1154+ ("yellow", "#eab308", "Yellow"),
1155+ ("green", "#22c55e", "Green"),
1156+ ("blue", "#3b82f6", "Blue"),
1157+ ("indigo", "#6366f1", "Indigo"),
1158+ ("purple", "#a855f7", "Purple"),
1159+ ("pink", "#ec4899", "Pink"),
1160+ ("gray", "#6b7280", "Gray"),
1161+ ("slate", "#64748b", "Slate"),
1162+ ("zinc", "#71717a", "Zinc"),
1163+ ("neutral", "#737373", "Neutral"),
1164+ ("stone", "#78716c", "Stone"),
1165+ ("amber", "#f59e0b", "Amber"),
1166+ ("lime", "#84cc16", "Lime"),
1167+ ("emerald", "#10b981", "Emerald"),
1168+ ("teal", "#14b8a6", "Teal"),
1169+ ("cyan", "#06b6d4", "Cyan"),
1170+ ("sky", "#0ea5e9", "Sky"),
1171+ ("violet", "#8b5cf6", "Violet"),
1172+ ("fuchsia", "#d946ef", "Fuchsia"),
1173+ ("rose", "#f43f5e", "Rose"),
1174+ ("transparent", "transparent", "Transparent"),
1175+ ("currentColor", "currentColor", "Inherit from parent text color"),
1176+ ];
1177+
1178+ let items: Vec<CompletionItem> = colors.iter().map(|(label, value, detail)| {
1179+ let doc = if value.starts_with('#') {
1180+ format!("{} (`{}`)", detail, value)
1181+ } else {
1182+ detail.to_string()
1183+ };
1184+ CompletionItem {
1185+ label: label.to_string(),
1186+ kind: Some(CompletionItemKind::COLOR),
1187+ detail: Some(doc),
1188+ text_edit: Some(CompletionTextEdit::Edit(TextEdit {
1189+ range,
1190+ new_text: value.to_string(),
1191+ })),
1192+ documentation: if value.starts_with('#') {
1193+ Some(Documentation::String(value.to_string()))
1194+ } else {
1195+ None
1196+ },
1197+ ..Default::default()
1198+ }
1199+ }).collect();
1200+
1201+ Some(items)
1202+}
1203+
11031204fn variable_completions(text: &str, range: Range) -> Vec<CompletionItem> {
11041205 let mut items = Vec::new();
11051206
@@ -2018,7 +2119,7 @@ fn document_symbols(text: &str) -> Vec<SymbolInformation> {
20182119
20192120fn code_actions(
20202121 text: &str,
2021− _range: &Range,
2122+ selection: &Range,
20222123 diagnostics: &[Diagnostic],
20232124 uri: &Url,
20242125) -> Vec<CodeActionOrCommand> {
@@ -2087,6 +2188,9 @@ fn code_actions(
20872188 if let Some(source_line) = lines.get(line) {
20882189 let trimmed = source_line.trim_start();
20892190 if trimmed.starts_with("@let ") {
2191+ let var_name = trimmed.strip_prefix("@let ")
2192+ .and_then(|r| r.trim().split_whitespace().next())
2193+ .unwrap_or("?");
20902194 let edit = TextEdit {
20912195 range: Range::new(
20922196 Position::new(diag.range.start.line, 0),
@@ -2097,7 +2201,7 @@ fn code_actions(
20972201 let mut changes = HashMap::new();
20982202 changes.insert(uri.clone(), vec![edit]);
20992203 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
2100− title: "Remove unused variable".into(),
2204+ title: format!("Remove unused variable '${}'", var_name),
21012205 kind: Some(CodeActionKind::QUICKFIX),
21022206 diagnostics: Some(vec![diag.clone()]),
21032207 edit: Some(WorkspaceEdit {
@@ -2117,6 +2221,12 @@ fn code_actions(
21172221 if let Some(source_line) = lines.get(line) {
21182222 let trimmed = source_line.trim_start();
21192223 if trimmed.starts_with("@define ") {
2224+ let def_name = trimmed.strip_prefix("@define ")
2225+ .and_then(|r| {
2226+ let r = r.trim();
2227+ r.find('[').map(|b| r[..b].trim()).or_else(|| r.split_whitespace().next())
2228+ })
2229+ .unwrap_or("?");
21202230 let edit = TextEdit {
21212231 range: Range::new(
21222232 Position::new(diag.range.start.line, 0),
@@ -2127,7 +2237,57 @@ fn code_actions(
21272237 let mut changes = HashMap::new();
21282238 changes.insert(uri.clone(), vec![edit]);
21292239 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
2130− title: "Remove unused define".into(),
2240+ title: format!("Remove unused define '${}'", def_name),
2241+ kind: Some(CodeActionKind::QUICKFIX),
2242+ diagnostics: Some(vec![diag.clone()]),
2243+ edit: Some(WorkspaceEdit {
2244+ changes: Some(changes),
2245+ ..Default::default()
2246+ }),
2247+ ..Default::default()
2248+ }));
2249+ }
2250+ }
2251+ }
2252+
2253+ // Quick-fix: remove unused @fn function (remove definition and its body)
2254+ if msg.contains("unused function") {
2255+ let line = diag.range.start.line as usize;
2256+ let lines: Vec<&str> = text.lines().collect();
2257+ if let Some(source_line) = lines.get(line) {
2258+ let trimmed = source_line.trim_start();
2259+ if trimmed.starts_with("@fn ") {
2260+ let fn_name = trimmed.strip_prefix("@fn ")
2261+ .and_then(|r| r.split_whitespace().next())
2262+ .unwrap_or("?");
2263+ // Find the end of the function body (indented lines below)
2264+ let start_indent = source_line.len() - trimmed.len();
2265+ let mut end_line = line;
2266+ let mut j = line + 1;
2267+ while j < lines.len() {
2268+ let l = lines[j];
2269+ if l.trim().is_empty() {
2270+ j += 1;
2271+ continue;
2272+ }
2273+ let indent = l.len() - l.trim_start().len();
2274+ if indent <= start_indent {
2275+ break;
2276+ }
2277+ end_line = j;
2278+ j += 1;
2279+ }
2280+ let edit = TextEdit {
2281+ range: Range::new(
2282+ Position::new(diag.range.start.line, 0),
2283+ Position::new(end_line as u32 + 1, 0),
2284+ ),
2285+ new_text: String::new(),
2286+ };
2287+ let mut changes = HashMap::new();
2288+ changes.insert(uri.clone(), vec![edit]);
2289+ actions.push(CodeActionOrCommand::CodeAction(CodeAction {
2290+ title: format!("Remove unused function '@{}'", fn_name),
21312291 kind: Some(CodeActionKind::QUICKFIX),
21322292 diagnostics: Some(vec![diag.clone()]),
21332293 edit: Some(WorkspaceEdit {
@@ -2193,6 +2353,135 @@ fn code_actions(
21932353 }
21942354 }
21952355 }
2356+
2357+ // Quick-fix: auto-import suggestion for unknown element @name
2358+ if msg.contains("unknown element @") {
2359+ if let Some(fn_name) = extract_between(msg, "unknown element @", ",")
2360+ .or_else(|| extract_between(msg, "unknown element @", ""))
2361+ {
2362+ let fn_name = fn_name.trim();
2363+ if !fn_name.is_empty() {
2364+ if let Ok(file_path) = uri.to_file_path() {
2365+ if let Some(dir) = file_path.parent() {
2366+ if let Ok(entries) = std::fs::read_dir(dir) {
2367+ for entry in entries.flatten() {
2368+ let path = entry.path();
2369+ if path.extension().and_then(|e| e.to_str()) != Some("hl") {
2370+ continue;
2371+ }
2372+ // Skip the current file
2373+ if path == file_path {
2374+ continue;
2375+ }
2376+ if let Ok(content) = std::fs::read_to_string(&path) {
2377+ let defines_fn = content.lines().any(|l| {
2378+ let t = l.trim();
2379+ if let Some(rest) = t.strip_prefix("@fn ") {
2380+ rest.split_whitespace().next() == Some(fn_name)
2381+ } else {
2382+ false
2383+ }
2384+ });
2385+ if defines_fn {
2386+ if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
2387+ // Check if already imported
2388+ let already_imported = text.lines().any(|l| {
2389+ let t = l.trim();
2390+ t == format!("@import {}", name)
2391+ || t == format!("@include {}", name)
2392+ });
2393+ if !already_imported {
2394+ let import_line = format!("@import {}\n", name);
2395+ let edit = TextEdit {
2396+ range: Range::new(
2397+ Position::new(0, 0),
2398+ Position::new(0, 0),
2399+ ),
2400+ new_text: import_line,
2401+ };
2402+ let mut changes = HashMap::new();
2403+ changes.insert(uri.clone(), vec![edit]);
2404+ actions.push(CodeActionOrCommand::CodeAction(CodeAction {
2405+ title: format!("Add '@import {}' for @{}", name, fn_name),
2406+ kind: Some(CodeActionKind::QUICKFIX),
2407+ diagnostics: Some(vec![diag.clone()]),
2408+ edit: Some(WorkspaceEdit {
2409+ changes: Some(changes),
2410+ ..Default::default()
2411+ }),
2412+ ..Default::default()
2413+ }));
2414+ }
2415+ }
2416+ }
2417+ }
2418+ }
2419+ }
2420+ }
2421+ }
2422+ }
2423+ }
2424+ }
2425+ }
2426+
2427+ // Refactoring: extract selection to @fn
2428+ if selection.start.line != selection.end.line {
2429+ let lines: Vec<&str> = text.lines().collect();
2430+ let start_line = selection.start.line as usize;
2431+ let end_line = (selection.end.line as usize).min(lines.len().saturating_sub(1));
2432+ if start_line < lines.len() && end_line < lines.len() {
2433+ // Collect selected lines
2434+ let selected: Vec<&str> = lines[start_line..=end_line].to_vec();
2435+ if !selected.is_empty() {
2436+ // Determine the minimum indentation of selected lines (ignoring blank lines)
2437+ let min_indent = selected.iter()
2438+ .filter(|l| !l.trim().is_empty())
2439+ .map(|l| l.len() - l.trim_start().len())
2440+ .min()
2441+ .unwrap_or(0);
2442+
2443+ // Build the function body with two-space indentation relative to @fn
2444+ let fn_body: String = selected.iter()
2445+ .map(|l| {
2446+ if l.trim().is_empty() {
2447+ String::from("\n")
2448+ } else {
2449+ let stripped = if l.len() > min_indent { &l[min_indent..] } else { l.trim_start() };
2450+ format!(" {}\n", stripped)
2451+ }
2452+ })
2453+ .collect();
2454+
2455+ let fn_def = format!("@fn extracted\n{}", fn_body);
2456+ let indent = " ".repeat(min_indent);
2457+ let fn_call = format!("{}@extracted", indent);
2458+
2459+ // Build edits: replace selected lines with @extracted call, and insert @fn definition at top
2460+ let replace_edit = TextEdit {
2461+ range: Range::new(
2462+ Position::new(selection.start.line, 0),
2463+ Position::new(selection.end.line + 1, 0),
2464+ ),
2465+ new_text: format!("{}\n", fn_call),
2466+ };
2467+ let insert_edit = TextEdit {
2468+ range: Range::new(Position::new(0, 0), Position::new(0, 0)),
2469+ new_text: format!("{}\n", fn_def),
2470+ };
2471+ let mut changes = HashMap::new();
2472+ changes.insert(uri.clone(), vec![insert_edit, replace_edit]);
2473+ actions.push(CodeActionOrCommand::CodeAction(CodeAction {
2474+ title: "Extract to @fn".into(),
2475+ kind: Some(CodeActionKind::REFACTOR_EXTRACT),
2476+ diagnostics: None,
2477+ edit: Some(WorkspaceEdit {
2478+ changes: Some(changes),
2479+ ..Default::default()
2480+ }),
2481+ ..Default::default()
2482+ }));
2483+ }
2484+ }
21962485 }
21972486
21982487 actions