@@ -538,14 +538,9 @@ function createAttrValueDoubleQuotesFix(
538538 // Check if this match is at or near the diagnostic position
539539 const diagnosticCol = diagnostic . data . col - 1 ;
540540 if ( Math . abs ( startCol - diagnosticCol ) <= 10 ) {
541- const lineStartPos = document . positionAt (
542- text
543- . split ( "\n" )
544- . slice ( 0 , diagnostic . data . line - 1 )
545- . join ( "\n" ) . length + ( diagnostic . data . line > 1 ? 1 : 0 ) ,
546- ) ;
547- const startPos = { line : lineStartPos . line , character : startCol } ;
548- const endPos = { line : lineStartPos . line , character : endCol } ;
541+ const lineIndex = diagnostic . data . line - 1 ;
542+ const startPos = { line : lineIndex , character : startCol } ;
543+ const endPos = { line : lineIndex , character : endCol } ;
549544
550545 edits . push ( {
551546 range : { start : startPos , end : endPos } ,
@@ -610,14 +605,9 @@ function createTagnameLowercaseFix(
610605 // Check if this match is at or near the diagnostic position
611606 const diagnosticCol = diagnostic . data . col - 1 ;
612607 if ( Math . abs ( match . index - diagnosticCol ) <= 5 ) {
613- const lineStartPos = document . positionAt (
614- text
615- . split ( "\n" )
616- . slice ( 0 , diagnostic . data . line - 1 )
617- . join ( "\n" ) . length + ( diagnostic . data . line > 1 ? 1 : 0 ) ,
618- ) ;
619- const startPos = { line : lineStartPos . line , character : startCol } ;
620- const endPos = { line : lineStartPos . line , character : endCol } ;
608+ const lineIndex = diagnostic . data . line - 1 ;
609+ const startPos = { line : lineIndex , character : startCol } ;
610+ const endPos = { line : lineIndex , character : endCol } ;
621611
622612 edits . push ( {
623613 range : { start : startPos , end : endPos } ,
@@ -682,14 +672,9 @@ function createAttrLowercaseFix(
682672 // Check if this match is at or near the diagnostic position
683673 const diagnosticCol = diagnostic . data . col - 1 ;
684674 if ( Math . abs ( startCol - diagnosticCol ) <= 5 ) {
685- const lineStartPos = document . positionAt (
686- text
687- . split ( "\n" )
688- . slice ( 0 , diagnostic . data . line - 1 )
689- . join ( "\n" ) . length + ( diagnostic . data . line > 1 ? 1 : 0 ) ,
690- ) ;
691- const startPos = { line : lineStartPos . line , character : startCol } ;
692- const endPos = { line : lineStartPos . line , character : endCol } ;
675+ const lineIndex = diagnostic . data . line - 1 ;
676+ const startPos = { line : lineIndex , character : startCol } ;
677+ const endPos = { line : lineIndex , character : endCol } ;
693678
694679 edits . push ( {
695680 range : { start : startPos , end : endPos } ,
@@ -1315,14 +1300,9 @@ function createSpecCharEscapeFix(
13151300 continue ;
13161301 }
13171302
1318- const lineStartPos = document . positionAt (
1319- text
1320- . split ( "\n" )
1321- . slice ( 0 , diagnostic . data . line - 1 )
1322- . join ( "\n" ) . length + ( diagnostic . data . line > 1 ? 1 : 0 ) ,
1323- ) ;
1324- const startPos = { line : lineStartPos . line , character : startCol } ;
1325- const endPos = { line : lineStartPos . line , character : endCol } ;
1303+ const lineIndex = diagnostic . data . line - 1 ;
1304+ const startPos = { line : lineIndex , character : startCol } ;
1305+ const endPos = { line : lineIndex , character : endCol } ;
13261306
13271307 // Map characters to their HTML entities
13281308 const entityMap : { [ key : string ] : string } = {
@@ -1359,6 +1339,133 @@ function createSpecCharEscapeFix(
13591339 } ;
13601340}
13611341
1342+ /**
1343+ * Create auto-fix action for tag-self-close rule
1344+ *
1345+ * This fixes void HTML elements (like img, br, hr, input, etc.) that don't properly self-close.
1346+ * The fix converts tags ending with ">" to end with " />" to comply with self-closing tag standards.
1347+ *
1348+ * Example:
1349+ * - Before: <img src="image.jpg">
1350+ * - After: <img src="image.jpg" />
1351+ */
1352+ function createTagSelfCloseFix (
1353+ document : TextDocument ,
1354+ diagnostic : Diagnostic ,
1355+ ) : CodeAction | null {
1356+ trace (
1357+ `[DEBUG] createTagSelfCloseFix called with diagnostic: ${ JSON . stringify ( diagnostic ) } ` ,
1358+ ) ;
1359+
1360+ if ( ! diagnostic . data || diagnostic . data . ruleId !== "tag-self-close" ) {
1361+ trace (
1362+ `[DEBUG] createTagSelfCloseFix: Invalid diagnostic data or ruleId: ${ JSON . stringify ( diagnostic . data ) } ` ,
1363+ ) ;
1364+ return null ;
1365+ }
1366+
1367+ trace (
1368+ `[DEBUG] createTagSelfCloseFix: Valid diagnostic for tag-self-close rule` ,
1369+ ) ;
1370+ trace ( `[DEBUG] Diagnostic range: ${ JSON . stringify ( diagnostic . range ) } ` ) ;
1371+ trace ( `[DEBUG] Diagnostic data: ${ JSON . stringify ( diagnostic . data ) } ` ) ;
1372+
1373+ const text = document . getText ( ) ;
1374+ const range = diagnostic . range ;
1375+ const raw = diagnostic . data . raw ; // Extract the tag with its attributes from the raw data
1376+ if ( ! raw ) {
1377+ trace ( `[DEBUG] createTagSelfCloseFix: No raw data found in diagnostic` ) ;
1378+ return null ;
1379+ }
1380+
1381+ trace ( `[DEBUG] createTagSelfCloseFix: Raw data: ${ raw } ` ) ;
1382+
1383+ // Get the position of the tag's closing '>'
1384+ const lineStart = document . offsetAt ( { line : range . start . line , character : 0 } ) ;
1385+ const position = document . offsetAt ( range . start ) ;
1386+ const lineContent = text . substring (
1387+ lineStart ,
1388+ text . indexOf ( "\n" , position ) !== - 1
1389+ ? text . indexOf ( "\n" , position )
1390+ : text . length ,
1391+ ) ;
1392+
1393+ trace ( `[DEBUG] createTagSelfCloseFix: Line content: ${ lineContent } ` ) ;
1394+
1395+ // Find the last character of the tag
1396+ let tagEndIndex ;
1397+
1398+ // If raw data contains the complete tag
1399+ if ( raw . endsWith ( ">" ) ) {
1400+ tagEndIndex = document . offsetAt ( range . start ) + raw . length - 1 ;
1401+ } else {
1402+ // Try to find the closest '>' after the diagnostic position
1403+ const tagStart = document . offsetAt ( range . start ) ;
1404+ const lineText = text . substring (
1405+ tagStart ,
1406+ text . indexOf ( "\n" , tagStart ) !== - 1
1407+ ? text . indexOf ( "\n" , tagStart )
1408+ : text . length ,
1409+ ) ;
1410+ const closeTagIndex = lineText . indexOf ( ">" ) ;
1411+
1412+ if ( closeTagIndex !== - 1 ) {
1413+ tagEndIndex = tagStart + closeTagIndex ;
1414+ } else {
1415+ trace (
1416+ `[DEBUG] createTagSelfCloseFix: Could not find closing '>' for tag` ,
1417+ ) ;
1418+ return null ;
1419+ }
1420+ }
1421+
1422+ if ( text [ tagEndIndex ] !== ">" ) {
1423+ trace (
1424+ `[DEBUG] createTagSelfCloseFix: Unexpected tag ending: ${ text [ tagEndIndex ] } at position ${ tagEndIndex } ` ,
1425+ ) ;
1426+ trace (
1427+ `[DEBUG] Text around position: ${ text . substring ( tagEndIndex - 10 , tagEndIndex + 10 ) } ` ,
1428+ ) ;
1429+ return null ;
1430+ }
1431+
1432+ trace (
1433+ `[DEBUG] createTagSelfCloseFix: Found tag ending '>' at position ${ tagEndIndex } ` ,
1434+ ) ;
1435+
1436+ // Create TextEdit to replace '>' with ' />'
1437+ const edit : TextEdit = {
1438+ range : {
1439+ start : document . positionAt ( tagEndIndex ) ,
1440+ end : document . positionAt ( tagEndIndex + 1 ) ,
1441+ } ,
1442+ newText : " />" ,
1443+ } ;
1444+
1445+ trace (
1446+ `[DEBUG] createTagSelfCloseFix: Created edit to replace '>' with ' />'` ,
1447+ ) ;
1448+
1449+ const action = CodeAction . create (
1450+ "Add self-closing tag" ,
1451+ {
1452+ changes : {
1453+ [ document . uri ] : [ edit ] ,
1454+ } ,
1455+ } ,
1456+ CodeActionKind . QuickFix ,
1457+ ) ;
1458+
1459+ action . diagnostics = [ diagnostic ] ;
1460+ action . isPreferred = true ;
1461+
1462+ trace (
1463+ `[DEBUG] createTagSelfCloseFix: Created code action: ${ JSON . stringify ( action ) } ` ,
1464+ ) ;
1465+
1466+ return action ;
1467+ }
1468+
13621469/**
13631470 * Create auto-fix actions for supported rules
13641471 */
@@ -1441,6 +1548,10 @@ async function createAutoFixes(
14411548 trace ( `[DEBUG] Calling createSpecCharEscapeFix` ) ;
14421549 fix = createSpecCharEscapeFix ( document , diagnostic ) ;
14431550 break ;
1551+ case "tag-self-close" :
1552+ trace ( `[DEBUG] Calling createTagSelfCloseFix` ) ;
1553+ fix = createTagSelfCloseFix ( document , diagnostic ) ;
1554+ break ;
14441555 default :
14451556 trace ( `[DEBUG] No autofix function found for rule: ${ ruleId } ` ) ;
14461557 break ;
@@ -1701,12 +1812,40 @@ connection.onRequest(
17011812 `[DEBUG] Context diagnostics: ${ JSON . stringify ( context . diagnostics ) } ` ,
17021813 ) ;
17031814
1815+ // Normalize range if it's in array format [start, end]
1816+ const normalizedRange = Array . isArray ( range )
1817+ ? { start : range [ 0 ] , end : range [ 1 ] }
1818+ : range ;
1819+
1820+ // Ensure range has proper structure
1821+ if (
1822+ ! normalizedRange . start ||
1823+ ! normalizedRange . end ||
1824+ typeof normalizedRange . start . line !== "number" ||
1825+ typeof normalizedRange . end . line !== "number"
1826+ ) {
1827+ trace ( `[DEBUG] Invalid range format: ${ JSON . stringify ( range ) } ` ) ;
1828+ return [ ] ;
1829+ }
1830+
17041831 // Filter diagnostics to only include those that intersect with the range
17051832 const filteredDiagnostics = context . diagnostics . filter ( ( diagnostic ) => {
1833+ // Ensure the diagnostic has a properly structured range
1834+ if (
1835+ ! diagnostic . range ||
1836+ typeof diagnostic . range . start ?. line !== "number" ||
1837+ typeof diagnostic . range . end ?. line !== "number"
1838+ ) {
1839+ trace (
1840+ `[DEBUG] Skipping diagnostic with invalid range: ${ JSON . stringify ( diagnostic ) } ` ,
1841+ ) ;
1842+ return false ;
1843+ }
1844+
17061845 const diagnosticRange = diagnostic . range ;
17071846 return (
1708- diagnosticRange . start . line <= range . end . line &&
1709- diagnosticRange . end . line >= range . start . line
1847+ diagnosticRange . start . line <= normalizedRange . end . line &&
1848+ diagnosticRange . end . line >= normalizedRange . start . line
17101849 ) ;
17111850 } ) ;
17121851
@@ -1736,7 +1875,15 @@ connection.onRequest(
17361875 href : diagnostic . codeDescription ?. href ,
17371876 line : diagnostic . range . start . line + 1 ,
17381877 col : diagnostic . range . start . character + 1 ,
1739- raw : diagnostic . message . split ( " " ) [ 0 ] ,
1878+ raw :
1879+ diagnostic . code === "tag-self-close"
1880+ ? document
1881+ . getText ( )
1882+ . substring (
1883+ document . offsetAt ( diagnostic . range . start ) ,
1884+ document . offsetAt ( diagnostic . range . end ) ,
1885+ )
1886+ : diagnostic . message . split ( " " ) [ 0 ] ,
17401887 } ,
17411888 } ;
17421889
@@ -1752,21 +1899,27 @@ connection.onRequest(
17521899 isPreferred : fix . isPreferred ,
17531900 edit : fix . edit
17541901 ? {
1755- changes : {
1756- [ uri ] : fix . edit . changes [ uri ] . map ( ( change ) => ( {
1757- range : {
1758- start : {
1759- line : change . range . start . line ,
1760- character : change . range . start . character ,
1761- } ,
1762- end : {
1763- line : change . range . end . line ,
1764- character : change . range . end . character ,
1765- } ,
1766- } ,
1767- newText : change . newText ,
1768- } ) ) ,
1769- } ,
1902+ changes : fix . edit . documentChanges
1903+ ? {
1904+ [ uri ] : ( fix . edit . documentChanges [ 0 ] as any ) . edits || [ ] ,
1905+ }
1906+ : fix . edit . changes && fix . edit . changes [ uri ]
1907+ ? {
1908+ [ uri ] : fix . edit . changes [ uri ] . map ( ( change ) => ( {
1909+ range : {
1910+ start : {
1911+ line : change . range . start . line ,
1912+ character : change . range . start . character ,
1913+ } ,
1914+ end : {
1915+ line : change . range . end . line ,
1916+ character : change . range . end . character ,
1917+ } ,
1918+ } ,
1919+ newText : change . newText ,
1920+ } ) ) ,
1921+ }
1922+ : { [ uri ] : [ ] } ,
17701923 }
17711924 : undefined ,
17721925 } ) ) ,
0 commit comments