@@ -727,6 +727,41 @@ fn snippet_completions(range: Range) -> Vec<CompletionItem> {
727727 "Interactive button with hover effect",
728728 "@el [padding 12 24, background ${1:#3b82f6}, hover:background ${2:#2563eb}, rounded 8, cursor pointer, transition all 0.15s ease] > @link ${3:url}\n @text [color white, bold] ${4:Click me}",
729729 ),
730+ (
731+ "form with inputs",
732+ "Form with labeled inputs and submit button",
733+ "@form [spacing 16]\n @label ${1:Name}\n @input [type text, name ${2:name}, placeholder ${3:Enter name}, required]\n @label ${4:Email}\n @input [type email, name ${5:email}, placeholder ${6:Enter email}, required]\n @button [type submit, padding 12 24, background ${7:#3b82f6}, color white, rounded 8, bold, cursor pointer] Submit",
734+ ),
735+ (
736+ "grid layout",
737+ "Responsive grid with columns",
738+ "@grid [grid-cols ${1:3}, gap ${2:20}]\n @el [padding 20, background ${3:#f3f4f6}, rounded 8]\n ${4:Item 1}\n @el [padding 20, background ${3:#f3f4f6}, rounded 8]\n ${5:Item 2}\n @el [padding 20, background ${3:#f3f4f6}, rounded 8]\n ${6:Item 3}",
739+ ),
740+ (
741+ "footer section",
742+ "Footer with columns and copyright",
743+ "@footer [padding 40, background ${1:#1a1a2e}, color ${2:#ccc}]\n @row [spacing 40, wrap]\n @column [spacing 10, width fill]\n @text [bold, color white] ${3:Company}\n @link ${4:#} ${5:About}\n @link ${6:#} ${7:Contact}\n @column [spacing 10, width fill]\n @text [bold, color white] ${8:Resources}\n @link ${9:#} ${10:Documentation}\n @text [size 14, color #888, center-x] \\u00a9 2026 ${11:Company Name}",
744+ ),
745+ (
746+ "avatar with image",
747+ "Circular avatar with fallback",
748+ "@avatar [width ${1:48}, height ${1:48}, background ${2:#e5e7eb}]\n @image [width ${1:48}, height ${1:48}, object-fit cover, alt ${3:avatar}] ${4:url}",
749+ ),
750+ (
751+ "carousel horizontal",
752+ "Scroll-snap horizontal carousel",
753+ "@carousel [gap ${1:16}, padding ${2:16}]\n @el [width ${3:300}, padding 20, background ${4:#f3f4f6}, rounded 8]\n ${5:Slide 1}\n @el [width ${3:300}, padding 20, background ${4:#f3f4f6}, rounded 8]\n ${6:Slide 2}\n @el [width ${3:300}, padding 20, background ${4:#f3f4f6}, rounded 8]\n ${7:Slide 3}",
754+ ),
755+ (
756+ "dark mode toggle",
757+ "Element with light/dark mode styles",
758+ "@el [padding 20, background ${1:white}, dark:background ${2:#1a1a2e}, color ${3:#333}, dark:color ${4:#eee}, rounded 8, transition all 0.2s ease]\n ${5:Content}",
759+ ),
760+ (
761+ "truncated text",
762+ "Text with ellipsis overflow",
763+ "@text [max-width ${1:200}, truncate] ${2:Long text that will be truncated...}",
764+ ),
730765 ];
731766
732767 snippets
@@ -1050,6 +1085,24 @@ fn attr_completions(range: Range) -> Vec<CompletionItem> {
10501085 ("allow", "Iframe permissions policy", true),
10511086 ("allowfullscreen", "Allow iframe fullscreen", false),
10521087 ("target", "Link/form target (_blank/_self/_parent/_top)", true),
1088+ // CSS shorthands
1089+ ("truncate", "Truncate text with ellipsis (single line)", false),
1090+ ("line-clamp", "Clamp text to N lines with ellipsis", true),
1091+ ("blur", "Apply blur filter (px)", true),
1092+ ("backdrop-blur", "Apply backdrop blur filter (px)", true),
1093+ ("no-scrollbar", "Hide scrollbar while keeping overflow", false),
1094+ ("skeleton", "Add shimmer loading skeleton animation", false),
1095+ ("gradient", "Linear gradient (color1 color2 [angle])", true),
1096+ // Direction
1097+ ("direction", "Text direction (ltr/rtl)", true),
1098+ // Container query prefixes
1099+ ("cq-sm:", "Container query at 640px+", false),
1100+ ("cq-md:", "Container query at 768px+", false),
1101+ ("cq-lg:", "Container query at 1024px+", false),
1102+ // Pseudo-elements
1103+ ("before:", "Style ::before pseudo-element", false),
1104+ ("after:", "Style ::after pseudo-element", false),
1105+ ("selection:", "Style text selection", false),
10531106 ]
10541107 .iter()
10551108 .map(|(name, detail, takes_value)| {
@@ -1357,20 +1410,54 @@ fn hover_variable(text: &str, name: &str) -> Option<String> {
13571410}
13581411
13591412fn hover_user_fn(text: &str, name: &str) -> Option<String> {
1360− for line in text.lines() {
1413+ let lines: Vec<&str> = text.lines().collect();
1414+ for (i, line) in lines.iter().enumerate() {
13611415 let trimmed = line.trim();
13621416 if let Some(rest) = trimmed.strip_prefix("@fn ") {
13631417 let parts: Vec<&str> = rest.split_whitespace().collect();
13641418 if parts.first() == Some(&name) {
13651419 let params = &parts[1..];
1420+
1421+ // Collect doc-comment lines above the @fn (lines starting with --)
1422+ let mut doc_lines: Vec<&str> = Vec::new();
1423+ let mut j = i;
1424+ while j > 0 {
1425+ j -= 1;
1426+ let prev = lines[j].trim();
1427+ if let Some(comment) = prev.strip_prefix("-- ") {
1428+ doc_lines.push(comment);
1429+ } else if prev.starts_with("--") {
1430+ doc_lines.push(&prev[2..]);
1431+ } else {
1432+ break;
1433+ }
1434+ }
1435+ doc_lines.reverse();
1436+
1437+ let doc_str = if doc_lines.is_empty() {
1438+ String::new()
1439+ } else {
1440+ format!("\n\n{}", doc_lines.join("\n"))
1441+ };
1442+
1443+ // Format params showing defaults
13661444 let params_str = if params.is_empty() {
13671445 String::new()
13681446 } else {
1369− format!("\n\nParameters: {}", params.join(", "))
1447+ let formatted: Vec<String> = params.iter().map(|p| {
1448+ if p.contains('=') {
1449+ let (name, default) = p.split_once('=').unwrap();
1450+ format!("{} (default: {})", name, default)
1451+ } else {
1452+ p.to_string()
1453+ }
1454+ }).collect();
1455+ format!("\n\nParameters: {}", formatted.join(", "))
13701456 };
1457+
13711458 return Some(format!(
1372− "**@{}** \u{2014} User function{}",
1373− name, params_str
1459+ "**@{}** \u{2014} User function{}{}",
1460+ name, params_str, doc_str
13741461 ));
13751462 }
13761463 }
@@ -1698,6 +1785,34 @@ fn hover_builtin(word: &str) -> Option<String> {
16981785 "@iframe" => "**@iframe** \u{2014} Embedded page\n\nRenders as `<iframe>`.\n\nUsage: `@iframe [width fill, height 400] https://example.com`\n\nAttributes: `sandbox`, `allow`, `allowfullscreen`",
16991786 "@output" => "**@output** \u{2014} Form output\n\nRenders as `<output>`. Displays calculation results in forms.\n\nUsage: `@output [for a b] Result`",
17001787 "@canvas" => "**@canvas** \u{2014} Drawing surface\n\nRenders as `<canvas>`. Use with `@raw` JavaScript for drawing.\n\nUsage: `@canvas [width 400, height 300, id myCanvas]`",
1788+ // Convenience elements
1789+ "@grid" => "**@grid** \u{2014} CSS Grid container\n\nRenders as `<div>` with `display: grid`.\n\nUsage: `@grid [grid-cols 3, gap 20]`",
1790+ "@stack" => "**@stack** \u{2014} Stack container\n\nRenders as `<div>` with `position: relative`. Children can be absolutely positioned on top of each other.",
1791+ "@spacer" => "**@spacer** \u{2014} Flexible spacer\n\nRenders as `<div>` with `flex: 1`. Pushes siblings apart in flex containers.",
1792+ "@badge" => "**@badge** \u{2014} Badge\n\nRenders as `<span>` with pill shape, centered text.\n\nUsage: `@badge [background #3b82f6, color white] NEW`",
1793+ "@tooltip" => "**@tooltip** \u{2014} Tooltip\n\nRenders as `<span>` with `title` attribute for native tooltips.\n\nUsage: `@tooltip [cursor help] Hover me`",
1794+ "@avatar" => "**@avatar** \u{2014} Avatar\n\nRenders as `<div>` with circular shape, centered content, `overflow: hidden`.\n\nUsage:\n```\n@avatar [width 48, height 48, background #e5e7eb]\n @image [object-fit cover] photo.jpg\n```",
1795+ "@carousel" => "**@carousel** \u{2014} Horizontal carousel\n\nRenders as `<div>` with horizontal scroll-snap.\nChildren auto-receive `scroll-snap-align: start` and `flex-shrink: 0`.\n\nUsage:\n```\n@carousel [gap 16]\n @el [width 300] Slide 1\n @el [width 300] Slide 2\n```",
1796+ "@chip" => "**@chip** \u{2014} Chip / pill\n\nRenders as `<span>` with rounded borders, inline-flex layout.\n\nUsage: `@chip [background #e5e7eb] Category`",
1797+ "@tag" => "**@tag** \u{2014} Tag label\n\nRenders as `<span>` with subtle rounded rectangle, bold small text.\n\nUsage: `@tag [background #dbeafe, color #1e40af] v2.0`",
1798+ // New directives
1799+ "@use" => "**@use** \u{2014} Selective import\n\nImport specific functions/defines from a file.\n\n```\n@use \"buttons.hl\" primary-button, secondary-button\n@use theme.hl card-style\n```\n\nOnly the named definitions are imported.",
1800+ // CSS shorthands
1801+ "truncate" => "**truncate** \u{2014} Truncate with ellipsis\n\nShorthand for: `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`\n\nUsage: `@text [max-width 200, truncate] Long text here...`",
1802+ "line-clamp" => "**line-clamp** `<N>` \u{2014} Multi-line truncation\n\nClamps text to N lines with ellipsis.\n\nUsage: `@paragraph [line-clamp 3] Long paragraph...`",
1803+ "blur" => "**blur** `<value>` \u{2014} Apply blur filter\n\nShorthand for `filter: blur(Npx)`.\n\nUsage: `[blur 4]` \u{2192} `filter: blur(4px)`",
1804+ "backdrop-blur" => "**backdrop-blur** `<value>` \u{2014} Apply backdrop blur\n\nShorthand for `backdrop-filter: blur(Npx)`.\n\nUsage: `[backdrop-blur 10]` \u{2192} `backdrop-filter: blur(10px)`",
1805+ "no-scrollbar" => "**no-scrollbar** \u{2014} Hide scrollbar\n\nHides scrollbar while keeping overflow scrollable.\n\nSets `scrollbar-width: none` and `::-webkit-scrollbar { display: none }`.",
1806+ "skeleton" => "**skeleton** \u{2014} Loading skeleton\n\nAdds a shimmer animation for loading placeholders.\n\nUsage: `@el [width fill, height 20, rounded 4, skeleton]`",
1807+ "gradient" => "**gradient** `<from> <to> [angle]` \u{2014} Linear gradient\n\nShorthand for `background: linear-gradient(...)`.\n\nUsage:\n- `[gradient #fff #000]` \u{2192} top-to-bottom\n- `[gradient #fff #000 45deg]` \u{2192} 45\u{00b0} angle",
1808+ "direction" => "**direction** `<value>` \u{2014} Text direction (`ltr`, `rtl`).",
1809+ // Variable filters
1810+ "\\$|uppercase" => "**|uppercase** \u{2014} Convert variable to UPPERCASE.\n\nUsage: `$name|uppercase`",
1811+ "\\$|lowercase" => "**|lowercase** \u{2014} Convert variable to lowercase.\n\nUsage: `$name|lowercase`",
1812+ "\\$|capitalize" => "**|capitalize** \u{2014} Capitalize first letter.\n\nUsage: `$name|capitalize`",
1813+ "\\$|trim" => "**|trim** \u{2014} Strip leading/trailing whitespace.\n\nUsage: `$name|trim`",
1814+ "\\$|length" => "**|length** \u{2014} Get string length.\n\nUsage: `$name|length`",
1815+ "\\$|reverse" => "**|reverse** \u{2014} Reverse string.\n\nUsage: `$name|reverse`",
17011816 _ => return None,
17021817 };
17031818
@@ -2354,6 +2469,70 @@ fn code_actions(
23542469 }
23552470 }
23562471
2472+ // Quick-fix: add missing type to @input
2473+ if msg.contains("@input missing 'type'") {
2474+ let line = diag.range.start.line as usize;
2475+ let lines: Vec<&str> = text.lines().collect();
2476+ if let Some(source_line) = lines.get(line) {
2477+ if let Some(bracket_pos) = source_line.find('[') {
2478+ let insert_pos = bracket_pos + 1;
2479+ let edit = TextEdit {
2480+ range: Range::new(
2481+ Position::new(diag.range.start.line, insert_pos as u32),
2482+ Position::new(diag.range.start.line, insert_pos as u32),
2483+ ),
2484+ new_text: "type text, ".into(),
2485+ };
2486+ let mut changes = HashMap::new();
2487+ changes.insert(uri.clone(), vec![edit]);
2488+ actions.push(CodeActionOrCommand::CodeAction(CodeAction {
2489+ title: "Add type=\"text\" attribute".into(),
2490+ kind: Some(CodeActionKind::QUICKFIX),
2491+ diagnostics: Some(vec![diag.clone()]),
2492+ edit: Some(WorkspaceEdit {
2493+ changes: Some(changes),
2494+ ..Default::default()
2495+ }),
2496+ ..Default::default()
2497+ }));
2498+ } else if source_line.contains("@input") {
2499+ if let Some(pos) = source_line.find("@input") {
2500+ let after = pos + "@input".len();
2501+ let edit = TextEdit {
2502+ range: Range::new(
2503+ Position::new(diag.range.start.line, after as u32),
2504+ Position::new(diag.range.start.line, after as u32),
2505+ ),
2506+ new_text: " [type text]".into(),
2507+ };
2508+ let mut changes = HashMap::new();
2509+ changes.insert(uri.clone(), vec![edit]);
2510+ actions.push(CodeActionOrCommand::CodeAction(CodeAction {
2511+ title: "Add type=\"text\" attribute".into(),
2512+ kind: Some(CodeActionKind::QUICKFIX),
2513+ diagnostics: Some(vec![diag.clone()]),
2514+ edit: Some(WorkspaceEdit {
2515+ changes: Some(changes),
2516+ ..Default::default()
2517+ }),
2518+ ..Default::default()
2519+ }));
2520+ }
2521+ }
2522+ }
2523+ }
2524+
2525+ // Quick-fix: low contrast ratio — suggest swapping to a high-contrast pair
2526+ if msg.contains("low contrast ratio") {
2527+ actions.push(CodeActionOrCommand::CodeAction(CodeAction {
2528+ title: "Acknowledged: low contrast ratio".into(),
2529+ kind: Some(CodeActionKind::QUICKFIX),
2530+ diagnostics: Some(vec![diag.clone()]),
2531+ is_preferred: Some(false),
2532+ ..Default::default()
2533+ }));
2534+ }
2535+
23572536 // Quick-fix: auto-import suggestion for unknown element @name
23582537 if msg.contains("unknown element @") {
23592538 if let Some(fn_name) = extract_between(msg, "unknown element @", ",")