Skip to content

Commit

Permalink
feat: support custom config file
Browse files Browse the repository at this point in the history
resolves #19
  • Loading branch information
tekumara committed Dec 10, 2023
1 parent 95570a5 commit 67886b9
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 43 deletions.
59 changes: 59 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Config files will be read from the workspace folder or its parents. If there is

This extension contributes the following settings:

- `typos.config`: Custom config. Used together with any workspace config files, taking precedence for settings declared in both. Equivalent to the typos `--config` [cli argument](https://github.com/crate-ci/typos/blob/master/docs/reference.md).
- `typos.diagnosticSeverity`: How typos are rendered in the editor, eg: as errors, warnings, information, or hints.
- `typos.logLevel`: Logging level of the language server. Logs appear in the _Output -> Typos_ pane.
- `typos.path`: Path to the `typos-lsp` binary. If empty the bundled binary will be used.
Expand Down
1 change: 1 addition & 0 deletions crates/typos-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ typos-cli = "1.16"
serde = { version = "1.0", features = ["derive"] }
ignore = "0.4.20"
matchit = "0.7.1"
shellexpand = "3.1.0"

[dev-dependencies]
test-log = { version = "0.2.11", features = ["trace"] }
Expand Down
98 changes: 57 additions & 41 deletions crates/typos-lsp/src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use matchit::{Match, Router};

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

use bstr::ByteSlice;
Expand All @@ -23,6 +23,7 @@ pub struct Backend<'s, 'p> {
#[derive(Default)]
struct BackendState<'s> {
severity: Option<DiagnosticSeverity>,
config: Option<PathBuf>,
workspace_folders: Vec<WorkspaceFolder>,
router: Router<TyposCli<'s>>,
}
Expand All @@ -32,28 +33,38 @@ struct TyposCli<'s> {
engine: policy::ConfigEngine<'s>,
}

impl<'s> TryFrom<&PathBuf> for TyposCli<'s> {
type Error = anyhow::Error;

// initialise an engine and overrides using the config file from path or its parent
fn try_from(path: &PathBuf) -> anyhow::Result<Self, Self::Error> {
// leak to get a 'static which is needed to satisfy the 's lifetime
// but does mean memory will grow unbounded
let storage = Box::leak(Box::new(policy::ConfigStorage::new()));
let mut engine = typos_cli::policy::ConfigEngine::new(storage);
engine.init_dir(path)?;

let walk_policy = engine.walk(path);

// add any explicit excludes
let mut overrides = OverrideBuilder::new(path);
for pattern in walk_policy.extend_exclude.iter() {
overrides.add(&format!("!{}", pattern))?;
// initialise an engine and overrides using the config file from path or its parent
fn try_new_cli<'s>(
path: &Path,
config: Option<&Path>,
) -> anyhow::Result<TyposCli<'s>, anyhow::Error> {
// leak to get a 'static which is needed to satisfy the 's lifetime
// but does mean memory will grow unbounded
let storage = Box::leak(Box::new(policy::ConfigStorage::new()));
let mut engine = typos_cli::policy::ConfigEngine::new(storage);

// TODO: currently mimicking typos here but do we need to create and the update
// a default config?
let mut overrides = typos_cli::config::Config::default();
if let Some(config_path) = config {
let custom = typos_cli::config::Config::from_file(config_path)?;
if let Some(custom) = custom {
overrides.update(&custom);
engine.set_overrides(overrides);
}
let overrides = overrides.build()?;
}

Ok(TyposCli { overrides, engine })
engine.init_dir(path)?;
let walk_policy = engine.walk(path);

// add any explicit excludes
let mut overrides = OverrideBuilder::new(path);
for pattern in walk_policy.extend_exclude.iter() {
overrides.add(&format!("!{}", pattern))?;
}
let overrides = overrides.build()?;

Ok(TyposCli { overrides, engine })
}

impl<'s> BackendState<'s> {
Expand Down Expand Up @@ -95,8 +106,8 @@ impl<'s> BackendState<'s> {
"/*p"
);
tracing::debug!("Adding route {}", &path_wildcard);
let config = TyposCli::try_from(&path)?;
self.router.insert(path_wildcard, config)?;
let cli = try_new_cli(&path, self.config.as_deref())?;
self.router.insert(path_wildcard, cli)?;
}
Ok(())
}
Expand Down Expand Up @@ -146,32 +157,37 @@ impl LanguageServer for Backend<'static, 'static> {
let mut state = self.state.lock().unwrap();

if let Some(ops) = params.initialization_options {
if let Some(value) = ops
.as_object()
.and_then(|o| o.get("diagnosticSeverity").cloned())
{
match value.as_str().unwrap_or("").to_lowercase().as_str() {
"error" => {
state.severity = Some(DiagnosticSeverity::ERROR);
}
"warning" => {
state.severity = Some(DiagnosticSeverity::WARNING);
}
"information" => {
state.severity = Some(DiagnosticSeverity::INFORMATION);
}
"hint" => {
state.severity = Some(DiagnosticSeverity::HINT);
if let Some(values) = ops.as_object() {
if let Some(value) = values.get("diagnosticSeverity").cloned() {
match value.as_str().unwrap_or("").to_lowercase().as_str() {
"error" => {
state.severity = Some(DiagnosticSeverity::ERROR);
}
"warning" => {
state.severity = Some(DiagnosticSeverity::WARNING);
}
"information" => {
state.severity = Some(DiagnosticSeverity::INFORMATION);
}
"hint" => {
state.severity = Some(DiagnosticSeverity::HINT);
}
_ => {
tracing::warn!("Unknown diagnostic severity: {}", value);
}
}
_ => {
tracing::warn!("Unknown diagnostic severity: {}", value);
}
if let Some(value) = values.get("config").cloned() {
if let Some(value) = value.as_str() {
let expanded_path = PathBuf::from(shellexpand::tilde(value).to_string());
state.config = Some(expanded_path);
}
}
}
}

if let Err(e) = state.set_workspace_folders(params.workspace_folders.unwrap_or_default()) {
tracing::warn!("Cannot set workspace folders: {}", e);
tracing::warn!("Falling back to default config: {}", e);
}

Ok(InitializeResult {
Expand Down
3 changes: 3 additions & 0 deletions crates/typos-lsp/tests/custom_typos.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[default.extend-words]
# tell typos which of the several possible corrections to use
fo = "go"
82 changes: 82 additions & 0 deletions crates/typos-lsp/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,88 @@ async fn test_config_file_e2e() {
);
}

// TODO refactor and extract the boilerplate
#[test_log::test(tokio::test)]
async fn test_custom_config_file_e2e() {
let workspace_folder_uri =
Url::from_file_path(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests")).unwrap();

let custom_config = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("custom_typos.toml");

let initialize = format!(
r#"{{
"jsonrpc": "2.0",
"method": "initialize",
"params": {{
"initializationOptions": {{
"diagnosticSeverity": "Warning",
"config": "{}"
}},
"capabilities": {{
"textDocument": {{ "publishDiagnostics": {{ "dataSupport": true }} }}
}},
"workspaceFolders": [
{{
"uri": "{}",
"name": "tests"
}}
]
}},
"id": 1
}}
"#,
custom_config.to_string_lossy().replace("\\", "\\\\"), // escape windows path separators to make valid json
workspace_folder_uri,
);

println!("{}", initialize);

let did_open_diag_txt = format!(
r#"{{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {{
"textDocument": {{
"uri": "{}/diagnostics.txt",
"languageId": "plaintext",
"version": 1,
"text": "this is an apropriate test\nfo typos\n"
}}
}}
}}
"#,
workspace_folder_uri
);

let (mut req_client, mut resp_client) = start_server();
let mut buf = vec![0; 10240];

req_client
.write_all(req(initialize).as_bytes())
.await
.unwrap();
let _ = resp_client.read(&mut buf).await.unwrap();

// check "fo" is corrected to "go" because of default.extend-words
// in custom_typos.toml which overrides typos.toml
tracing::debug!("{}", did_open_diag_txt);
req_client
.write_all(req(did_open_diag_txt).as_bytes())
.await
.unwrap();
let n = resp_client.read(&mut buf).await.unwrap();

similar_asserts::assert_eq!(
body(&buf[..n]).unwrap(),
format!(
r#"{{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{{"diagnostics":[{{"data":{{"corrections":["appropriate"]}},"message":"`apropriate` should be `appropriate`","range":{{"end":{{"character":21,"line":0}},"start":{{"character":11,"line":0}}}},"severity":2,"source":"typos"}},{{"data":{{"corrections":["go"]}},"message":"`fo` should be `go`","range":{{"end":{{"character":2,"line":1}},"start":{{"character":0,"line":1}}}},"severity":2,"source":"typos"}}],"uri":"{}/diagnostics.txt","version":1}}}}"#,
workspace_folder_uri
),
);
}

fn start_server() -> (tokio::io::DuplexStream, tokio::io::DuplexStream) {
let (req_client, req_server) = tokio::io::duplex(1024);
let (resp_server, resp_client) = tokio::io::duplex(1024);
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
"type": "string",
"description": "Path to the `typos-lsp` binary. If empty the bundled binary will be used."
},
"typos.config": {
"scope": "machine-overridable",
"type": "string",
"description": "Path to a custom config file. Used together with any workspace config files, taking precedence for settings declared in both."
},
"typos.diagnosticSeverity": {
"scope": "window",
"type": "string",
Expand Down
6 changes: 4 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export async function activate(
vscode.workspace.onDidChangeConfiguration(
async (e: vscode.ConfigurationChangeEvent) => {
const restartTriggeredBy = [
"typos.path",
"typos.logLevel",
"typos.config",
"typos.diagnosticSeverity",
"typos.logLevel",
"typos.path",
].find((s) => e.affectsConfiguration(s));

if (restartTriggeredBy) {
Expand Down Expand Up @@ -99,6 +100,7 @@ async function createClient(
outputChannel: outputChannel,
traceOutputChannel: outputChannel,
initializationOptions: {
config: config.get("config") ? config.get("config") : null,
diagnosticSeverity: config.get("diagnosticSeverity"),
},
};
Expand Down

0 comments on commit 67886b9

Please sign in to comment.