Merge branch 'master' into local-stats

This commit is contained in:
Elijah Potter 2025-04-02 14:23:47 -06:00
commit e5a5f7e5bb
26 changed files with 428 additions and 82 deletions

17
Cargo.lock generated
View file

@ -726,6 +726,7 @@ dependencies = [
"anyhow",
"ariadne",
"clap",
"dirs 6.0.0",
"harper-comments",
"harper-core",
"harper-literate-haskell",
@ -738,7 +739,7 @@ dependencies = [
[[package]]
name = "harper-comments"
version = "0.26.0"
version = "0.27.0"
dependencies = [
"harper-core",
"harper-html",
@ -769,7 +770,7 @@ dependencies = [
[[package]]
name = "harper-core"
version = "0.26.0"
version = "0.27.0"
dependencies = [
"blanket",
"cached",
@ -802,7 +803,7 @@ dependencies = [
[[package]]
name = "harper-html"
version = "0.26.0"
version = "0.27.0"
dependencies = [
"harper-core",
"harper-tree-sitter",
@ -813,7 +814,7 @@ dependencies = [
[[package]]
name = "harper-literate-haskell"
version = "0.26.0"
version = "0.27.0"
dependencies = [
"harper-comments",
"harper-core",
@ -824,7 +825,7 @@ dependencies = [
[[package]]
name = "harper-ls"
version = "0.26.0"
version = "0.27.0"
dependencies = [
"anyhow",
"clap",
@ -851,7 +852,7 @@ dependencies = [
[[package]]
name = "harper-stats"
version = "0.26.0"
version = "0.27.0"
dependencies = [
"chrono",
"harper-core",
@ -864,7 +865,7 @@ dependencies = [
[[package]]
name = "harper-tree-sitter"
version = "0.26.0"
version = "0.27.0"
dependencies = [
"harper-core",
"tree-sitter",
@ -872,7 +873,7 @@ dependencies = [
[[package]]
name = "harper-typst"
version = "0.26.0"
version = "0.27.0"
dependencies = [
"harper-core",
"itertools 0.14.0",

View file

@ -10,11 +10,12 @@ repository = "https://github.com/automattic/harper"
anyhow = "1.0.97"
ariadne = "0.4.1"
clap = { version = "4.5.34", features = ["derive", "string"] }
harper-stats = { path = "../harper-stats", version = "0.26.0" }
harper-literate-haskell = { path = "../harper-literate-haskell", version = "0.26.0" }
harper-core = { path = "../harper-core", version = "0.26.0" }
harper-comments = { path = "../harper-comments", version = "0.26.0" }
harper-typst = { path = "../harper-typst", version = "0.26.0" }
harper-stats = { path = "../harper-stats", version = "0.27.0" }
dirs = "6.0.0"
harper-literate-haskell = { path = "../harper-literate-haskell", version = "0.27.0" }
harper-core = { path = "../harper-core", version = "0.27.0" }
harper-comments = { path = "../harper-comments", version = "0.27.0" }
harper-typst = { path = "../harper-typst", version = "0.27.0" }
hashbrown = "0.15.2"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"

View file

@ -3,18 +3,20 @@
use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::process;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use std::{fs, process};
use anyhow::format_err;
use ariadne::{Color, Label, Report, ReportKind, Source};
use clap::Parser;
use dirs::{config_dir, data_local_dir};
use harper_comments::CommentParser;
use harper_core::linting::{LintGroup, Linter};
use harper_core::parsers::{Markdown, MarkdownOptions};
use harper_core::{
remove_overlaps, CharStringExt, Dialect, Dictionary, Document, FstDictionary,
MutableDictionary, TokenKind, TokenStringExt, WordId,
remove_overlaps, CharStringExt, Dialect, Dictionary, Document, FstDictionary, MergedDictionary,
MutableDictionary, TokenKind, TokenStringExt, WordId, WordMetadata,
};
use harper_literate_haskell::LiterateHaskellParser;
use harper_stats::Stats;
@ -39,6 +41,12 @@ enum Args {
/// Specify the dialect.
#[arg(short, long, default_value = Dialect::American.to_string())]
dialect: Dialect,
/// Path to the user dictionary.
#[arg(short, long, default_value = config_dir().unwrap().join("harper-ls/dictionary.txt").into_os_string())]
user_dict_path: PathBuf,
/// Path to the directory for file-local dictionaries.
#[arg(short, long, default_value = data_local_dir().unwrap().join("harper-ls/file_dictionaries/").into_os_string())]
file_dict_path: PathBuf,
},
/// Parse a provided document and print the detected symbols.
Parse {
@ -81,10 +89,26 @@ fn main() -> anyhow::Result<()> {
count,
only_lint_with,
dialect,
user_dict_path,
file_dict_path,
} => {
let (doc, source) = load_file(&file, markdown_options)?;
let mut merged_dict = MergedDictionary::new();
merged_dict.add_dictionary(dictionary);
let mut linter = LintGroup::new_curated(dictionary, dialect);
match load_dict(&user_dict_path) {
Ok(user_dict) => merged_dict.add_dictionary(Arc::new(user_dict)),
Err(err) => println!("{}: {}", user_dict_path.display(), err),
}
let file_dict_path = file_dict_path.join(file_dict_name(&file));
match load_dict(&file_dict_path) {
Ok(file_dict) => merged_dict.add_dictionary(Arc::new(file_dict)),
Err(err) => println!("{}: {}", file_dict_path.display(), err),
}
let (doc, source) = load_file(&file, markdown_options, &merged_dict)?;
let mut linter = LintGroup::new_curated(Arc::new(merged_dict), dialect);
if let Some(rules) = only_lint_with {
linter.set_all_rules_to(Some(false));
@ -131,7 +155,7 @@ fn main() -> anyhow::Result<()> {
process::exit(1)
}
Args::Parse { file } => {
let (doc, _) = load_file(&file, markdown_options)?;
let (doc, _) = load_file(&file, markdown_options, &dictionary)?;
for token in doc.tokens() {
let json = serde_json::to_string(&token)?;
@ -144,7 +168,7 @@ fn main() -> anyhow::Result<()> {
file,
include_newlines,
} => {
let (doc, source) = load_file(&file, markdown_options)?;
let (doc, source) = load_file(&file, markdown_options, &dictionary)?;
let primary_color = Color::Blue;
let secondary_color = Color::Magenta;
@ -311,7 +335,7 @@ fn main() -> anyhow::Result<()> {
Ok(())
}
Args::MineWords { file } => {
let (doc, _source) = load_file(&file, MarkdownOptions::default())?;
let (doc, _source) = load_file(&file, MarkdownOptions::default(), &dictionary)?;
let mut words = HashMap::new();
@ -340,7 +364,11 @@ fn main() -> anyhow::Result<()> {
}
}
fn load_file(file: &Path, markdown_options: MarkdownOptions) -> anyhow::Result<(Document, String)> {
fn load_file(
file: &Path,
markdown_options: MarkdownOptions,
dictionary: &impl Dictionary,
) -> anyhow::Result<(Document, String)> {
let source = std::fs::read_to_string(file)?;
let parser: Box<dyn harper_core::parsers::Parser> =
@ -357,7 +385,7 @@ fn load_file(file: &Path, markdown_options: MarkdownOptions) -> anyhow::Result<(
),
};
Ok((Document::new_curated(&source, &parser), source))
Ok((Document::new(&source, &parser, dictionary), source))
}
/// Split a dictionary line into its word and annotation segments
@ -385,3 +413,30 @@ fn print_word_derivations(word: &str, annot: &str, dictionary: &impl Dictionary)
println!(" - {}", child_str);
}
}
/// Sync version of harper-ls/src/dictionary_io@load_dict
fn load_dict(path: &Path) -> anyhow::Result<MutableDictionary> {
let str = fs::read_to_string(path)?;
let mut dict = MutableDictionary::new();
dict.extend_words(
str.lines()
.map(|l| (l.chars().collect::<Vec<_>>(), WordMetadata::default())),
);
Ok(dict)
}
/// Path version of harper-ls/src/dictionary_io@file_dict_name
fn file_dict_name(path: &Path) -> PathBuf {
let mut rewritten = String::new();
for seg in path.components() {
if !matches!(seg, Component::RootDir) {
rewritten.push_str(&seg.as_os_str().to_string_lossy());
rewritten.push('%');
}
}
rewritten.into()
}

View file

@ -1,6 +1,6 @@
[package]
name = "harper-comments"
version = "0.26.0"
version = "0.27.0"
edition = "2024"
description = "The language checker for developers."
license = "Apache-2.0"
@ -8,9 +8,9 @@ readme = "README.md"
repository = "https://github.com/automattic/harper"
[dependencies]
harper-core = { path = "../harper-core", version = "0.26.0" }
harper-html = { path = "../harper-html", version = "0.26.0" }
harper-tree-sitter = { path = "../harper-tree-sitter", version = "0.26.0" }
harper-core = { path = "../harper-core", version = "0.27.0" }
harper-html = { path = "../harper-html", version = "0.27.0" }
harper-tree-sitter = { path = "../harper-tree-sitter", version = "0.27.0" }
tree-sitter = "0.20.10"
tree-sitter-rust = "0.20.4"
tree-sitter-typescript = "0.20.3"

View file

@ -1,6 +1,6 @@
[package]
name = "harper-core"
version = "0.26.0"
version = "0.27.0"
edition = "2024"
description = "The language checker for developers."
license = "Apache-2.0"

View file

@ -53,7 +53,10 @@
"condition": "."
}
],
"adds_metadata": {},
"adds_metadata": {
"adjective": {},
"adverb": {}
},
"gifts_metadata": {}
},
"U": {

View file

@ -15,12 +15,16 @@ const FALSE_POSITIVES: &[&str] = &[
// The word is used more as a noun in this context.
// (using .kind.is_likely_homograph() here is too strict)
"bottom",
"chance",
"front",
"head",
"kind",
"left",
"meaning",
"middle",
"one",
"part",
"potential",
"shadow",
"short",
"something",
@ -172,6 +176,7 @@ mod tests {
#[test]
fn dont_flag_kind() {
// Adjective as in "a kind person" vs noun as in "A kind of person"
assert_lint_count(
"Log.txt file automatic creation in PWD is kind of an anti-feature",
AdjectiveOfA,
@ -181,6 +186,7 @@ mod tests {
#[test]
fn dont_flag_part() {
// Can be an adjective in e.g. "He is just part owner"
assert_lint_count(
"cannot delete a food that is no longer part of a recipe",
AdjectiveOfA,
@ -190,6 +196,7 @@ mod tests {
#[test]
fn dont_flag_much() {
// "much of" is correct idiomatic usage
assert_lint_count(
"How much of a performance impact when switching from rails to rails-api ?",
AdjectiveOfA,
@ -199,6 +206,7 @@ mod tests {
#[test]
fn dont_flag_part_uppercase() {
// Can be an adjective in e.g. "Part man, part machine"
assert_lint_count(
"Quarkus Extension as Part of a Project inside a Monorepo?",
AdjectiveOfA,
@ -206,8 +214,19 @@ mod tests {
);
}
#[test]
fn dont_flag_all_of() {
// "all of" is correct idiomatic usage
assert_lint_count(
"This repository is deprecated. All of its content and history has been moved.",
AdjectiveOfA,
0,
);
}
#[test]
fn dont_flag_inside() {
// "inside of" is idiomatic usage
assert_lint_count(
"Michael and Brock sat inside of a diner in Brandon",
AdjectiveOfA,
@ -217,6 +236,7 @@ mod tests {
#[test]
fn dont_flag_out() {
// "out of" is correct idiomatic usage
assert_lint_count(
"not only would he potentially be out of a job and back to sort of poverty",
AdjectiveOfA,
@ -226,6 +246,7 @@ mod tests {
#[test]
fn dont_flag_full() {
// "full of" is correct idiomatic usage
assert_lint_count(
"fortunately I happen to have this Tupperware full of an unceremoniously disassembled LED Mac Mini",
AdjectiveOfA,
@ -235,6 +256,7 @@ mod tests {
#[test]
fn dont_flag_something() {
// Can be a noun in e.g. "a certain something"
assert_lint_count(
"Well its popularity seems to be taking something of a dip right now.",
AdjectiveOfA,
@ -244,6 +266,7 @@ mod tests {
#[test]
fn dont_flag_short() {
// Can be a noun in e.g. "use a multimeter to find the short"
assert_lint_count(
"I found one Youtube short of an indonesian girl.",
AdjectiveOfA,
@ -253,6 +276,7 @@ mod tests {
#[test]
fn dont_flag_bottom() {
// Can be an adjective in e.g. "bottom bunk"
assert_lint_count(
"When leaves are just like coming out individually from the bottom of a fruit.",
AdjectiveOfA,
@ -262,6 +286,7 @@ mod tests {
#[test]
fn dont_flag_left() {
// Can be an adjective in e.g. "left hand"
assert_lint_count("and what is left of a 12vt coil", AdjectiveOfA, 0)
}
@ -269,4 +294,44 @@ mod tests {
fn dont_flag_full_uppercase() {
assert_lint_count("Full of a bunch varnish like we get.", AdjectiveOfA, 0);
}
#[test]
fn dont_flag_head() {
// Can be an adjective in e.g. "the head cook"
assert_lint_count(
"You need to get out if you're the head of an education department and you're not using AI",
AdjectiveOfA,
0,
);
}
#[test]
fn dont_flag_middle() {
// Can be an adjective in e.g. "middle child"
assert_lint_count(
"just to get to that part in the middle of a blizzard",
AdjectiveOfA,
0,
);
}
#[test]
fn dont_flag_chance() {
// Can be an adjective in e.g. "a chance encounter"
assert_lint_count(
"products that you overpay for because there are subtle details in the terms and conditions that reduce the size or chance of a payout.",
AdjectiveOfA,
0,
);
}
#[test]
fn dont_flag_potential() {
// Can be an adjective in e.g. "a potential candidate"
assert_lint_count(
"People that are happy to accept it for the potential of a reward.",
AdjectiveOfA,
0,
);
}
}

View file

@ -121,15 +121,6 @@ impl Matcher {
"wordlist" => "word list"
});
// mixing up than/then in context
triggers.extend(pt! {
"then","her" => "than her",
"then","hers" => "than hers",
"then","him" => "than him",
"then","his" => "than his",
"then","last","week" => "than last week"
});
// not a perfect fit for any of the other categories
triggers.extend(pt! {
"performing","this" => "perform this",

View file

@ -1,6 +1,5 @@
use super::{Lint, LintKind, PatternLinter};
use crate::Token;
use crate::char_string::char_string;
use crate::linting::Suggestion;
use crate::patterns::{
All, AnyCapitalization, Invert, OwnedPatternExt, Pattern, SequencePattern, WordSet,
@ -23,7 +22,7 @@ impl ThenThan {
.then_whitespace()
.then_any_capitalization_of("then")
.then_whitespace()
.then(Invert::new(AnyCapitalization::new(char_string!("that")))),
.then(Invert::new(AnyCapitalization::of("that"))),
),
// Denotes exceptions to the rule.
Box::new(Invert::new(WordSet::new(&["back", "this", "so", "but"]))),
@ -206,4 +205,131 @@ mod tests {
0,
);
}
#[test]
fn issue_720_school_but_then_his() {
assert_lint_count(
"She loved the atmosphere of the school but then his argument is that it lacks proper resources for students.",
ThenThan::default(),
0,
);
assert_lint_count(
"The teacher praised the efforts of the school but then his argument is that the curriculum needs to be updated.",
ThenThan::default(),
0,
);
assert_lint_count(
"They were excited about the new program at school but then his argument is that it won't be effective without proper training.",
ThenThan::default(),
0,
);
assert_lint_count(
"The community supported the school but then his argument is that funding is still a major issue.",
ThenThan::default(),
0,
);
}
#[test]
fn issue_720_so_then_these_resistors() {
assert_lint_count(
"So then these resistors are connected up in parallel to reduce the overall resistance.",
ThenThan::default(),
0,
);
assert_lint_count(
"So then these resistors are connected up to ensure the current flows properly.",
ThenThan::default(),
0,
);
assert_lint_count(
"So then these resistors are connected up to achieve the desired voltage drop.",
ThenThan::default(),
0,
);
assert_lint_count(
"So then these resistors are connected up to demonstrate the principles of series and parallel circuits.",
ThenThan::default(),
0,
);
assert_lint_count(
"So then these resistors are connected up to optimize the circuit's performance.",
ThenThan::default(),
0,
);
}
#[test]
fn issue_720_yes_so_then_sorry() {
assert_lint_count(
"Yes so then sorry you didn't receive the memo about the meeting changes.",
ThenThan::default(),
0,
);
assert_lint_count(
"Yes so then sorry you had to wait so long for a response from our team.",
ThenThan::default(),
0,
);
assert_lint_count(
"Yes so then sorry you felt left out during the discussion; we value your input.",
ThenThan::default(),
0,
);
assert_lint_count(
"Yes so then sorry you missed the deadline; we can discuss an extension.",
ThenThan::default(),
0,
);
assert_lint_count(
"Yes so then sorry you encountered issues with the software; let me help you troubleshoot.",
ThenThan::default(),
0,
);
}
#[test]
fn more_talented_then_her_issue_720() {
assert_suggestion_result(
"He was more talented then her at writing code.",
ThenThan::default(),
"He was more talented than her at writing code.",
);
}
#[test]
fn simpler_then_hers_issue_720() {
assert_suggestion_result(
"The design was simpler then hers in layout and color scheme.",
ThenThan::default(),
"The design was simpler than hers in layout and color scheme.",
);
}
#[test]
fn earlier_then_him_issue_720() {
assert_suggestion_result(
"We arrived earlier then him at the event.",
ThenThan::default(),
"We arrived earlier than him at the event.",
);
}
#[test]
fn more_robust_then_his_issue_720() {
assert_suggestion_result(
"This approach is more robust then his for handling edge cases.",
ThenThan::default(),
"This approach is more robust than his for handling edge cases.",
);
}
#[test]
fn patch_more_recently_then_last_week_issue_720() {
assert_suggestion_result(
"We submitted the patch more recently then last week, so they should have it already.",
ThenThan::default(),
"We submitted the patch more recently than last week, so they should have it already.",
);
}
}

View file

@ -1,14 +1,14 @@
[package]
name = "harper-html"
version = "0.26.0"
version = "0.27.0"
edition = "2024"
description = "The language checker for developers."
license = "Apache-2.0"
repository = "https://github.com/automattic/harper"
[dependencies]
harper-core = { path = "../harper-core", version = "0.26.0" }
harper-tree-sitter = { path = "../harper-tree-sitter", version = "0.26.0" }
harper-core = { path = "../harper-core", version = "0.27.0" }
harper-tree-sitter = { path = "../harper-tree-sitter", version = "0.27.0" }
tree-sitter-html = "0.19.0"
tree-sitter = "0.20.10"

View file

@ -1,14 +1,14 @@
[package]
name = "harper-literate-haskell"
version = "0.26.0"
version = "0.27.0"
edition = "2024"
description = "The language checker for developers."
license = "Apache-2.0"
repository = "https://github.com/automattic/harper"
[dependencies]
harper-core = { path = "../harper-core", version = "0.26.0" }
harper-tree-sitter = { path = "../harper-tree-sitter", version = "0.26.0" }
harper-comments = { path = "../harper-comments", version = "0.26.0" }
harper-core = { path = "../harper-core", version = "0.27.0" }
harper-tree-sitter = { path = "../harper-tree-sitter", version = "0.27.0" }
harper-comments = { path = "../harper-comments", version = "0.27.0" }
itertools = "0.14.0"
paste = "1.0.14"

View file

@ -1,6 +1,6 @@
[package]
name = "harper-ls"
version = "0.26.0"
version = "0.27.0"
edition = "2024"
description = "The language checker for developers."
license = "Apache-2.0"
@ -8,12 +8,12 @@ readme = "README.md"
repository = "https://github.com/automattic/harper"
[dependencies]
harper-literate-haskell = { path = "../harper-literate-haskell", version = "0.26.0" }
harper-stats = { path = "../harper-stats", version = "0.26.0" }
harper-core = { path = "../harper-core", version = "0.26.0", features = ["concurrent"] }
harper-comments = { path = "../harper-comments", version = "0.26.0" }
harper-typst = { path = "../harper-typst", version = "0.26.0" }
harper-html = { path = "../harper-html", version = "0.26.0" }
harper-stats = { path = "../harper-stats", version = "0.27.0" }
harper-literate-haskell = { path = "../harper-literate-haskell", version = "0.27.0" }
harper-core = { path = "../harper-core", version = "0.27.0", features = ["concurrent"] }
harper-comments = { path = "../harper-comments", version = "0.27.0" }
harper-typst = { path = "../harper-typst", version = "0.27.0" }
harper-html = { path = "../harper-html", version = "0.27.0" }
tower-lsp = "0.20.0"
tokio = { version = "1.44.1", features = ["fs", "rt", "rt-multi-thread", "macros", "io-std", "io-util", "net"] }
clap = { version = "4.5.34", features = ["derive"] }

View file

@ -292,7 +292,7 @@ impl Backend {
Some(Box::new(GitCommitParser::new_markdown(markdown_options)))
}
"html" => Some(Box::new(HtmlParser::default())),
"mail" | "plaintext" => Some(Box::new(PlainEnglish)),
"mail" | "plaintext" | "text" => Some(Box::new(PlainEnglish)),
"typst" => Some(Box::new(Typst)),
_ => None,
};

View file

@ -1,11 +1,11 @@
[package]
name = "harper-stats"
version = "0.26.0"
version = "0.27.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.217", features = ["derive"] }
harper-core = { path = "../harper-core", version = "0.26.0", features = ["concurrent"] }
harper-core = { path = "../harper-core", version = "0.27.0", features = ["concurrent"] }
uuid = { version = "1.12.0", features = ["serde", "v4"] }
serde_json = "1.0.140"
chrono = "0.4.40"

View file

@ -1,11 +1,11 @@
[package]
name = "harper-tree-sitter"
version = "0.26.0"
version = "0.27.0"
edition = "2024"
description = "The language checker for developers."
license = "Apache-2.0"
repository = "https://github.com/automattic/harper"
[dependencies]
harper-core = { path = "../harper-core", version = "0.26.0" }
harper-core = { path = "../harper-core", version = "0.27.0" }
tree-sitter = "0.20.10"

View file

@ -1,13 +1,13 @@
[package]
name = "harper-typst"
version = "0.26.0"
version = "0.27.0"
edition = "2024"
description = "The language checker for developers."
license = "Apache-2.0"
repository = "https://github.com/automattic/harper"
[dependencies]
harper-core = { path = "../harper-core", version = "0.26.0" }
harper-core = { path = "../harper-core", version = "0.27.0" }
typst-syntax = { version = "0.13.1" }
ordered-float = { version = "5.0.0", features = ["serde"] }
itertools = "0.14.0"

View file

@ -14,9 +14,9 @@ console_error_panic_hook = "0.1.7"
tracing = "0.1.41"
tracing-wasm = "0.2.1"
wasm-bindgen = "0.2.97"
harper-core = { path = "../harper-core", version = "0.26.0", features = ["concurrent"] }
harper-core = { path = "../harper-core", version = "0.27.0", features = ["concurrent"] }
once_cell = "1.21.3"
serde-wasm-bindgen = "0.6.5"
serde_json = "1.0.140"
serde = { version = "1.0.219", features = ["derive"] }
harper-stats = { path = "../harper-stats", version = "0.26.0", features = ["js"] }
harper-stats = { path = "../harper-stats", version = "0.27.0", features = ["js"] }

View file

@ -1,6 +1,6 @@
{
"name": "harper.js",
"version": "0.26.0",
"version": "0.27.0",
"license": "Apache-2.0",
"author": "Elijah Potter",
"description": "The grammar checker for developers.",

View file

@ -2,7 +2,7 @@
"name": "harper",
"displayName": "Harper",
"description": "The grammar checker for developers",
"version": "0.26.0",
"version": "0.27.0",
"private": true,
"author": "Elijah Potter",
"publisher": "elijah-potter",
@ -125,6 +125,18 @@
"default": true,
"description": "Corrects `a lot worst` to `a lot worse` for proper comparative usage."
},
"harper.linters.AWholeEntire": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Corrects the redundancy in `whole entire` to `whole` or `entire`."
},
"harper.linters.AdjectiveOfA": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "This rule looks for sequences of words of the form `adjective of a`."
},
"harper.linters.AlzheimersDisease": {
"scope": "resource",
"type": "boolean",
@ -149,6 +161,12 @@
"default": true,
"description": "A rule that looks for incorrect indefinite articles. For example, `this is an mule` would be flagged as incorrect."
},
"harper.linters.AnAnother": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Corrects `an another` and `a another`."
},
"harper.linters.AndIn": {
"scope": "resource",
"type": "boolean",
@ -161,6 +179,24 @@
"default": true,
"description": "Fixes the typo in `and the like`."
},
"harper.linters.AnotherAn": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Corrects `another an` to `another`."
},
"harper.linters.AnotherOnes": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Corrects `another ones`."
},
"harper.linters.AnotherThings": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Corrects `another things`."
},
"harper.linters.Anybody": {
"scope": "resource",
"type": "boolean",
@ -617,6 +653,12 @@
"default": true,
"description": "Ensures `gotten rid of` is used instead of `gotten rid off`."
},
"harper.linters.GuineaBissau": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Checks for the correct official name of the African country."
},
"harper.linters.HadOf": {
"scope": "resource",
"type": "boolean",
@ -725,6 +767,12 @@
"default": true,
"description": "Detects and corrects a spacing error where `in the` is mistakenly written as `int he`. Proper spacing is essential for readability and grammatical correctness in common phrases."
},
"harper.linters.InflectedVerbAfterTo": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "This rule looks for `to verb` where `verb` is not in the infinitive form."
},
"harper.linters.Insofar": {
"scope": "resource",
"type": "boolean",
@ -1109,12 +1157,6 @@
"default": true,
"description": "Flags oxymoronic phrases (e.g. `amateur expert`, `increasingly less`, etc.)."
},
"harper.linters.PerformThis": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Corrects `performing this` to `perform this` for proper verb usage."
},
"harper.linters.PiggyBag": {
"scope": "resource",
"type": "boolean",
@ -1157,6 +1199,18 @@
"default": true,
"description": "Typo: `moot` (meaning debatable) is correct rather than `mute`."
},
"harper.linters.PortAuPrince": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Checks for the correct official name of the capital of Haiti."
},
"harper.linters.PortoNovo": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Checks for the correct official name of the capital of Benin."
},
"harper.linters.PossessiveYour": {
"scope": "resource",
"type": "boolean",
@ -1349,6 +1403,12 @@
"default": true,
"description": "Repeating the word \"that\" is often redundant. The phrase `that which` is easier to read."
},
"harper.linters.TheAnother": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Corrects `the another`."
},
"harper.linters.ThenThan": {
"scope": "resource",
"type": "boolean",
@ -1499,6 +1559,12 @@
"default": true,
"description": "Ensures `whet your appetite` is used correctly, distinguishing it from the incorrect `wet` variation."
},
"harper.linters.WholeEntire": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Corrects the redundancy in `whole entire` to `whole` or `entire`."
},
"harper.linters.Widespread": {
"scope": "resource",
"type": "boolean",

View file

@ -2,6 +2,7 @@ import type { ExtensionContext } from 'vscode';
import type { Executable, LanguageClientOptions } from 'vscode-languageclient/node';
import { Uri, commands, window, workspace } from 'vscode';
import { StatusBarAlignment, type StatusBarItem } from 'vscode';
import { LanguageClient, ResponseError, TransportKind } from 'vscode-languageclient/node';
// There's no publicly available extension manifest type except for the internal one from VS Code's
@ -51,6 +52,8 @@ const clientOptions: LanguageClientOptions = {
},
};
let dialectStatusBarItem: StatusBarItem | undefined;
export async function activate(context: ExtensionContext): Promise<void> {
serverOptions.command = getExecutablePath(context);
@ -99,6 +102,22 @@ export async function activate(context: ExtensionContext): Promise<void> {
);
await startLanguageServer();
// <= 100 is between Copilot and Notifications.
// 101..102 is between the magnifying glass and encoding
// >= 103 is left of the magnifying glass
dialectStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Right, 101);
context.subscriptions.push(dialectStatusBarItem);
context.subscriptions.push(
workspace.onDidChangeConfiguration(async (event) => {
if (event.affectsConfiguration('harper.dialect')) {
updateDialectStatusBar();
}
}),
);
updateDialectStatusBar();
}
function getExecutablePath(context: ExtensionContext): string {
@ -158,6 +177,20 @@ function showError(message: string, error: Error | unknown): void {
});
}
function updateDialectStatusBar(): void {
if (!dialectStatusBarItem) return;
const dialect = workspace.getConfiguration('harper').get<string>('dialect', '');
if (dialect === '') return;
const flagAndCode = getFlagAndCode(dialect);
if (!flagAndCode) return;
dialectStatusBarItem.text = flagAndCode.join(' ');
dialectStatusBarItem.show();
console.log(`** dialect set to ${dialect} **`, dialect);
}
export function deactivate(): Thenable<void> | undefined {
if (!client) {
return undefined;
@ -165,3 +198,12 @@ export function deactivate(): Thenable<void> | undefined {
return client.stop();
}
function getFlagAndCode(dialect: string): string[] | undefined {
return {
American: ['🇺🇸', 'US'],
Australian: ['🇦🇺', 'AU'],
British: ['🇬🇧', 'GB'],
Canadian: ['🇨🇦', 'CA'],
}[dialect];
}

View file

@ -55,7 +55,7 @@ describe('Integration >', () => {
it('gives correct diagnostics for untitled', async () => {
const untitledUri = await openUntitled('Errorz');
await waitForUpdatesFromOpenedFile();
await waitForHarperToActivate(); // requires a longer time
compareActualVsExpectedDiagnostics(
getActualDiagnostics(untitledUri),
@ -69,9 +69,7 @@ describe('Integration >', () => {
it('gives correct diagnostics when language is changed', async () => {
const untitledUri = await openUntitled('Errorz # Errorz');
await setTextDocumentLanguage(untitledUri, 'plaintext');
// Wait for `harper-ls` to send diagnostics
await waitForUpdatesFromConfigChange();
await waitForHarperToActivate(); // requires a longer time
compareActualVsExpectedDiagnostics(
getActualDiagnostics(untitledUri),

View file

@ -82,7 +82,7 @@ let mobile = $derived(width < 640);
<span slot="title">Native Everywhere</span>
<span slot="subtitle"
>Harper is both available as a <a
href="https://github.com/automattic/harper/tree/master/harper-ls">language server</a
href="https://writewithharper.com/docs/integrations/language-server">language server</a
>, and through WebAssembly, so you can get fantastic grammar checking anywhere you work.
<br /><br /> That said, we take extra care to make sure the
<a href="https://marketplace.visualstudio.com/items?itemName=elijah-potter.harper"

View file

@ -15,7 +15,7 @@ Most Harper users are catching their mistakes in Neovim, [Obsidian](./integratio
## How Does It Work?
Harper takes advantage of decades of natural language research analyze how exactly how your words come together.
Harper takes advantage of decades of natural language research to analyze exactly how your words come together.
If something is off, Harper lets you know.
In a way, Harper is an error-tolerant parser for English.

View file

@ -244,7 +244,7 @@ These configs are under the `markdown` key:
| Markdown | `markdown` | |
| Nix | `nix` | ✅ |
| PHP | `php` | ✅ |
| Plain Text | `plaintext` | |
| Plain Text | `plaintext`/`text` | |
| Python | `python` | ✅ |
| Ruby | `ruby` | ✅ |
| Rust | `rust` | ✅ |

View file

@ -1,5 +1,3 @@
import * as React from 'react';
export default function Logo() {
return (
<svg

View file

@ -9,7 +9,7 @@ import LinterProvider from './LinterProvider';
function Sidebar() {
return (
<>
<PluginSidebarMoreMenuItem target="harper-sidebar" icon={Logo}>
<PluginSidebarMoreMenuItem target="harper-sidebar" icon={Logo()}>
Harper
</PluginSidebarMoreMenuItem>
<PluginSidebar name="harper-sidebar" title="Harper" icon={Logo}>