mirror of
https://github.com/Automattic/harper.git
synced 2025-12-23 08:48:15 +00:00
Merge branch 'master' into local-stats
This commit is contained in:
commit
e5a5f7e5bb
26 changed files with 428 additions and 82 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -53,7 +53,10 @@
|
|||
"condition": "."
|
||||
}
|
||||
],
|
||||
"adds_metadata": {},
|
||||
"adds_metadata": {
|
||||
"adjective": {},
|
||||
"adverb": {}
|
||||
},
|
||||
"gifts_metadata": {}
|
||||
},
|
||||
"U": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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` | ✅ |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export default function Logo() {
|
||||
return (
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue