From 7903d856810e8f3d33b5f0edb67d8946bf6f65fd Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Sat, 10 Oct 2026 14:42:57 +0900 Subject: [PATCH 1/3] Add per-line HTMLHint disable quick fix Introduce a new code action for diagnostics that adds `` above the offending line, or appends the rule to an existing disable-next-line comment. The server now deduplicates these actions per rule/line and lists them after autofixes. Documentation and changelog were updated, and autofix test fixtures/config were expanded with additional rule cases. --- htmlhint-server/src/server.ts | 162 ++++++++++++++++++++++++------- htmlhint/CHANGELOG.md | 3 +- htmlhint/README.md | 2 + test/autofix/.htmlhintrc | 3 + test/autofix/test-autofixes.html | 11 +++ 5 files changed, 146 insertions(+), 35 deletions(-) diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts index 2ac9e4b..4a3269f 100644 --- a/htmlhint-server/src/server.ts +++ b/htmlhint-server/src/server.ts @@ -2232,6 +2232,80 @@ async function createAutoFixes( return actions; } +/** + * Suppress a rule for the diagnostic's line by adding an + * comment above it. If the line above + * already has such a comment, the rule is appended to it. + */ +function createDisableNextLineAction( + document: TextDocument, + diagnostic: Diagnostic, +): CodeAction | null { + const ruleId = diagnostic.data?.ruleId; + if (typeof ruleId !== "string" || !ruleId) { + return null; + } + + const lineIndex = diagnostic.range.start.line; + const text = document.getText(); + const lines = text.split(/\r?\n/); + const lineText = lines[lineIndex]; + if (lineText === undefined) { + return null; + } + + const title = `Disable ${ruleId} for this line`; + const previousLine = lineIndex > 0 ? lines[lineIndex - 1] : undefined; + const existing = previousLine?.match( + /^(\s*)\s*$/i, + ); + + if (previousLine !== undefined && existing) { + // Without a rule list the comment already disables every rule + if (!existing[2]) { + return null; + } + + const rules = existing[2].split(/\s+/).filter((rule) => rule.length > 0); + if (rules.includes(ruleId)) { + return null; + } + + return makeFix( + document, + title, + [ + { + range: { + start: { line: lineIndex - 1, character: 0 }, + end: { line: lineIndex - 1, character: previousLine.length }, + }, + newText: `${existing[1]}`, + }, + ], + diagnostic, + ); + } + + const indent = lineText.match(/^\s*/)?.[0] ?? ""; + const eol = text.includes("\r\n") ? "\r\n" : "\n"; + + return makeFix( + document, + title, + [ + { + range: { + start: { line: lineIndex, character: 0 }, + end: { line: lineIndex, character: 0 }, + }, + newText: `${indent}${eol}`, + }, + ], + diagnostic, + ); +} + connection.onInitialize( (_params: InitializeParams, _token: CancellationToken) => { // Since Files API is no longer available, we'll use embedded htmlhint directly @@ -2549,7 +2623,9 @@ connection.onRequest( } // Create auto-fixes for each diagnostic - const codeActions: LSPCodeAction[] = []; + const fixActions: CodeAction[] = []; + const disableActions: CodeAction[] = []; + const disabledRuleLines = new Set(); for (const diagnostic of filteredDiagnostics) { trace( `[DEBUG] Creating fixes for diagnostic: ${JSON.stringify(diagnostic)}`, @@ -2581,41 +2657,59 @@ connection.onRequest( trace( `[DEBUG] Created ${fixes.length} fixes for diagnostic: ${JSON.stringify(enhancedDiagnostic)}`, ); - codeActions.push( - ...fixes.map((fix) => ({ - title: fix.title, - kind: fix.kind, - diagnostics: fix.diagnostics, - isPreferred: fix.isPreferred, - edit: fix.edit - ? { - changes: fix.edit.documentChanges - ? { - [uri]: (fix.edit.documentChanges[0] as any).edits || [], - } - : fix.edit.changes && fix.edit.changes[uri] - ? { - [uri]: fix.edit.changes[uri].map((change) => ({ - range: { - start: { - line: change.range.start.line, - character: change.range.start.character, - }, - end: { - line: change.range.end.line, - character: change.range.end.character, - }, - }, - newText: change.newText, - })), - } - : { [uri]: [] }, - } - : undefined, - })), - ); + + // Offer one disable action per rule and line, listed after all autofixes + const disableKey = `${enhancedDiagnostic.range.start.line}:${enhancedDiagnostic.data?.ruleId}`; + if (!disabledRuleLines.has(disableKey)) { + disabledRuleLines.add(disableKey); + const disableAction = createDisableNextLineAction( + document, + enhancedDiagnostic, + ); + if (disableAction) { + disableAction.isPreferred = false; + disableActions.push(disableAction); + } + } + + fixActions.push(...fixes); } + const codeActions: LSPCodeAction[] = [ + ...fixActions, + ...disableActions, + ].map((fix) => ({ + title: fix.title, + kind: fix.kind, + diagnostics: fix.diagnostics, + isPreferred: fix.isPreferred, + edit: fix.edit + ? { + changes: fix.edit.documentChanges + ? { + [uri]: (fix.edit.documentChanges[0] as any).edits || [], + } + : fix.edit.changes && fix.edit.changes[uri] + ? { + [uri]: fix.edit.changes[uri].map((change) => ({ + range: { + start: { + line: change.range.start.line, + character: change.range.start.character, + }, + end: { + line: change.range.end.line, + character: change.range.end.character, + }, + }, + newText: change.newText, + })), + } + : { [uri]: [] }, + } + : undefined, + })); + trace(`[DEBUG] Created ${codeActions.length} auto-fix actions`); trace(`[DEBUG] Code actions: ${JSON.stringify(codeActions)}`); diff --git a/htmlhint/CHANGELOG.md b/htmlhint/CHANGELOG.md index a9e8ed7..0848c4f 100644 --- a/htmlhint/CHANGELOG.md +++ b/htmlhint/CHANGELOG.md @@ -2,9 +2,10 @@ All notable changes to the "vscode-htmlhint" extension will be documented in this file. -### v1.18.0 (2026-09-26) +### v1.18.0 (Unreleased) - Add autofix for the `attr-unsafe-chars`, `attr-value-single-quotes` and `frame-title-require` rules +- Add "Disable rule for this line" Quick Fix for every rule, which inserts an `` comment ### v1.17.0 (2026-09-23) diff --git a/htmlhint/README.md b/htmlhint/README.md index 9d031a2..801eb87 100644 --- a/htmlhint/README.md +++ b/htmlhint/README.md @@ -20,6 +20,8 @@ Errors in HTML files are highlighted with squiggles and you can hover over the s Many problems can now be fixed automatically by clicking on the lightbulb icon in problems panel or right-clicking on the error in the HTML file and selecting "Quick Fix". +Every problem also offers a "Disable rule-id for this line" Quick Fix, which adds an `` comment above the line (or appends the rule to an existing one). + ![hover](https://github.com/htmlhint/vscode-htmlhint/raw/main/htmlhint/images/hover.png) ### Auto-fix Support diff --git a/test/autofix/.htmlhintrc b/test/autofix/.htmlhintrc index 1c532fd..68e6903 100644 --- a/test/autofix/.htmlhintrc +++ b/test/autofix/.htmlhintrc @@ -3,13 +3,16 @@ "attr-lowercase": true, "attr-no-duplication": true, "attr-no-unnecessary-whitespace": true, + "attr-unsafe-chars": true, "attr-value-double-quotes": true, "attr-value-no-duplication": true, + "attr-value-single-quotes": false, "attr-whitespace": true, "button-type-require": true, "doctype-first": true, "doctype-html5": true, "empty-tag-not-self-closed": false, + "frame-title-require": true, "html-lang-require": true, "id-unique": true, "link-rel-canonical-require": true, diff --git a/test/autofix/test-autofixes.html b/test/autofix/test-autofixes.html index 93a5ea8..43a677d 100644 --- a/test/autofix/test-autofixes.html +++ b/test/autofix/test-autofixes.html @@ -63,6 +63,17 @@ XML + + Unsafe link +
Soft hyphen in title
+ + + Quotes + + + + +
From b0e8a8c73d04106b4f9c75a541de811575b50f5f Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Sat, 10 Oct 2026 14:46:34 +0900 Subject: [PATCH 2/3] update --- htmlhint/LICENSE.md | 2 +- htmlhint/package-lock.json | 8 ++++---- htmlhint/package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/htmlhint/LICENSE.md b/htmlhint/LICENSE.md index 8362552..c7a9dba 100644 --- a/htmlhint/LICENSE.md +++ b/htmlhint/LICENSE.md @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright (c) 2015-2022 Microsoft -Copyright (c) 2022-2025 HTMLHint +Copyright (c) 2022-2026 HTMLHint Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/htmlhint/package-lock.json b/htmlhint/package-lock.json index 3fb5963..590e517 100644 --- a/htmlhint/package-lock.json +++ b/htmlhint/package-lock.json @@ -9,7 +9,7 @@ "version": "1.18.0", "license": "SEE LICENSE IN LICENSE.md", "devDependencies": { - "@types/node": "^22.20.4", + "@types/node": "^22.20.5", "@types/vscode": "^1.101.0", "@vscode/test-electron": "^3.1.0", "esbuild": "^0.28.2", @@ -463,9 +463,9 @@ } }, "node_modules/@types/node": { - "version": "22.20.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", - "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/htmlhint/package.json b/htmlhint/package.json index 32953b7..2d4a976 100644 --- a/htmlhint/package.json +++ b/htmlhint/package.json @@ -91,7 +91,7 @@ "package": "vsce package" }, "devDependencies": { - "@types/node": "^22.20.4", + "@types/node": "^22.20.5", "@types/vscode": "^1.101.0", "@vscode/test-electron": "^3.1.0", "esbuild": "^0.28.2", From bd0c766d1aaecb62d45331e5b7a55fe2366c753b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 05:56:30 +0000 Subject: [PATCH 3/3] Preserve inline disable-next-line directives when adding rules Match the htmlhint directive anywhere on the previous line and replace only the directive itself, so content before or after it is kept and an existing suppression is not shifted onto the newly inserted comment. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01KuJTTYq3CpvR1S6x5iQxwH --- htmlhint-server/src/server.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts index 4a3269f..3e1f6de 100644 --- a/htmlhint-server/src/server.ts +++ b/htmlhint-server/src/server.ts @@ -2256,31 +2256,40 @@ function createDisableNextLineAction( const title = `Disable ${ruleId} for this line`; const previousLine = lineIndex > 0 ? lines[lineIndex - 1] : undefined; + // HTMLHint only honours the first directive on a line, wherever it appears const existing = previousLine?.match( - /^(\s*)\s*$/i, + //i, ); - if (previousLine !== undefined && existing) { + if ( + existing?.index !== undefined && + existing[1].toLowerCase() === "disable" && + existing[2] + ) { // Without a rule list the comment already disables every rule - if (!existing[2]) { + if (!existing[3]) { return null; } - const rules = existing[2].split(/\s+/).filter((rule) => rule.length > 0); + const rules = existing[3].split(/\s+/).filter((rule) => rule.length > 0); if (rules.includes(ruleId)) { return null; } + // Replace only the directive so any surrounding content on the line is kept return makeFix( document, title, [ { range: { - start: { line: lineIndex - 1, character: 0 }, - end: { line: lineIndex - 1, character: previousLine.length }, + start: { line: lineIndex - 1, character: existing.index }, + end: { + line: lineIndex - 1, + character: existing.index + existing[0].length, + }, }, - newText: `${existing[1]}`, + newText: ``, }, ], diagnostic,