From 4fc8aac7d293b3785ded2717e35f785f458af363 Mon Sep 17 00:00:00 2001 From: coliff Date: Fri, 20 Jun 2025 13:11:42 +0900 Subject: [PATCH 1/2] feat: add .gitignore option --- .cursor/rules/general.mdc | 10 ++++ htmlhint-server/src/server.ts | 106 +++++++++++++++++++++++++++++++++- htmlhint/.vscodeignore | 1 + htmlhint/CHANGELOG.md | 5 +- htmlhint/README.md | 25 ++++++++ htmlhint/extension.ts | 1 + htmlhint/package-lock.json | 18 +++++- htmlhint/package.json | 13 ++++- 8 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 .cursor/rules/general.mdc diff --git a/.cursor/rules/general.mdc b/.cursor/rules/general.mdc new file mode 100644 index 0000000..ae692e6 --- /dev/null +++ b/.cursor/rules/general.mdc @@ -0,0 +1,10 @@ +--- +description: +globs: +alwaysApply: true +--- +- Before declaring a task is complete, compile the extension to ensure it completes with no errors. +- Never downgrade dependencies +- Always run commands using PowerShell on Windows +- Newly added version/features for the Extension Changelog go at the top. +- As a general rule: rules, attributes, lists shoud be alphabetical order. diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts index ab20d90..991cbc5 100644 --- a/htmlhint-server/src/server.ts +++ b/htmlhint-server/src/server.ts @@ -43,14 +43,19 @@ import { TextDocument } from "vscode-languageserver-textdocument"; import * as htmlhint from "htmlhint"; import fs = require("fs"); import { URI } from "vscode-uri"; +import ignore from "ignore"; let stripJsonComments: any = require("strip-json-comments"); +// Cache for gitignore patterns to avoid repeatedly parsing .gitignore files +let gitignoreCache: Map = new Map(); + interface Settings { htmlhint: { configFile: string; enable: boolean; options: any; optionsFile: string; + ignoreGitignore: boolean; }; [key: string]: any; } @@ -1665,6 +1670,8 @@ async function createAutoFixes( } trace(`[DEBUG] Returning ${actions.length} auto-fix actions`); + trace(`[DEBUG] Code actions: ${JSON.stringify(actions)}`); + return actions; } @@ -1723,6 +1730,7 @@ connection.onInitialized(() => { options: {}, configFile: "", optionsFile: "", + ignoreGitignore: false, }, }; validateAllTextDocuments(connection, documents.all()); @@ -1743,6 +1751,20 @@ function doValidate(connection: Connection, document: TextDocument): void { trace(`[DEBUG] doValidate called for: ${fsPath}`); + // Check if file should be ignored based on .gitignore + if (settings.htmlhint.ignoreGitignore) { + // Find workspace root by looking for .git directory or .gitignore file + let workspaceRoot = findWorkspaceRoot(fsPath); + if (workspaceRoot && shouldIgnoreFile(fsPath, workspaceRoot)) { + trace( + `[DEBUG] File ${fsPath} is ignored by .gitignore, skipping validation`, + ); + // Clear any existing diagnostics for this file + connection.sendDiagnostics({ uri, diagnostics: [] }); + return; + } + } + let contents = document.getText(); let lines = contents.split("\n"); @@ -1807,6 +1829,9 @@ connection.onDidChangeConfiguration((params) => { htmlhintrcOptions[configPath] = undefined; }); + // Clear gitignore cache when settings change + gitignoreCache.clear(); + trace(`[DEBUG] Triggering revalidation due to settings change`); validateAllTextDocuments(connection, documents.all()); }) @@ -1818,6 +1843,7 @@ connection.onDidChangeConfiguration((params) => { Object.keys(htmlhintrcOptions).forEach((configPath) => { htmlhintrcOptions[configPath] = undefined; }); + gitignoreCache.clear(); validateAllTextDocuments(connection, documents.all()); } }); @@ -1837,7 +1863,7 @@ connection.onDidChangeWatchedFiles((params) => { trace(`[DEBUG] Processing config file change: ${fsPath}`); trace(`[DEBUG] Change type: ${params.changes[i].type}`); - // Only process .htmlhintrc files + // Process .htmlhintrc files if (fsPath.endsWith(".htmlhintrc") || fsPath.endsWith(".htmlhintrc.json")) { shouldRevalidate = true; @@ -1859,6 +1885,15 @@ connection.onDidChangeWatchedFiles((params) => { htmlhintrcOptions[configPath] = undefined; }); } + + // Process .gitignore files + if (fsPath.endsWith(".gitignore")) { + shouldRevalidate = true; + + // Clear gitignore cache when .gitignore changes + trace(`[DEBUG] .gitignore file changed, clearing cache`); + gitignoreCache.clear(); + } } if (shouldRevalidate) { @@ -1866,7 +1901,7 @@ connection.onDidChangeWatchedFiles((params) => { // Force revalidation of all documents validateAllTextDocuments(connection, documents.all()); } else { - trace(`[DEBUG] No .htmlhintrc files changed, skipping revalidation`); + trace(`[DEBUG] No relevant files changed, skipping revalidation`); } }); @@ -2034,3 +2069,70 @@ connection.onRequest( ); connection.listen(); + +/** + * Check if a file should be ignored based on .gitignore patterns + */ +function shouldIgnoreFile(filePath: string, workspaceRoot: string): boolean { + try { + // Find the .gitignore file in the workspace root + const gitignorePath = path.join(workspaceRoot, ".gitignore"); + + if (!fs.existsSync(gitignorePath)) { + return false; // No .gitignore file, so don't ignore anything + } + + // Check cache first + if (gitignoreCache.has(workspaceRoot)) { + const ig = gitignoreCache.get(workspaceRoot); + const relativePath = path.relative(workspaceRoot, filePath); + return ig.ignores(relativePath); + } + + // Parse .gitignore file + const gitignoreContent = fs.readFileSync(gitignorePath, "utf8"); + const ig = ignore().add(gitignoreContent); + + // Cache the parsed patterns + gitignoreCache.set(workspaceRoot, ig); + + // Check if the file should be ignored + const relativePath = path.relative(workspaceRoot, filePath); + return ig.ignores(relativePath); + } catch (error) { + trace(`[DEBUG] Error checking .gitignore for ${filePath}: ${error}`); + return false; // On error, don't ignore the file + } +} + +/** + * Find the workspace root directory by looking for .git directory or .gitignore file + */ +function findWorkspaceRoot(filePath: string): string | null { + try { + let currentDir = path.dirname(filePath); + const rootDir = path.parse(filePath).root; + + while (currentDir !== rootDir) { + // Check for .git directory or .gitignore file + if ( + fs.existsSync(path.join(currentDir, ".git")) || + fs.existsSync(path.join(currentDir, ".gitignore")) + ) { + return currentDir; + } + + // Move up one directory + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + break; // Reached root + } + currentDir = parentDir; + } + + return null; // No workspace root found + } catch (error) { + trace(`[DEBUG] Error finding workspace root for ${filePath}: ${error}`); + return null; + } +} diff --git a/htmlhint/.vscodeignore b/htmlhint/.vscodeignore index 57aa910..1aa0533 100644 --- a/htmlhint/.vscodeignore +++ b/htmlhint/.vscodeignore @@ -41,6 +41,7 @@ images/status-bar.png **/node_modules/**/*.md **/node_modules/**/*.txt **/node_modules/**/LICENSE* +**/node_modules/**/LICENSE-MIT* **/node_modules/**/license* **/node_modules/**/CHANGELOG* **/node_modules/**/changelog* diff --git a/htmlhint/CHANGELOG.md b/htmlhint/CHANGELOG.md index eda5c41..ac7533f 100644 --- a/htmlhint/CHANGELOG.md +++ b/htmlhint/CHANGELOG.md @@ -2,8 +2,11 @@ All notable changes to the "vscode-htmlhint" extension will be documented in this file. -### v1.10.3 (2025-06-20) +### v1.11.0 (2025-06-20) +- Option to skip linting files ignored by `.gitignore` (`htmlhint.ignoreGitignore`). + - When enabled, HTMLHint will not lint files or folders listed in your workspace's `.gitignore` (e.g., `node_modules/`, `dist/`, etc). + - Enable this in VS Code settings: `HTMLHint: Ignore Gitignore`. - Add autofix for `attr-whitespace` rule ### v1.10.2 (2025-06-19) diff --git a/htmlhint/README.md b/htmlhint/README.md index 83604d2..b6c8c25 100644 --- a/htmlhint/README.md +++ b/htmlhint/README.md @@ -125,3 +125,28 @@ Here's an example using the `htmlhint.documentSelector` and `htmlhint.options` s "title-require": true } ``` + +## Skipping Linting for `.gitignore`-d Files + +You can configure the extension to **skip linting files and folders that are listed in your `.gitignore`**. This is useful for ignoring generated files, dependencies, and other files you don't want to lint (like `node_modules/`, `dist/`, `build/`, etc). + +### How to Enable + +1. Open VS Code settings. +2. Search for `HTMLHint: Ignore Gitignore`. +3. Enable the option: + **`htmlhint.ignoreGitignore`** (default: `false`) + +When enabled, any HTML files ignored by your workspace's `.gitignore` will not be linted by the extension. + +### Example + +If your `.gitignore` contains: + +``` +node_modules/ +dist/ +*.tmp +``` + +Then files like `dist/index.html` or `node_modules/foo/bar.html` will be skipped by HTMLHint. diff --git a/htmlhint/extension.ts b/htmlhint/extension.ts index bb18522..d4b6ebe 100644 --- a/htmlhint/extension.ts +++ b/htmlhint/extension.ts @@ -58,6 +58,7 @@ export function activate(context: vscode.ExtensionContext) { fileEvents: [ vscode.workspace.createFileSystemWatcher("**/.htmlhintrc"), vscode.workspace.createFileSystemWatcher("**/.htmlhintrc.json"), + vscode.workspace.createFileSystemWatcher("**/.gitignore"), ], }, middleware: { diff --git a/htmlhint/package-lock.json b/htmlhint/package-lock.json index c0e3b4d..11b980a 100644 --- a/htmlhint/package-lock.json +++ b/htmlhint/package-lock.json @@ -1,23 +1,25 @@ { "name": "vscode-htmlhint", - "version": "1.10.3", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-htmlhint", - "version": "1.10.3", + "version": "1.11.0", "bundleDependencies": [ "vscode-languageclient", "htmlhint", "strip-json-comments", "vscode-languageserver", "vscode-languageserver-textdocument", - "vscode-uri" + "vscode-uri", + "ignore" ], "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "htmlhint": "1.6.3", + "ignore": "^5.2.2", "strip-json-comments": "3.1.1", "vscode-languageclient": "9.0.1", "vscode-languageserver": "9.0.1", @@ -466,6 +468,16 @@ "node": ">= 6" } }, + "node_modules/ignore": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.2.tgz", + "integrity": "sha512-m1MJSy4Z2NAcyhoYpxQeBsc1ZdNQwYjN0wGbLBlnVArdJ90Gtr8IhNSfZZcCoR0fM/0E0BJ0mf1KnLNDOCJP4w==", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", diff --git a/htmlhint/package.json b/htmlhint/package.json index 8a6a1de..b9c72a9 100644 --- a/htmlhint/package.json +++ b/htmlhint/package.json @@ -3,7 +3,7 @@ "displayName": "HTMLHint", "description": "VS Code integration for HTMLHint - A Static Code Analysis Tool for HTML", "icon": "images/icon.png", - "version": "1.10.3", + "version": "1.11.0", "publisher": "HTMLHint", "galleryBanner": { "color": "#333333", @@ -66,6 +66,11 @@ "type": "string", "default": null, "description": "The HTMLHint options config file path." + }, + "htmlhint.ignoreGitignore": { + "type": "boolean", + "default": false, + "description": "Skip linting files that are ignored by .gitignore. This is useful to avoid linting generated files, dependencies, and other files that shouldn't be checked." } } }, @@ -82,7 +87,7 @@ "vscode:prepublish": "npm run compile && npm run bundle-dependencies", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", - "bundle-dependencies": "npm install --no-package-lock --no-save --no-fund htmlhint@1.6.3 strip-json-comments@3.1.1 vscode-languageserver@9.0.1 vscode-languageserver-textdocument@1.0.12 vscode-uri@3.1.0", + "bundle-dependencies": "npm install --no-package-lock --no-save --no-fund htmlhint@1.6.3 strip-json-comments@3.1.1 vscode-languageserver@9.0.1 vscode-languageserver-textdocument@1.0.12 vscode-uri@3.1.0 ignore@5.2.2", "package": "vsce package" }, "devDependencies": { @@ -93,6 +98,7 @@ }, "dependencies": { "htmlhint": "1.6.3", + "ignore": "^5.2.2", "strip-json-comments": "3.1.1", "vscode-languageclient": "9.0.1", "vscode-languageserver": "9.0.1", @@ -105,7 +111,8 @@ "strip-json-comments", "vscode-languageserver", "vscode-languageserver-textdocument", - "vscode-uri" + "vscode-uri", + "ignore" ], "volta": { "node": "22.16.0" From f30080b6f7cd16c63e1d91b03fc7d5f87f98f49d Mon Sep 17 00:00:00 2001 From: coliff Date: Fri, 20 Jun 2025 13:23:08 +0900 Subject: [PATCH 2/2] followup fix --- .markdownlint.json | 1 + htmlhint-server/src/server.ts | 38 ++++++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/.markdownlint.json b/.markdownlint.json index 3693d1a..a1abe7e 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -3,5 +3,6 @@ "MD013": false, "MD033": false, "MD034": false, + "MD040": false, "MD041": false } diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts index 991cbc5..a085a2c 100644 --- a/htmlhint-server/src/server.ts +++ b/htmlhint-server/src/server.ts @@ -49,6 +49,9 @@ let stripJsonComments: any = require("strip-json-comments"); // Cache for gitignore patterns to avoid repeatedly parsing .gitignore files let gitignoreCache: Map = new Map(); +// Cache for workspace root detection to avoid repeated filesystem calls +let workspaceRootCache: Map = new Map(); + interface Settings { htmlhint: { configFile: string; @@ -1832,6 +1835,9 @@ connection.onDidChangeConfiguration((params) => { // Clear gitignore cache when settings change gitignoreCache.clear(); + // Clear workspace root cache when settings change + workspaceRootCache.clear(); + trace(`[DEBUG] Triggering revalidation due to settings change`); validateAllTextDocuments(connection, documents.all()); }) @@ -1844,6 +1850,7 @@ connection.onDidChangeConfiguration((params) => { htmlhintrcOptions[configPath] = undefined; }); gitignoreCache.clear(); + workspaceRootCache.clear(); validateAllTextDocuments(connection, documents.all()); } }); @@ -1893,6 +1900,9 @@ connection.onDidChangeWatchedFiles((params) => { // Clear gitignore cache when .gitignore changes trace(`[DEBUG] .gitignore file changed, clearing cache`); gitignoreCache.clear(); + + // Clear workspace root cache since it depends on .gitignore detection + workspaceRootCache.clear(); } } @@ -2110,15 +2120,28 @@ function shouldIgnoreFile(filePath: string, workspaceRoot: string): boolean { */ function findWorkspaceRoot(filePath: string): string | null { try { + // Check cache first + const cacheKey = path.dirname(filePath); + if (workspaceRootCache.has(cacheKey)) { + return workspaceRootCache.get(cacheKey); + } + let currentDir = path.dirname(filePath); const rootDir = path.parse(filePath).root; + const visitedDirs = new Set(); + + while (currentDir !== rootDir && !visitedDirs.has(currentDir)) { + visitedDirs.add(currentDir); - while (currentDir !== rootDir) { // Check for .git directory or .gitignore file - if ( - fs.existsSync(path.join(currentDir, ".git")) || - fs.existsSync(path.join(currentDir, ".gitignore")) - ) { + const gitPath = path.join(currentDir, ".git"); + const gitignorePath = path.join(currentDir, ".gitignore"); + + if (fs.existsSync(gitPath) || fs.existsSync(gitignorePath)) { + // Cache the result for this directory and all its subdirectories + visitedDirs.forEach((dir) => { + workspaceRootCache.set(dir, currentDir); + }); return currentDir; } @@ -2130,6 +2153,11 @@ function findWorkspaceRoot(filePath: string): string | null { currentDir = parentDir; } + // Cache negative results too + visitedDirs.forEach((dir) => { + workspaceRootCache.set(dir, null); + }); + return null; // No workspace root found } catch (error) { trace(`[DEBUG] Error finding workspace root for ${filePath}: ${error}`);