Skip to content

Commit

Permalink
feat: wrote first draft of statistics logging
Browse files Browse the repository at this point in the history
  • Loading branch information
elijah-potter committed Jan 17, 2025
1 parent 37a3e72 commit b563994
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 3 deletions.
33 changes: 33 additions & 0 deletions Cargo.lock

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

19 changes: 18 additions & 1 deletion harper-core/src/linting/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl Default for Lint {
}
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Is, Default)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Is, Default, PartialEq, Eq, Hash)]
pub enum LintKind {
Spelling,
Capitalization,
Expand All @@ -42,6 +42,23 @@ pub enum LintKind {
Miscellaneous,
}

impl LintKind {
pub fn new_from_str(s: &str) -> Option<Self> {
Some(match s {
"Spelling" => LintKind::Spelling,
"Capitalization" => LintKind::Capitalization,
"Formatting" => LintKind::Formatting,
"Repetition" => LintKind::Repetition,
"Readability" => LintKind::Readability,
"Miscellaneous" => LintKind::Miscellaneous,
"Enhancement" => LintKind::Enhancement,
"Word Choice" => LintKind::WordChoice,
"Style" => LintKind::Style,
_ => return None,
})
}
}

impl Display for LintKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Expand Down
2 changes: 2 additions & 0 deletions harper-ls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ resolve-path = "0.1.0"
open = "5.3.0"
futures = "0.3.31"
serde = { version = "1.0.214", features = ["derive"] }
uuid = { version = "1.12.0", features = ["serde", "v4"] }
csv = "1.3.1"

[features]
default = []
47 changes: 46 additions & 1 deletion harper-ls/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Component, PathBuf};
use std::sync::Arc;

use anyhow::{anyhow, Context, Result};
use futures::future::join;
use harper_comments::CommentParser;
use harper_core::linting::{LintGroup, Linter};
use harper_core::linting::{LintGroup, LintKind, Linter};
use harper_core::parsers::{CollapseIdentifiers, IsolateEnglish, Markdown, Parser, PlainEnglish};
use harper_core::{
Dictionary, Document, FstDictionary, FullDictionary, MergedDictionary, Token, TokenKind,
Expand Down Expand Up @@ -37,17 +40,20 @@ use crate::dictionary_io::{load_dict, save_dict};
use crate::document_state::DocumentState;
use crate::git_commit_parser::GitCommitParser;
use crate::pos_conv::range_to_span;
use crate::stats::{LintRecord, Stats};

pub struct Backend {
client: Client,
config: RwLock<Config>,
stats: RwLock<Stats>,
doc_state: Mutex<HashMap<Url, DocumentState>>,
}

impl Backend {
pub fn new(client: Client, config: Config) -> Self {
Self {
client,
stats: RwLock::new(Stats::new()),
config: RwLock::new(config),
doc_state: Mutex::new(HashMap::new()),
}
Expand Down Expand Up @@ -121,6 +127,26 @@ impl Backend {
.map_err(|err| anyhow!("Unable to save the dictionary to file: {err}"))
}

async fn save_stats(&self) -> Result<()> {
let (config, stats) = join(self.config.read(), self.stats.read()).await;

if let Some(parent) = config.stats_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}

let mut writer = BufWriter::new(
OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(&config.stats_path)?,
);
stats.write_csv(&mut writer)?;
writer.flush()?;

Ok(())
}

async fn generate_global_dictionary(&self) -> Result<MergedDictionary> {
let mut dict = MergedDictionary::new();
dict.add_dictionary(FstDictionary::curated());
Expand Down Expand Up @@ -390,6 +416,7 @@ impl LanguageServer for Backend {
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
execute_command_provider: Some(ExecuteCommandOptions {
commands: vec![
"HarperRecordLint".to_owned(),
"HarperAddToUserDict".to_owned(),
"HarperAddToFileDict".to_owned(),
"HarperOpen".to_owned(),
Expand Down Expand Up @@ -523,6 +550,20 @@ impl LanguageServer for Backend {
info!("Received command: \"{}\"", params.command.as_str());

match params.command.as_str() {
"HarperRecordLint" => {
let Some(kind) = LintKind::new_from_str(&first) else {
error!("Unable to deserialize LintKind.");
return Ok(None);
};

let Ok(record) = LintRecord::record_now(kind) else {
error!("System time error");
return Ok(None);
};

let mut stats = self.stats.write().await;
stats.lint_applied(record);
}
"HarperAddToUserDict" => {
let word = &first.chars().collect::<Vec<_>>();

Expand Down Expand Up @@ -646,6 +687,10 @@ impl LanguageServer for Backend {
.await;
}

if self.save_stats().await.is_err() {
error!("Unable to save stats.")
}

Ok(())
}
}
10 changes: 10 additions & 0 deletions harper-ls/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ impl CodeActionConfig {
pub struct Config {
pub user_dict_path: PathBuf,
pub file_dict_path: PathBuf,
pub stats_path: PathBuf,
pub lint_config: LintGroupConfig,
pub diagnostic_severity: DiagnosticSeverity,
pub code_action_config: CodeActionConfig,
Expand Down Expand Up @@ -100,6 +101,14 @@ impl Config {
}
}

if let Some(v) = value.get("statsPath") {
if let Value::String(path) = v {
base.file_dict_path = path.try_resolve()?.to_path_buf();
} else {
bail!("fileDict path must be a string.");
}
}

if let Some(v) = value.get("linters") {
base.lint_config = serde_json::from_value(v.clone())?;
}
Expand Down Expand Up @@ -135,6 +144,7 @@ impl Default for Config {
file_dict_path: data_local_dir()
.unwrap()
.join("harper-ls/file_dictionaries/"),
stats_path: data_local_dir().unwrap().join("harper-ls/stats.txt"),
lint_config: LintGroupConfig::default(),
diagnostic_severity: DiagnosticSeverity::Hint,
code_action_config: CodeActionConfig::default(),
Expand Down
8 changes: 7 additions & 1 deletion harper-ls/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::collections::HashMap;
use std::str::FromStr;

use harper_core::linting::{Lint, Suggestion};
use harper_core::CharStringExt;
use serde_json::Value;
use tower_lsp::lsp_types::{
CodeAction, CodeActionKind, CodeActionOrCommand, Command, Diagnostic, TextEdit, Url,
WorkspaceEdit,
Expand Down Expand Up @@ -60,7 +62,11 @@ pub fn lint_to_code_actions<'a>(
document_changes: None,
change_annotations: None,
}),
command: None,
command: Some(Command {
title: "Record lint statistic".to_owned(),
command: "HarperRecordLint".to_owned(),
arguments: Some(vec![Value::String(lint.lint_kind.to_string())]),
}),
is_preferred: None,
disabled: None,
data: None,
Expand Down
1 change: 1 addition & 0 deletions harper-ls/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod dictionary_io;
mod document_state;
mod git_commit_parser;
mod pos_conv;
mod stats;

use backend::Backend;
use clap::Parser;
Expand Down
24 changes: 24 additions & 0 deletions harper-ls/src/stats/lint_record.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};

use harper_core::linting::LintKind;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Deserialize, Serialize)]
pub struct LintRecord {
pub kind: LintKind,
/// Recorded as seconds from the Unix Epoch
pub when: u64,
pub uuid: Uuid,
}

impl LintRecord {
/// Record a new instance at the current system time.
pub fn record_now(kind: LintKind) -> Result<Self, SystemTimeError> {
Ok(Self {
kind,
when: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
uuid: Uuid::new_v4(),
})
}
}
28 changes: 28 additions & 0 deletions harper-ls/src/stats/lint_summary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::collections::HashMap;

use harper_core::linting::LintKind;

pub struct LintSummary {
counts: HashMap<LintKind, usize>,
}

impl LintSummary {
pub fn new() -> Self {
Self {
counts: HashMap::new(),
}
}

/// Increment the count for a particular lint kind.
pub fn inc(&mut self, kind: LintKind) {
self.counts
.entry(kind)
.and_modify(|counter| *counter += 1)
.or_insert(1);
}

/// Get the count for a particular lint kind.
pub fn get(&self, kind: LintKind) -> usize {
self.counts.get(&kind).copied().unwrap_or(0)
}
}
52 changes: 52 additions & 0 deletions harper-ls/src/stats/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
mod lint_record;
mod lint_summary;

use std::io::Write;

pub use lint_record::LintRecord;
pub use lint_summary::LintSummary;
use tokio::io;

pub struct Stats {
/// A record of the lints the user has applied.
lints_applied: Vec<LintRecord>,
}

impl Stats {
pub fn new() -> Self {
Self {
lints_applied: Vec::new(),
}
}

/// Count the number of each kind of lint applied.
pub fn summarize_lints_applied(&self) -> LintSummary {
let mut summary = LintSummary::new();

for lint in &self.lints_applied {
summary.inc(lint.kind);
}

summary
}

pub fn lint_applied(&mut self, record: LintRecord) {
self.lints_applied.push(record);
}

pub fn write_csv(&self, w: &mut impl Write) -> io::Result<()> {
let mut writer = csv::WriterBuilder::new().has_headers(false).from_writer(w);

for record in &self.lints_applied {
writer.serialize(record)?;
}

Ok(())
}
}

impl Default for Stats {
fn default() -> Self {
Self::new()
}
}

0 comments on commit b563994

Please sign in to comment.