Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .cursor/rules/general.mdc
Original file line number Diff line number Diff line change
@@ -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.

Check warning on line 10 in .cursor/rules/general.mdc

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (shoud)
1 change: 1 addition & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"MD013": false,
"MD033": false,
"MD034": false,
"MD040": false,
"MD041": false
}
134 changes: 132 additions & 2 deletions htmlhint-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,22 @@ 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<string, any> = new Map();

// Cache for workspace root detection to avoid repeated filesystem calls
let workspaceRootCache: Map<string, string | null> = new Map();

interface Settings {
htmlhint: {
configFile: string;
enable: boolean;
options: any;
optionsFile: string;
ignoreGitignore: boolean;
};
[key: string]: any;
}
Expand Down Expand Up @@ -1665,6 +1673,8 @@ async function createAutoFixes(
}

trace(`[DEBUG] Returning ${actions.length} auto-fix actions`);
trace(`[DEBUG] Code actions: ${JSON.stringify(actions)}`);

return actions;
}

Expand Down Expand Up @@ -1723,6 +1733,7 @@ connection.onInitialized(() => {
options: {},
configFile: "",
optionsFile: "",
ignoreGitignore: false,
},
};
validateAllTextDocuments(connection, documents.all());
Expand All @@ -1743,6 +1754,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");

Expand Down Expand Up @@ -1807,6 +1832,12 @@ connection.onDidChangeConfiguration((params) => {
htmlhintrcOptions[configPath] = undefined;
});

// 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());
})
Expand All @@ -1818,6 +1849,8 @@ connection.onDidChangeConfiguration((params) => {
Object.keys(htmlhintrcOptions).forEach((configPath) => {
htmlhintrcOptions[configPath] = undefined;
});
gitignoreCache.clear();
workspaceRootCache.clear();
validateAllTextDocuments(connection, documents.all());
}
});
Expand All @@ -1837,7 +1870,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;

Expand All @@ -1859,14 +1892,26 @@ 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();

// Clear workspace root cache since it depends on .gitignore detection
workspaceRootCache.clear();
}
}

if (shouldRevalidate) {
trace(`[DEBUG] Triggering revalidation of all documents`);
// 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`);
}
});

Expand Down Expand Up @@ -2034,3 +2079,88 @@ 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 {
// 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<string>();

while (currentDir !== rootDir && !visitedDirs.has(currentDir)) {
visitedDirs.add(currentDir);

// Check for .git directory or .gitignore file
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;
}

// Move up one directory
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
break; // Reached root
}
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}`);
return null;
}
}
1 change: 1 addition & 0 deletions htmlhint/.vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
5 changes: 4 additions & 1 deletion htmlhint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions htmlhint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions htmlhint/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
18 changes: 15 additions & 3 deletions htmlhint/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions htmlhint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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."
}
}
},
Expand All @@ -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": {
Expand All @@ -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",
Expand All @@ -105,7 +111,8 @@
"strip-json-comments",
"vscode-languageserver",
"vscode-languageserver-textdocument",
"vscode-uri"
"vscode-uri",
"ignore"
],
"volta": {
"node": "22.16.0"
Expand Down