Skip to content

Commit

Permalink
fixup! feat: code action to add a misspelling to the config file
Browse files Browse the repository at this point in the history
  • Loading branch information
mikavilpas committed Jun 21, 2024
1 parent 08753d6 commit ef9a6ad
Show file tree
Hide file tree
Showing 8 changed files with 373 additions and 99 deletions.
50 changes: 49 additions & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion crates/typos-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ matchit = "0.8.2"
shellexpand = "3.1.0"
regex = "1.10.4"
once_cell = "1.19.0"
toml = "0.8.12"
toml_edit = "0.22.14"

[dev-dependencies]
test-log = { version = "0.2.16", features = ["trace"] }
httparse = "1.8"
similar-asserts = "1.4"
tempfile = "3.10.1"
60 changes: 30 additions & 30 deletions crates/typos-lsp/src/lsp.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use ignore_typo_action::IGNORE_IN_PROJECT;
use matchit::Match;

use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::sync::Mutex;

use serde_json::{json, to_string};
use tower_lsp::lsp_types::*;
use tower_lsp::*;
use tower_lsp::{Client, LanguageServer};
use typos_cli::config::DictConfig;
use typos_cli::policy;

use crate::state::{url_path_sanitised, BackendState};
Expand All @@ -19,6 +19,8 @@ pub struct Backend<'s, 'p> {
default_policy: policy::Policy<'p, 'p, 'p>,
}

mod ignore_typo_action;

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct DiagnosticData<'c> {
corrections: Vec<Cow<'c, str>>,
Expand All @@ -28,8 +30,6 @@ struct DiagnosticData<'c> {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct IgnoreInProjectCommandArguments {
typo: String,
/// The file that contains the typo to ignore
typo_file_path: String,
/// The configuration file that should be modified to ignore the typo
config_file_path: String,
}
Expand Down Expand Up @@ -109,8 +109,7 @@ impl LanguageServer for Backend<'static, 'static> {
},
)),
execute_command_provider: Some(ExecuteCommandOptions {
// TODO this magic string should be a constant
commands: vec!["ignore-in-project".to_string()],
commands: vec![IGNORE_IN_PROJECT.to_string()],
work_done_progress_options: WorkDoneProgressOptions::default(),
}),
workspace: Some(WorkspaceServerCapabilities {
Expand Down Expand Up @@ -218,27 +217,38 @@ impl LanguageServer for Backend<'static, 'static> {
.router
.at(params.text_document.uri.to_file_path().unwrap().to_str().unwrap())
{
let typo_file: &Url = &params.text_document.uri;
let config_files =
value.config_files_in_project(Path::new(typo_file.as_str()));
let config_files = value.config_files_in_project();

suggestions.push(CodeActionOrCommand::Command(Command {
title: format!("Ignore `{}` in the project", typo),
command: "ignore-in-project".to_string(),
command: IGNORE_IN_PROJECT.to_string(),
arguments: Some(
[serde_json::to_value(IgnoreInProjectCommandArguments {
typo: typo.to_string(),
typo_file_path: typo_file.to_string(),
config_file_path: config_files
.project_root
.path
.to_string_lossy()
.to_string(),
})
.unwrap()]
.into(),
.unwrap()]
.into(),
),
}));

if let Some(explicit_config) = &config_files.explicit {
suggestions.push(CodeActionOrCommand::Command(Command {
title: format!("Ignore `{}` in the configuration file", typo),
command: IGNORE_IN_PROJECT.to_string(),
arguments: Some(
[serde_json::to_value(IgnoreInProjectCommandArguments {
typo: typo.to_string(),
config_file_path: explicit_config.to_string_lossy().to_string(),
})
.unwrap()]
.into(),
),
}));
}
} else {
tracing::warn!(
"code_action: Cannot create a code action for ignoring a typo in the project. Reason: No route found for file '{}'",
Expand Down Expand Up @@ -275,8 +285,7 @@ impl LanguageServer for Backend<'static, 'static> {
to_string(&raw_params).unwrap_or_default()
);

// TODO reduce the nesting
if raw_params.command == "ignore-in-project" {
if raw_params.command == IGNORE_IN_PROJECT {
let argument = raw_params
.arguments
.into_iter()
Expand All @@ -289,21 +298,12 @@ impl LanguageServer for Backend<'static, 'static> {
..
}) = serde_json::from_value::<IgnoreInProjectCommandArguments>(argument)
{
let mut config = typos_cli::config::Config::from_file(Path::new(&config_file_path))
.ok()
.flatten()
.unwrap_or_default();

config.default.dict.update(&DictConfig {
extend_words: HashMap::from([(typo.clone().into(), typo.into())]),
..Default::default()
});

std::fs::write(
&config_file_path,
toml::to_string_pretty(&config).expect("cannot serialize config"),
ignore_typo_action::ignore_typo_in_config_file(
PathBuf::from(config_file_path),
typo,
)
.unwrap_or_else(|_| panic!("Cannot write to {}", config_file_path));
.unwrap();
self.state.lock().unwrap().update_router().unwrap();
};
}

Expand Down
111 changes: 111 additions & 0 deletions crates/typos-lsp/src/lsp/ignore_typo_action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use std::{
fs::read_to_string,
path::{Path, PathBuf},
};

use anyhow::{anyhow, Context};
use toml_edit::DocumentMut;

pub(super) const IGNORE_IN_PROJECT: &str = "ignore-in-project";

pub(super) fn ignore_typo_in_config_file(config_file: PathBuf, typo: String) -> anyhow::Result<()> {
let input = read_to_string(&config_file)
.with_context(|| anyhow!("Cannot read config file at {}", config_file.display()))
.unwrap_or("".to_string());

let document = add_typo(input, typo, &config_file)?;

std::fs::write(&config_file, document.to_string())
.with_context(|| anyhow!("Cannot write config file to {}", config_file.display()))?;

Ok(())
}

fn add_typo(
input: String,
typo: String,
config_file_path: &Path,
) -> Result<DocumentMut, anyhow::Error> {
// preserve comments and formatting
let mut document = input
.parse::<DocumentMut>()
.with_context(|| anyhow!("Cannot parse config file at {}", config_file_path.display()))?;
let extend_words = document
.entry("default")
.or_insert(toml_edit::table())
.as_table_mut()
.context("Cannot get 'default' table")?
.entry("extend-words")
.or_insert(toml_edit::table())
.as_table_mut()
.context("Cannot get 'extend-words' table")?;
extend_words[typo.as_str()] = toml_edit::value(typo.clone());
Ok(document)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_add_typo_to_empty_file() {
let empty_file = "";
let document = add_typo(
empty_file.to_string(),
"typo".to_string(),
PathBuf::from("test.toml").as_path(),
)
.unwrap();

similar_asserts::assert_eq!(
document.to_string(),
[
"[default]",
"",
"[default.extend-words]",
"typo = \"typo\"",
""
]
.join("\n")
);
}

#[test]
fn test_add_typo_to_existing_file() -> anyhow::Result<()> {
// should preserve comments and formatting

let existing_file = [
"[files] # comment",
"# comment",
"extend-exclude = [\"CHANGELOG.md\", \"crates/typos-lsp/tests/integration_test.rs\"]",
]
.join("\n");

// make sure the config is valid (so the test makes sense)
let _ = typos_cli::config::Config::from_toml(&existing_file)?;

let document = add_typo(
existing_file.to_string(),
"typo".to_string(),
PathBuf::from("test.toml").as_path(),
)?;

similar_asserts::assert_eq!(
document.to_string(),
[
"[files] # comment",
"# comment",
"extend-exclude = [\"CHANGELOG.md\", \"crates/typos-lsp/tests/integration_test.rs\"]",
"",
"[default]",
"",
"[default.extend-words]",
"typo = \"typo\"",
""
]
.join("\n")
);

Ok(())
}
}
Loading

0 comments on commit ef9a6ad

Please sign in to comment.