on master help sign in
Formatting: improve code line wrapping across codebase
cc6e75ciko
crates/htmlang-core/src/codegen.rs+11−23
@@ -3724,12 +3724,12 @@ fn attrs_to_css(
3724 "type" | "placeholder" | "name" | "value" | "disabled" | "required" | "checked"
3724 "type" | "placeholder" | "name" | "value" | "disabled" | "required" | "checked"
3725 | "for" | "action" | "method" | "autocomplete" | "min" | "max" | "step" | "pattern"
3725 | "for" | "action" | "method" | "autocomplete" | "min" | "max" | "step" | "pattern"
3726 | "maxlength" | "rows" | "cols" | "multiple" | "alt" | "role" | "tabindex"
3726 | "maxlength" | "rows" | "cols" | "multiple" | "alt" | "role" | "tabindex"
3727− | "title" | "controls" | "autoplay" | "loop" | "muted" | "playsinline" | "poster" | "preload"
3727+ | "title" | "controls" | "autoplay" | "loop" | "muted" | "playsinline" | "poster"
3728− | "loading" | "decoding" | "ordered" | "src" | "open" | "novalidate" | "low"
3728+ | "preload" | "loading" | "decoding" | "ordered" | "src" | "open" | "novalidate"
3729− | "high" | "optimum" | "colspan" | "rowspan" | "scope" | "inline" | "responsive"
3729+ | "low" | "high" | "optimum" | "colspan" | "rowspan" | "scope" | "inline"
3730− | "datetime" | "media" | "sizes" | "srcset" | "cite" | "list" | "sandbox" | "allow"
3730+ | "responsive" | "datetime" | "media" | "sizes" | "srcset" | "cite" | "list"
3731− | "allowfullscreen" | "referrerpolicy" | "formaction" | "formmethod" | "formtarget"
3731+ | "sandbox" | "allow" | "allowfullscreen" | "referrerpolicy" | "formaction"
3732− | "target" | "autofocus" => {}
3732+ | "formmethod" | "formtarget" | "target" | "autofocus" => {}
3733
3733
3734 _ => {}
3734 _ => {}
3735 }
3735 }
@@ -3795,17 +3795,11 @@ fn substitute_css_vars(css: &str, vars: &[(String, String)]) -> String {
3795
3795
3796 let is_boundary_before = |b: Option<u8>| match b {
3796 let is_boundary_before = |b: Option<u8>| match b {
3797 None => true,
3797 None => true,
3798− Some(c) => matches!(
3798+ Some(c) => matches!(c, b':' | b' ' | b',' | b'(' | b';' | b'{' | b'\n' | b'\t'),
3799− c,
3800− b':' | b' ' | b',' | b'(' | b';' | b'{' | b'\n' | b'\t'
3801− ),
3802 };
3799 };
3803 let is_boundary_after = |b: Option<u8>| match b {
3800 let is_boundary_after = |b: Option<u8>| match b {
3804 None => true,
3801 None => true,
3805− Some(c) => matches!(
3802+ Some(c) => matches!(c, b';' | b'}' | b',' | b' ' | b')' | b'\n' | b'\t'),
3806− c,
3807− b';' | b'}' | b',' | b' ' | b')' | b'\n' | b'\t'
3808− ),
3809 };
3803 };
3810
3804
3811 let bytes = css.as_bytes();
3805 let bytes = css.as_bytes();
@@ -3920,16 +3914,10 @@ fn auto_extract_repeats(
3920 // two-digit indices, which only affects very large extraction counts.
3914 // two-digit indices, which only affects very large extraction counts.
3921 let existing: std::collections::HashSet<&str> =
3915 let existing: std::collections::HashSet<&str> =
3922 existing_vars.iter().map(|(n, _)| n.as_str()).collect();
3916 existing_vars.iter().map(|(n, _)| n.as_str()).collect();
3923− let mut candidates: Vec<(String, usize)> = counts
3917+ let mut candidates: Vec<(String, usize)> =
3924− .into_iter()
3925− .filter(|(_, n)| *n >= 2)
3918+ counts.into_iter().filter(|(_, n)| *n >= 2).collect();
3926− .collect();
3927 // Stable, deterministic ordering — longest values first, tiebreak by text.
3919 // Stable, deterministic ordering — longest values first, tiebreak by text.
3928− candidates.sort_by(|a, b| {
3920+ candidates.sort_by(|a, b| b.0.len().cmp(&a.0.len()).then_with(|| a.0.cmp(&b.0)));
3929− b.0.len()
3930− .cmp(&a.0.len())
3931− .then_with(|| a.0.cmp(&b.0))
3932− });
3933
3921
3934 let mut extracted: Vec<(String, String)> = Vec::new();
3922 let mut extracted: Vec<(String, String)> = Vec::new();
3935 let mut next_idx: usize = 0;
3923 let mut next_idx: usize = 0;
crates/htmlang-core/src/parser.rs+11−9
@@ -420,8 +420,8 @@ impl Parser {
420 let rest = rest.trim();
420 let rest = rest.trim();
421
421
422 // Check if next lines are indented (function/component definition)
422 // Check if next lines are indented (function/component definition)
423− let has_body = self.pos < self.lines.len()
423+ let has_body =
424− && self.lines[self.pos].indent > current_indent;
424+ self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent;
425
425
426 if has_body {
426 if has_body {
427 // Function definition: @let name $param1 $param2=default
427 // Function definition: @let name $param1 $param2=default
@@ -447,9 +447,7 @@ impl Parser {
447
447
448 // Collect body lines (all lines indented deeper than @let)
448 // Collect body lines (all lines indented deeper than @let)
449 let mut body_lines = Vec::new();
449 let mut body_lines = Vec::new();
450− while self.pos < self.lines.len()
451− && self.lines[self.pos].indent > current_indent
450+ while self.pos < self.lines.len() && self.lines[self.pos].indent > current_indent {
452− {
453 body_lines.push(self.lines[self.pos].clone());
451 body_lines.push(self.lines[self.pos].clone());
454 self.pos += 1;
452 self.pos += 1;
455 }
453 }
@@ -481,9 +479,7 @@ impl Parser {
481 if value.starts_with('[') {
479 if value.starts_with('[') {
482 let (attrs, _) = parse_attr_brackets(value, line_num, ctx)?;
480 let (attrs, _) = parse_attr_brackets(value, line_num, ctx)?;
483 ctx.defines.insert(name.to_string(), attrs);
481 ctx.defines.insert(name.to_string(), attrs);
484− ctx.define_lines
485− .entry(name.to_string())
482+ ctx.define_lines.entry(name.to_string()).or_insert(line_num);
486− .or_insert(line_num);
487 return Ok(None);
483 return Ok(None);
488 }
484 }
489
485
@@ -5189,7 +5185,13 @@ fn validate_tree(
5189 // Media-specific attributes only on @video/@audio
5185 // Media-specific attributes only on @video/@audio
5190 if matches!(
5186 if matches!(
5191 base,
5187 base,
5192− "controls" | "autoplay" | "loop" | "muted" | "playsinline" | "poster" | "preload"
5188+ "controls"
5189+ | "autoplay"
5190+ | "loop"
5191+ | "muted"
5192+ | "playsinline"
5193+ | "poster"
5194+ | "preload"
5193 ) && !matches!(elem.kind, ElementKind::Video | ElementKind::Audio)
5195 ) && !matches!(elem.kind, ElementKind::Video | ElementKind::Audio)
5194 {
5196 {
5195 diagnostics.push(Diagnostic {
5197 diagnostics.push(Diagnostic {
src/bin/htmlang_lsp/analysis.rs+9−13
@@ -28,9 +28,7 @@ pub(crate) fn document_symbols(text: &str) -> Vec<SymbolInformation> {
28 .map(|l| l.starts_with(" ") || l.starts_with('\t'))
28 .map(|l| l.starts_with(" ") || l.starts_with('\t'))
29 .unwrap_or(false);
29 .unwrap_or(false);
30
30
31− if has_body
32− && (value_part.is_empty() || value_part.starts_with('$'))
31+ if has_body && (value_part.is_empty() || value_part.starts_with('$')) {
33− {
34 // Component/function definition
32 // Component/function definition
35 let params = parts[1..].join(" ");
33 let params = parts[1..].join(" ");
36 let detail = if params.is_empty() {
34 let detail = if params.is_empty() {
@@ -1031,22 +1029,20 @@ pub(crate) fn semantic_tokens(text: &str) -> Vec<SemanticToken> {
1031 }
1029 }
1032 let word = &line[start..i];
1030 let word = &line[start..i];
1033 let token_type = match word {
1031 let token_type = match word {
1034− "@page" | "@let" | "@if" | "@else" | "@each"
1032+ "@page" | "@let" | "@if" | "@else" | "@each" | "@include" | "@import"
1035− | "@include" | "@import" | "@meta" | "@head" | "@style" | "@keyframes"
1033+ | "@meta" | "@head" | "@style" | "@keyframes" | "@match" | "@case"
1036− | "@match" | "@case" | "@default" | "@slot" | "@children" | "@warn"
1034+ | "@default" | "@slot" | "@children" | "@warn" | "@debug" | "@lang"
1037− | "@debug" | "@lang" | "@favicon" | "@fragment" | "@unless" | "@og"
1035+ | "@favicon" | "@fragment" | "@unless" | "@og" | "@breakpoint"
1038− | "@breakpoint" | "@canonical" | "@base" | "@font-face" | "@json-ld"
1036+ | "@canonical" | "@base" | "@font-face" | "@json-ld" | "@assert" | "@theme"
1039− | "@assert" | "@theme" | "@deprecated" | "@extends" | "@use"
1037+ | "@deprecated" | "@extends" | "@use" | "@data" | "@env" | "@fetch"
1040− | "@data" | "@env" | "@fetch" | "@svg" | "@css-property" => 0, // keyword
1038+ | "@svg" | "@css-property" => 0, // keyword
1041 _ => {
1039 _ => {
1042 // Check if it's a user function call (starts with @ but not a builtin element)
1040 // Check if it's a user function call (starts with @ but not a builtin element)
1043 if is_builtin_element(word) { 0 } else { 2 } // function
1041 if is_builtin_element(word) { 0 } else { 2 } // function
1044 }
1042 }
1045 };
1043 };
1046 // Mark unused definitions with deprecated modifier (dimmed)
1044 // Mark unused definitions with deprecated modifier (dimmed)
1047− let modifier = if trimmed.starts_with("@let ")
1045+ let modifier = if trimmed.starts_with("@let ") && word != "@let" {
1048− && word != "@let"
1049− {
1050 let name_part = &word[1..]; // strip @
1046 let name_part = &word[1..]; // strip @
1051 if unused_vars.contains(&format!("@{}", name_part)) {
1047 if unused_vars.contains(&format!("@{}", name_part)) {
1052 1
1048 1
src/bin/htmlang_lsp/completion.rs+10−2
@@ -272,7 +272,11 @@ fn directive_completions(range: Range) -> Vec<CompletionItem> {
272 ("@meta", "Add a <meta> tag to <head>", "@meta "),
272 ("@meta", "Add a <meta> tag to <head>", "@meta "),
273 ("@head", "Add raw content to <head>", "@head"),
273 ("@head", "Add raw content to <head>", "@head"),
274 ("@style", "Add raw CSS to stylesheet", "@style"),
274 ("@style", "Add raw CSS to stylesheet", "@style"),
275+ (
276+ "@slot",
275− ("@slot", "Named slot in component for caller content", "@slot "),
277+ "Named slot in component for caller content",
278+ "@slot ",
279+ ),
276 ("@match", "Pattern matching on a value", "@match "),
280 ("@match", "Pattern matching on a value", "@match "),
277 ("@case", "Match case (inside @match)", "@case "),
281 ("@case", "Match case (inside @match)", "@case "),
278 ("@default", "Default case (inside @match)", "@default"),
282 ("@default", "Default case (inside @match)", "@default"),
@@ -308,7 +312,11 @@ fn directive_completions(range: Range) -> Vec<CompletionItem> {
308 "Define design tokens (colors, spacing, fonts)",
312 "Define design tokens (colors, spacing, fonts)",
309 "@theme",
313 "@theme",
310 ),
314 ),
315+ (
316+ "@deprecated",
311− ("@deprecated", "Mark next @let component as deprecated", "@deprecated "),
317+ "Mark next @let component as deprecated",
318+ "@deprecated ",
319+ ),
312 (
320 (
313 "@extends",
321 "@extends",
314 "Inherit a layout template and fill @slot blocks",
322 "Inherit a layout template and fill @slot blocks",
src/bin/htmlang_lsp/hover.rs+9−3
@@ -225,9 +225,15 @@ fn hover_builtin(word: &str) -> Option<String> {
225 "@paragraph" | "@p" => {
225 "@paragraph" | "@p" => {
226 "**@paragraph** \u{2014} Text block\n\nRenders as `<p>`.\n\nSupports inline elements: `{@text [bold] word}`"
226 "**@paragraph** \u{2014} Text block\n\nRenders as `<p>`.\n\nSupports inline elements: `{@text [bold] word}`"
227 }
227 }
228+ "@h1" => {
228− "@h1" => "**@h1** \u{2014} Heading level 1\n\nRenders as `<h1>`.\n\nUsage: `@h1 Page Title`",
229+ "**@h1** \u{2014} Heading level 1\n\nRenders as `<h1>`.\n\nUsage: `@h1 Page Title`"
230+ }
231+ "@h2" => {
229− "@h2" => "**@h2** \u{2014} Heading level 2\n\nRenders as `<h2>`.\n\nUsage: `@h2 Section Title`",
232+ "**@h2** \u{2014} Heading level 2\n\nRenders as `<h2>`.\n\nUsage: `@h2 Section Title`"
233+ }
234+ "@h3" => {
230− "@h3" => "**@h3** \u{2014} Heading level 3\n\nRenders as `<h3>`.\n\nUsage: `@h3 Subsection`",
235+ "**@h3** \u{2014} Heading level 3\n\nRenders as `<h3>`.\n\nUsage: `@h3 Subsection`"
236+ }
231 "@h4" => "**@h4** \u{2014} Heading level 4\n\nRenders as `<h4>`.",
237 "@h4" => "**@h4** \u{2014} Heading level 4\n\nRenders as `<h4>`.",
232 "@h5" => "**@h5** \u{2014} Heading level 5\n\nRenders as `<h5>`.",
238 "@h5" => "**@h5** \u{2014} Heading level 5\n\nRenders as `<h5>`.",
233 "@h6" => "**@h6** \u{2014} Heading level 6\n\nRenders as `<h6>`.",
239 "@h6" => "**@h6** \u{2014} Heading level 6\n\nRenders as `<h6>`.",
src/bin/htmlang_lsp/main.rs+1−2
@@ -682,8 +682,7 @@ impl LanguageServer for Backend {
682 .unwrap_or(false);
682 .unwrap_or(false);
683 let value_after_name = rest[name.len()..].trim_start();
683 let value_after_name = rest[name.len()..].trim_start();
684 if has_body
684 if has_body
685− && (value_after_name.is_empty()
686− || value_after_name.starts_with('$'))
685+ && (value_after_name.is_empty() || value_after_name.starts_with('$'))
687 {
686 {
688 defs.push(Def {
687 defs.push(Def {
689 line: i as u32,
688 line: i as u32,
src/main.rs+2−2
@@ -2238,8 +2238,8 @@ fn main() {
2238 .map(|l| l.starts_with(" ") || l.starts_with('\t'))
2238 .map(|l| l.starts_with(" ") || l.starts_with('\t'))
2239 .unwrap_or(false);
2239 .unwrap_or(false);
2240 // Also include if it has $params (even without checking body)
2240 // Also include if it has $params (even without checking body)
2241− let has_params = parts.len() > 1
2241+ let has_params =
2242− && parts[1..].iter().any(|p| p.starts_with('$'));
2242+ parts.len() > 1 && parts[1..].iter().any(|p| p.starts_with('$'));
2243 if is_fn || has_params {
2243 if is_fn || has_params {
2244 let params: Vec<&str> = parts[1..]
2244 let params: Vec<&str> = parts[1..]
2245 .iter()
2245 .iter()
tests/snapshots.rs+1−2
@@ -5180,8 +5180,7 @@ fn markdown_file_missing_reports_error() {
5180 let _ = std::fs::remove_dir_all(&dir);
5180 let _ = std::fs::remove_dir_all(&dir);
5181
5181
5182 let has_err = result.diagnostics.iter().any(|d| {
5182 let has_err = result.diagnostics.iter().any(|d| {
5183− d.severity == htmlang::parser::Severity::Error
5183+ d.severity == htmlang::parser::Severity::Error && d.message.contains("cannot read markdown")
5184− && d.message.contains("cannot read markdown")
5185 });
5184 });
5186 assert!(
5185 assert!(
5187 has_err,
5186 has_err,