@@ -16,74 +16,78 @@ pub(crate) fn document_symbols(text: &str) -> Vec<SymbolInformation> {
16 let trimmed = line.trim();
16 let trimmed = line.trim();
17 let line_num = i as u32;
17 let line_num = i as u32;
19− // @fn definitions
19+ // @let definitions (variables, attribute bundles, and components)
20− if let Some(rest) = trimmed.strip_prefix("@fn ") {
20+ if let Some(rest) = trimmed.strip_prefix("@let ") {
21+ let rest_trimmed = rest.trim();
21− let parts: Vec<&str> = rest.split_whitespace().collect();
22+ let parts: Vec<&str> = rest_trimmed.split_whitespace().collect();
22− if let Some(name) = parts.first() {
23+ if let Some(&name) = parts.first() {
24+ let value_part = rest_trimmed.get(name.len()..).unwrap_or("").trim_start();
28+ .map(|l| l.starts_with(" ") || l.starts_with('\t'))
32+ && (value_part.is_empty() || value_part.starts_with('$'))
34+ // Component/function definition
23− let params = parts[1..].join(" ");
35+ let params = parts[1..].join(" ");
24− let detail = if params.is_empty() {
36+ let detail = if params.is_empty() {
27− Some(format!("({})", params))
39+ Some(format!("({})", params))
29− symbols.push(SymbolInformation {
41+ symbols.push(SymbolInformation {
30− name: format!("@{}", name),
42+ name: format!("@{}", name),
31− kind: SymbolKind::FUNCTION,
43+ kind: SymbolKind::FUNCTION,
32− tags: None,
44+ tags: None,
33− deprecated: None,
45+ deprecated: None,
34− location: Location {
46+ location: Location {
35− uri: Url::parse("file:///").unwrap(), // replaced by caller
47+ uri: Url::parse("file:///").unwrap(), // replaced by caller
49+ Position::new(line_num, 0),
50+ Position::new(line_num, line.len() as u32),
53+ container_name: detail,
55+ } else if value_part.starts_with('[') {
57+ symbols.push(SymbolInformation {
58+ name: format!("${}", name),
59+ kind: SymbolKind::CONSTANT,
63+ uri: Url::parse("file:///").unwrap(),
36− range: Range::new(
64+ range: Range::new(
37− Position::new(line_num, 0),
65+ Position::new(line_num, 0),
38− Position::new(line_num, line.len() as u32),
66+ Position::new(line_num, line.len() as u32),
69+ container_name: Some("attribute bundle".to_string()),
71+ } else if !value_part.is_empty() {
73+ symbols.push(SymbolInformation {
74+ name: format!("${}", name),
75+ kind: SymbolKind::VARIABLE,
79+ uri: Url::parse("file:///").unwrap(),
81+ Position::new(line_num, 0),
82+ Position::new(line_num, line.len() as u32),
41− container_name: detail,
85+ container_name: Some(format!("= {}", value_part)),
47− if let Some(rest) = trimmed.strip_prefix("@let ")
48− && let Some((name, value)) = rest.trim().split_once(' ')
50− symbols.push(SymbolInformation {
51− name: format!("${}", name),
52− kind: SymbolKind::VARIABLE,
56− uri: Url::parse("file:///").unwrap(),
58− Position::new(line_num, 0),
59− Position::new(line_num, line.len() as u32),
62− container_name: Some(format!("= {}", value.trim())),
66− // @define definitions
67− if let Some(rest) = trimmed.strip_prefix("@define ")
68− && let Some(bracket) = rest.find('[')
70− let name = rest[..bracket].trim();
71− symbols.push(SymbolInformation {
72− name: format!("${}", name),
73− kind: SymbolKind::CONSTANT,
77− uri: Url::parse("file:///").unwrap(),
79− Position::new(line_num, 0),
80− Position::new(line_num, line.len() as u32),
83− container_name: Some("attribute bundle".to_string()),
87 // @keyframes definitions
91 // @keyframes definitions
88 if let Some(rest) = trimmed.strip_prefix("@keyframes ") {
92 if let Some(rest) = trimmed.strip_prefix("@keyframes ") {
89 let name = rest.trim();
93 let name = rest.trim();
@@ -211,15 +215,15 @@ pub(crate) fn code_actions(
214− // Quick-fix: remove unused @define
218+ // Quick-fix: remove unused attribute bundle (@let name [...])
215− if msg.contains("unused define") {
219+ if msg.contains("unused attribute bundle") {
216 let line = diag.range.start.line as usize;
220 let line = diag.range.start.line as usize;
217 let lines: Vec<&str> = text.lines().collect();
221 let lines: Vec<&str> = text.lines().collect();
218 if let Some(source_line) = lines.get(line) {
222 if let Some(source_line) = lines.get(line) {
219 let trimmed = source_line.trim_start();
223 let trimmed = source_line.trim_start();
220− if trimmed.starts_with("@define ") {
224+ if trimmed.starts_with("@let ") {
221 let def_name = trimmed
225 let def_name = trimmed
222− .strip_prefix("@define ")
226+ .strip_prefix("@let ")
223 .and_then(|r| {
227 .and_then(|r| {
224 let r = r.trim();
228 let r = r.trim();
225 r.find('[')
229 r.find('[')
@@ -237,7 +241,7 @@ pub(crate) fn code_actions(
237 let mut changes = HashMap::new();
241 let mut changes = HashMap::new();
238 changes.insert(uri.clone(), vec![edit]);
242 changes.insert(uri.clone(), vec![edit]);
239 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
243 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
240− title: format!("Remove unused define '${}'", def_name),
244+ title: format!("Remove unused attribute bundle '${}'", def_name),
241 kind: Some(CodeActionKind::QUICKFIX),
245 kind: Some(CodeActionKind::QUICKFIX),
242 diagnostics: Some(vec![diag.clone()]),
246 diagnostics: Some(vec![diag.clone()]),
243 edit: Some(WorkspaceEdit {
247 edit: Some(WorkspaceEdit {
@@ -250,15 +254,15 @@ pub(crate) fn code_actions(
253− // Quick-fix: remove unused @fn function (remove definition and its body)
257+ // Quick-fix: remove unused function (@let name ... with body)
254 if msg.contains("unused function") {
258 if msg.contains("unused function") {
255 let line = diag.range.start.line as usize;
259 let line = diag.range.start.line as usize;
256 let lines: Vec<&str> = text.lines().collect();
260 let lines: Vec<&str> = text.lines().collect();
257 if let Some(source_line) = lines.get(line) {
261 if let Some(source_line) = lines.get(line) {
258 let trimmed = source_line.trim_start();
262 let trimmed = source_line.trim_start();
259− if trimmed.starts_with("@fn ") {
263+ if trimmed.starts_with("@let ") {
260 let fn_name = trimmed
264 let fn_name = trimmed
261− .strip_prefix("@fn ")
265+ .strip_prefix("@let ")
262 .and_then(|r| r.split_whitespace().next())
266 .and_then(|r| r.split_whitespace().next())
263 .unwrap_or("?");
267 .unwrap_or("?");
264 // Find the end of the function body (indented lines below)
268 // Find the end of the function body (indented lines below)
@@ -301,45 +305,6 @@ pub(crate) fn code_actions(
304− // Quick-fix: remove unused @mixin
305− if msg.contains("unused mixin") {
306− let line = diag.range.start.line as usize;
307− let lines: Vec<&str> = text.lines().collect();
308− if let Some(source_line) = lines.get(line) {
309− let trimmed = source_line.trim_start();
310− if trimmed.starts_with("@mixin ") {
311− let mixin_name = trimmed
312− .strip_prefix("@mixin ")
316− .map(|b| r[..b].trim())
317− .or_else(|| r.split_whitespace().next())
320− let edit = TextEdit {
322− Position::new(diag.range.start.line, 0),
323− Position::new(diag.range.start.line + 1, 0),
325− new_text: String::new(),
327− let mut changes = HashMap::new();
328− changes.insert(uri.clone(), vec![edit]);
329− actions.push(CodeActionOrCommand::CodeAction(CodeAction {
330− title: format!("Remove unused mixin '{}'", mixin_name),
331− kind: Some(CodeActionKind::QUICKFIX),
332− diagnostics: Some(vec![diag.clone()]),
333− edit: Some(WorkspaceEdit {
334− changes: Some(changes),
335− ..Default::default()
337− ..Default::default()
343 // Quick-fix: add alt attribute to @image
308 // Quick-fix: add alt attribute to @image
344 if msg.contains("@image should have") && msg.contains("alt") {
309 if msg.contains("@image should have") && msg.contains("alt") {
345 let line = diag.range.start.line as usize;
310 let line = diag.range.start.line as usize;
@@ -459,7 +424,7 @@ pub(crate) fn code_actions(
461 // Quick-fix: auto-import suggestion for unknown element @name
426 // Quick-fix: auto-import suggestion for unknown element @name
462− // Searches current directory and subdirectories for @fn definitions
427+ // Searches current directory and subdirectories for component definitions
463 if msg.contains("unknown element @")
428 if msg.contains("unknown element @")
464 && let Some(fn_name) = extract_between(msg, "unknown element @", ",")
429 && let Some(fn_name) = extract_between(msg, "unknown element @", ",")
465 .or_else(|| extract_between(msg, "unknown element @", ""))
430 .or_else(|| extract_between(msg, "unknown element @", ""))
@@ -495,7 +460,7 @@ pub(crate) fn code_actions(
495 if let Ok(content) = std::fs::read_to_string(&path) {
460 if let Ok(content) = std::fs::read_to_string(&path) {
496 let defines_fn = content.lines().any(|l| {
461 let defines_fn = content.lines().any(|l| {
497 let t = l.trim();
462 let t = l.trim();
498− if let Some(rest) = t.strip_prefix("@fn ") {
463+ if let Some(rest) = t.strip_prefix("@let ") {
499 rest.split_whitespace().next() == Some(fn_name)
464 rest.split_whitespace().next() == Some(fn_name)
@@ -577,7 +542,7 @@ pub(crate) fn code_actions(
580− // Refactoring: extract selection to @fn
545+ // Refactoring: extract selection to @let component
581 if selection.start.line != selection.end.line {
546 if selection.start.line != selection.end.line {
582 let lines: Vec<&str> = text.lines().collect();
547 let lines: Vec<&str> = text.lines().collect();
583 let start_line = selection.start.line as usize;
548 let start_line = selection.start.line as usize;
@@ -594,7 +559,7 @@ pub(crate) fn code_actions(
595 .unwrap_or(0);
560 .unwrap_or(0);
597− // Build the function body with two-space indentation relative to @fn
562+ // Build the function body with two-space indentation relative to @let
598 let fn_body: String = selected
563 let fn_body: String = selected
600 .map(|l| {
565 .map(|l| {
@@ -611,11 +576,11 @@ pub(crate) fn code_actions(
612 .collect();
577 .collect();
614− let fn_def = format!("@fn extracted\n{}", fn_body);
579+ let fn_def = format!("@let extracted\n{}", fn_body);
615 let indent = " ".repeat(min_indent);
580 let indent = " ".repeat(min_indent);
616 let fn_call = format!("{}@extracted", indent);
581 let fn_call = format!("{}@extracted", indent);
618− // Build edits: replace selected lines with @extracted call, and insert @fn definition at top
583+ // Build edits: replace selected lines with @extracted call, and insert @let definition at top
619 let replace_edit = TextEdit {
584 let replace_edit = TextEdit {
620 range: Range::new(
585 range: Range::new(
621 Position::new(selection.start.line, 0),
586 Position::new(selection.start.line, 0),
@@ -630,7 +595,7 @@ pub(crate) fn code_actions(
630 let mut changes = HashMap::new();
595 let mut changes = HashMap::new();
631 changes.insert(uri.clone(), vec![insert_edit, replace_edit]);
596 changes.insert(uri.clone(), vec![insert_edit, replace_edit]);
632 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
597 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
633− title: "Extract to @fn".into(),
598+ title: "Extract to @let component".into(),
634 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
599 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
635 diagnostics: None,
600 diagnostics: None,
636 edit: Some(WorkspaceEdit {
601 edit: Some(WorkspaceEdit {
@@ -643,8 +608,8 @@ pub(crate) fn code_actions(
646− // Refactoring: extract attributes to @define
611+ // Refactoring: extract attributes to @let attribute bundle
647− // Works on a single line with [attrs] — extracts attrs into a @define
612+ // Works on a single line with [attrs] — extracts attrs into a @let
649 let lines: Vec<&str> = text.lines().collect();
614 let lines: Vec<&str> = text.lines().collect();
650 let line_idx = selection.start.line as usize;
615 let line_idx = selection.start.line as usize;
@@ -658,7 +623,7 @@ pub(crate) fn code_actions(
658 let attr_count = attrs_str.split(',').count();
623 let attr_count = attrs_str.split(',').count();
659 if attr_count >= 2 {
624 if attr_count >= 2 {
660 let define_name = "extracted-style";
625 let define_name = "extracted-style";
661− let define_line = format!("@define {} [{}]\n", define_name, attrs_str.trim());
626+ let define_line = format!("@let {} [{}]\n", define_name, attrs_str.trim());
662 let indent = " ".repeat(line.len() - line.trim_start().len());
627 let indent = " ".repeat(line.len() - line.trim_start().len());
664 // Replace [attrs] with [$extracted-style]
629 // Replace [attrs] with [$extracted-style]
@@ -684,7 +649,7 @@ pub(crate) fn code_actions(
684 let mut changes = HashMap::new();
649 let mut changes = HashMap::new();
685 changes.insert(uri.clone(), vec![insert_edit, replace_edit]);
650 changes.insert(uri.clone(), vec![insert_edit, replace_edit]);
686 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
651 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
687− title: "Extract to @define".into(),
652+ title: "Extract to @let attribute bundle".into(),
688 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
653 kind: Some(CodeActionKind::REFACTOR_EXTRACT),
689 diagnostics: None,
654 diagnostics: None,
690 edit: Some(WorkspaceEdit {
655 edit: Some(WorkspaceEdit {
@@ -921,8 +886,8 @@ pub(crate) fn folding_ranges(text: &str) -> Vec<FoldingRange> {
921 let mut i = 0;
886 let mut i = 0;
922 while i < lines.len() {
887 while i < lines.len() {
923 let trimmed = lines[i].trim();
888 let trimmed = lines[i].trim();
924− // Fold blocks that start with @fn, @if, @else, @each, @match, @style, @head, @keyframes, @define
889+ // Fold blocks that start with @let (with body), @if, @else, @each, @match, @style, @head, @keyframes
925− if trimmed.starts_with("@fn ")
890+ if trimmed.starts_with("@let ")
926 || trimmed.starts_with("@if ")
891 || trimmed.starts_with("@if ")
927 || trimmed == "@else"
892 || trimmed == "@else"
928 || trimmed.starts_with("@else if ")
893 || trimmed.starts_with("@else if ")
@@ -1066,25 +1031,21 @@ pub(crate) fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
1067 let word = &line[start..i];
1032 let word = &line[start..i];
1068 let token_type = match word {
1033 let token_type = match word {
1069− "@page" | "@let" | "@define" | "@fn" | "@if" | "@else" | "@each"
1034+ "@page" | "@let" | "@if" | "@else" | "@each"
1070 | "@include" | "@import" | "@meta" | "@head" | "@style" | "@keyframes"
1035 | "@include" | "@import" | "@meta" | "@head" | "@style" | "@keyframes"
1071 | "@match" | "@case" | "@default" | "@slot" | "@children" | "@warn"
1036 | "@match" | "@case" | "@default" | "@slot" | "@children" | "@warn"
1072 | "@debug" | "@lang" | "@favicon" | "@fragment" | "@unless" | "@og"
1037 | "@debug" | "@lang" | "@favicon" | "@fragment" | "@unless" | "@og"
1073 | "@breakpoint" | "@canonical" | "@base" | "@font-face" | "@json-ld"
1038 | "@breakpoint" | "@canonical" | "@base" | "@font-face" | "@json-ld"
1074− | "@mixin" | "@assert" | "@theme" | "@deprecated" | "@extends" | "@use"
1039+ | "@assert" | "@theme" | "@deprecated" | "@extends" | "@use"
1075 | "@data" | "@env" | "@fetch" | "@svg" | "@css-property" => 0, // keyword
1040 | "@data" | "@env" | "@fetch" | "@svg" | "@css-property" => 0, // keyword
1077 // Check if it's a user function call (starts with @ but not a builtin element)
1042 // Check if it's a user function call (starts with @ but not a builtin element)
1078 if is_builtin_element(word) { 0 } else { 2 } // function
1043 if is_builtin_element(word) { 0 } else { 2 } // function
1081− // Mark unused @fn definitions with deprecated modifier (dimmed)
1046+ // Mark unused definitions with deprecated modifier (dimmed)
1082− let modifier = if (trimmed.starts_with("@fn ")
1083− || trimmed.starts_with("@define ")
1047+ let modifier = if trimmed.starts_with("@let ")
1084− || trimmed.starts_with("@mixin "))
1086− && word != "@define"
1048+ && word != "@let"
1087− && word != "@mixin"
1089 let name_part = &word[1..]; // strip @
1050 let name_part = &word[1..]; // strip @
1090 if unused_vars.contains(&format!("@{}", name_part)) {
1051 if unused_vars.contains(&format!("@{}", name_part)) {
@@ -1333,10 +1294,10 @@ pub(crate) fn get_signature_help(text: &str, position: Position) -> Option<Signa
1334 let inside_args = in_brackets(before);
1295 let inside_args = in_brackets(before);
1336− // Find the @fn definition
1297+ // Find the @let component definition
1337 for (line_idx, line_text) in text.lines().enumerate() {
1298 for (line_idx, line_text) in text.lines().enumerate() {
1338 let t = line_text.trim_start();
1299 let t = line_text.trim_start();
1339− if let Some(rest) = t.strip_prefix("@fn ") {
1300+ if let Some(rest) = t.strip_prefix("@let ") {
1340 let rest = rest.trim();
1301 let rest = rest.trim();
1341 let def_name_end = rest
1302 let def_name_end = rest
1342 .find(|c: char| !c.is_alphanumeric() && c != '_' && c != '-')
1303 .find(|c: char| !c.is_alphanumeric() && c != '_' && c != '-')