Repository: Automattic/harper (rust)

Issue:
Exclude Files
In VSCode, is there anyway to exclude files that Harper checks? I have a text file that I use to track tasks and Harper views all the lines in that file as an issue, largely reporting that they are too long. I have to manually tell Harper to ignore those lines, which works until I need to type "DONE" or whatever. That leads Harper to view those lines as an error once again. If I could tell Harper to exclude a specific file, or file type, that would be wonderful. 

I looked at the Harper help files and did a search to see if this has been asked previously, but didn't find anything. If I have missed something I would appreciate being pointed towards it.

Elwood
[VSCode Extension] Be able to exclude by filetype
## Note

## Description

When working with certain filetypes, there are times where acronyms, internal naming conventions that can be falsely flagged are being used. Being able to exclude by filetype would eliminate the issue, allowing for a less  cluttered workspace when you know some things are spelled oddly.

## Component

- VS Code extension
- The `harper-ls` language server
- `harper.js`


## Use Cases


When working with Solidity (.sol) files, "unit's" are used and Harper considers that word wrong. Also naming conventions for variables can be falsely flagged: "prevPrize"


@mcecode 


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 12c021028..e1d2bdb6b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -270,6 +270,16 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
 
+[[package]]
+name = "bstr"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.19.0"
@@ -2177,6 +2187,19 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
 
+[[package]]
+name = "globset"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
 [[package]]
 name = "glow"
 version = "0.16.0"
@@ -2398,6 +2421,7 @@ dependencies = [
  "clap",
  "dirs 6.0.0",
  "futures",
+ "globset",
  "harper-comments",
  "harper-core",
  "harper-html",
diff --git a/harper-ls/Cargo.toml b/harper-ls/Cargo.toml
index 237f0f262..97a7fb120 100644
--- a/harper-ls/Cargo.toml
+++ b/harper-ls/Cargo.toml
@@ -30,6 +30,7 @@ futures = "0.3.31"
 serde = { version = "1.0.219", features = ["derive"] }
 indexmap = { version = "2.10.0", features = ["serde"] }
 reqwest = { version = "0.12.23", features = ["rustls-tls"], default-features = false }
+globset = "0.4.16"
 
 [features]
 default = []
diff --git a/harper-ls/src/backend.rs b/harper-ls/src/backend.rs
index b2242d98c..470a7449f 100644
--- a/harper-ls/src/backend.rs
+++ b/harper-ls/src/backend.rs
@@ -237,7 +237,14 @@ impl Backend {
         self.pull_config().await;
 
         // Copy necessary configuration to avoid holding lock.
-        let (lint_config, markdown_options, isolate_english, dialect, max_file_length) = {
+        let (
+            lint_config,
+            markdown_options,
+            isolate_english,
+            dialect,
+            max_file_length,
+            exclude_patterns,
+        ) = {
             let config = self.config.read().await;
             (
                 config.lint_config.clone(),
@@ -245,18 +252,30 @@ impl Backend {
                 config.isolate_english,
                 config.dialect,
                 config.max_file_length,
+                config.exclude_patterns.clone(),
             )
         };
 
+        let mut doc_lock = self.doc_state.lock().await;
+
+        if !exclude_patterns.is_empty()
+            && exclude_patterns.is_match(
+                uri.to_file_path()
+                    .ok_or_else(|| anyhow!("Unable to convert URI to file path."))?,
+            )
+        {
+            doc_lock.remove(uri);
+            return Ok(());
+        }
+
+        let ignored_lints = self.load_ignored_lints(uri).await.unwrap_or_default();
+
         let dict = Arc::new(
             self.generate_file_dictionary(uri)
                 .await
                 .context("Unable to generate the file dictionary.")?,
         );
 
-        let mut doc_lock = self.doc_state.lock().await;
-        let ignored_lints = self.load_ignored_lints(uri).await.unwrap_or_default();
-
         let doc_state = doc_lock.entry(uri.clone()).or_insert_with(|| {
             info!("Constructing new LintGroup for new document.");
 
diff --git a/harper-ls/src/config.rs b/harper-ls/src/config.rs
index 763b99f36..7fd4db942 100644
--- a/harper-ls/src/config.rs
+++ b/harper-ls/src/config.rs
@@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
 
 use anyhow::{Result, bail};
 use dirs::{config_dir, data_local_dir};
+use globset::{Glob, GlobSet};
 use harper_core::{Dialect, linting::LintGroupConfig, parsers::MarkdownOptions};
 use resolve_path::PathResolveExt;
 use serde::{Deserialize, Serialize};
@@ -77,6 +78,7 @@ pub struct Config {
     /// Maximum length (in bytes) a file can have before it's skipped.
     /// Above this limit, the file will not be linted.
     pub max_file_length: usize,
+    pub exclude_patterns: GlobSet,
 }
 
 impl Config {
@@ -185,6 +187,23 @@ impl Config {
             base.markdown_options.ignore_link_title = serde_json::from_value(v.clone())?;
         }
 
+        if let Some(v) = value.get("excludePatterns") {
+            let Some(a) = v.as_array() else {
+                bail!("excludePatterns must be an array.");
+            };
+
+            let patterns: Vec<Value> = a.to_vec();
+            if !patterns.is_empty() {
+                let mut builder = GlobSet::builder();
+
+                for pattern in patterns {
+                    builder.add(Glob::new(pattern.as_str().unwrap())?);
+                }
+
+                base.exclude_patterns = builder.build()?;
+            }
+        }
+
         Ok(base)
     }
 }
@@ -206,6 +225,7 @@ impl Default for Config {
             markdown_options: MarkdownOptions::default(),
             dialect: Dialect::American,
             max_file_length: 120_000,
+            exclude_patterns: GlobSet::empty(),
         }
     }
 }
diff --git a/packages/vscode-plugin/package.json b/packages/vscode-plugin/package.json
index 9ee60586d..1cf645140 100644
--- a/packages/vscode-plugin/package.json
+++ b/packages/vscode-plugin/package.json
@@ -119,6 +119,11 @@
 					"default": "information",
 					"description": "How severe do you want diagnostics to appear in the editor?"
 				},
+				"harper.excludePatterns": {
+					"scope": "resource",
+					"type": "array",
+					"description": "A set of globs to ignore. If a file matches any of the globs, it will not be linted."
+				},
 				"harper.fileDictPath": {
 					"scope": "resource",
 					"type": "string",
diff --git a/packages/web/src/routes/docs/integrations/emacs/+page.md b/packages/web/src/routes/docs/integrations/emacs/+page.md
index 621d6bea3..7a5907356 100644
--- a/packages/web/src/routes/docs/integrations/emacs/+page.md
+++ b/packages/web/src/routes/docs/integrations/emacs/+page.md
@@ -61,7 +61,8 @@ Additionally, you can also configure things like which linters to use or how you
                             :diagnosticSeverity "hint"
                             :isolateEnglish :json-false
                             :dialect "American"
-                            :maxFileLength 120000)))
+                            :maxFileLength 120000
+                            :ignoredLintsPath [])))
 ```
 
 :::note
diff --git a/packages/web/src/routes/docs/integrations/helix/+page.md b/packages/web/src/routes/docs/integrations/helix/+page.md
index 18e32ab0d..67ee30531 100644
--- a/packages/web/src/routes/docs/integrations/helix/+page.md
+++ b/packages/web/src/routes/docs/integrations/helix/+page.md
@@ -47,6 +47,7 @@ diagnosticSeverity = "hint"
 isolateEnglish = false
 dialect = "American"
 maxFileLength = 120000
+ignoredLintsPath = []
 
 [language-server.harper-ls.config.harper-ls.linters]
 SpellCheck = true
diff --git a/packages/web/src/routes/docs/integrations/language-server/+page.md b/packages/web/src/routes/docs/integrations/language-server/+page.md
index 2cba2151b..2adca7225 100644
--- a/packages/web/src/routes/docs/integrations/language-server/+page.md
+++ b/packages/web/src/routes/docs/integrations/language-server/+page.md
@@ -261,6 +261,7 @@ These configs are under the `markdown` key:
 | `isolateEnglish`     | `boolean`                                               | `false`       | In documents that are a mixture of English and another language, only lint English text. This feature is incredibly new and unstable. Do not expect it to work perfectly. |
 | `dialect`            | `"American"`, `"British"`, `"Australian"`, `"Canadian"` | `"American"`  | Set the dialect of English Harper should expect.                                                                                                                          |
 | `maxFileLength`      | `number`                                                | `120000`      | Maximum length of file to be linted (in bytes). If a file is larger/longer than this, it will not be linted.                                                              |
+| `excludePatterns`    | `array`                                                 | `[]`          | A set of globs to ignore. If a file matches any of the globs, it will not be linted.                                                                                      |
 
 ## Supported Languages
 
diff --git a/packages/web/src/routes/docs/integrations/neovim/+page.md b/packages/web/src/routes/docs/integrations/neovim/+page.md
index 0b18c49d3..2fac20ffd 100644
--- a/packages/web/src/routes/docs/integrations/neovim/+page.md
+++ b/packages/web/src/routes/docs/integrations/neovim/+page.md
@@ -49,7 +49,8 @@ require('lspconfig').harper_ls.setup {
       diagnosticSeverity = "hint",
       isolateEnglish = false,
       dialect = "American",
-      maxFileLength = 120000
+      maxFileLength = 120000,
+      ignoredLintsPath = {}
     }
   }
 }
diff --git a/packages/web/src/routes/docs/integrations/sublime-text/+page.md b/packages/web/src/routes/docs/integrations/sublime-text/+page.md
index e9f144e50..212fffde8 100644
--- a/packages/web/src/routes/docs/integrations/sublime-text/+page.md
+++ b/packages/web/src/routes/docs/integrations/sublime-text/+page.md
@@ -50,7 +50,9 @@ Open `Preferences > Package Settings > LSP > Settings` and add the `harper-ls` c
           },
           "diagnosticSeverity": "hint",
           "isolateEnglish": false,
-          "dialect": "American"
+          "dialect": "American",
+          "maxFileLength": 120000,
+          "ignoredLintsPath": []
         }
       }
     }


Test patch:
diff --git a/packages/vscode-plugin/src/tests/fixtures/.vscode/settings.json b/packages/vscode-plugin/src/tests/fixtures/.vscode/settings.json
index 338fa6386..e0801147a 100644
--- a/packages/vscode-plugin/src/tests/fixtures/.vscode/settings.json
+++ b/packages/vscode-plugin/src/tests/fixtures/.vscode/settings.json
@@ -4,5 +4,6 @@
 	},
 	"harper.linters.SpellCheck": true,
 	"harper.linters.RepeatedWords": true,
-	"harper.dialect": "American"
+	"harper.dialect": "American",
+	"harper.excludePatterns": []
 }
diff --git a/packages/vscode-plugin/src/tests/suite/integration.test.ts b/packages/vscode-plugin/src/tests/suite/integration.test.ts
index 2c2395e49..21317291f 100644
--- a/packages/vscode-plugin/src/tests/suite/integration.test.ts
+++ b/packages/vscode-plugin/src/tests/suite/integration.test.ts
@@ -148,6 +148,32 @@ describe('Integration >', () => {
 		);
 	});
 
+	it('excludes Markdown files when excludePatterns include *.md', async () => {
+		const config = workspace.getConfiguration('harper');
+
+		compareActualVsExpectedDiagnostics(
+			await waitForDiagnosticsChange(markdownUri, async () => {
+				await config.update('excludePatterns', ['*.md'], ConfigurationTarget.Workspace);
+			}),
+			createExpectedDiagnostics(),
+		);
+
+		await waitForDiagnosticsChange(markdownUri, async () => {
+			// Set config back to default value
+			await config.update('excludePatterns', [], ConfigurationTarget.Workspace);
+
+			// Ideally, we can just execute `workbench.action.closeActiveEditor` then
+			// `workbench.action.reopenClosedEditor` here and the diagnostics should reset since that
+			// works when done manually as that triggers `textDocument/didOpen`, but when done automated,
+			// it won't work. So, we delete, restore, then reopen the file instead.
+			const markdownContent = await workspace.fs.readFile(markdownUri);
+			await commands.executeCommand('workbench.files.action.showActiveFileInExplorer');
+			await commands.executeCommand('deleteFile');
+			await workspace.fs.writeFile(markdownUri, markdownContent);
+			await openUri(markdownUri);
+		});
+	});
+
 	it('updates diagnostics when files are deleted', async () => {
 		const markdownContent = await workspace.fs.readFile(markdownUri);