Skip to content

Commit 6646630

Browse files
committed
Support external tool as formatter
1 parent 349ecfb commit 6646630

File tree

6 files changed

+176
-4
lines changed

6 files changed

+176
-4
lines changed

crates/emmylua_code_analysis/resources/schema.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@
5959
"enable": true
6060
}
6161
},
62+
"format": {
63+
"$ref": "#/$defs/EmmyrcReformat",
64+
"default": {
65+
"externalTool": null
66+
}
67+
},
6268
"hint": {
6369
"$ref": "#/$defs/EmmyrcInlayHint",
6470
"default": {
@@ -618,6 +624,30 @@
618624
}
619625
}
620626
},
627+
"EmmyrcExternalTool": {
628+
"type": "object",
629+
"properties": {
630+
"args": {
631+
"description": "The arguments to pass to the external tool.",
632+
"type": "array",
633+
"default": [],
634+
"items": {
635+
"type": "string"
636+
}
637+
},
638+
"program": {
639+
"description": "The command to run the external tool.",
640+
"type": "string",
641+
"default": ""
642+
},
643+
"timeout": {
644+
"type": "integer",
645+
"format": "uint64",
646+
"default": 5000,
647+
"minimum": 0
648+
}
649+
}
650+
},
621651
"EmmyrcFilenameConvention": {
622652
"oneOf": [
623653
{
@@ -802,6 +832,23 @@
802832
}
803833
}
804834
},
835+
"EmmyrcReformat": {
836+
"type": "object",
837+
"properties": {
838+
"externalTool": {
839+
"description": "Whether to enable internal code reformatting.",
840+
"anyOf": [
841+
{
842+
"$ref": "#/$defs/EmmyrcExternalTool"
843+
},
844+
{
845+
"type": "null"
846+
}
847+
],
848+
"default": null
849+
}
850+
}
851+
},
805852
"EmmyrcResource": {
806853
"type": "object",
807854
"properties": {

crates/emmylua_code_analysis/src/config/configs/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod hover;
88
mod inlayhint;
99
mod inline_values;
1010
mod references;
11+
mod reformat;
1112
mod resource;
1213
mod runtime;
1314
mod semantictoken;
@@ -25,6 +26,7 @@ pub use hover::EmmyrcHover;
2526
pub use inlayhint::EmmyrcInlayHint;
2627
pub use inline_values::EmmyrcInlineValues;
2728
pub use references::EmmyrcReference;
29+
pub use reformat::{EmmyrcExternalTool, EmmyrcReformat};
2830
pub use resource::EmmyrcResource;
2931
pub use runtime::{EmmyrcLuaVersion, EmmyrcRuntime};
3032
pub use semantictoken::EmmyrcSemanticToken;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use schemars::JsonSchema;
2+
use serde::{Deserialize, Serialize};
3+
4+
#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone, Default)]
5+
#[serde(rename_all = "camelCase")]
6+
pub struct EmmyrcReformat {
7+
/// Whether to enable internal code reformatting.
8+
#[serde(default)]
9+
pub external_tool: Option<EmmyrcExternalTool>,
10+
}
11+
12+
#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone, Default)]
13+
#[serde(rename_all = "camelCase")]
14+
pub struct EmmyrcExternalTool {
15+
/// The command to run the external tool.
16+
#[serde(default)]
17+
pub program: String,
18+
/// The arguments to pass to the external tool.
19+
#[serde(default)]
20+
pub args: Vec<String>,
21+
#[serde(default = "default_timeout")]
22+
pub timeout: u64,
23+
}
24+
25+
fn default_timeout() -> u64 {
26+
5000
27+
}

crates/emmylua_code_analysis/src/config/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77
path::{Path, PathBuf},
88
};
99

10+
pub use crate::config::configs::{EmmyrcExternalTool, EmmyrcReformat};
1011
pub use config_loader::{load_configs, load_configs_raw};
1112
use configs::{
1213
EmmyrcCodeAction, EmmyrcCodeLens, EmmyrcCompletion, EmmyrcDiagnostic, EmmyrcDoc,
@@ -59,6 +60,8 @@ pub struct Emmyrc {
5960
pub inline_values: EmmyrcInlineValues,
6061
#[serde(default)]
6162
pub doc: EmmyrcDoc,
63+
#[serde(default)]
64+
pub format: EmmyrcReformat,
6265
}
6366

6467
impl Emmyrc {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use emmylua_code_analysis::EmmyrcExternalTool;
2+
use std::process::Stdio;
3+
use std::time::Duration;
4+
use tokio::io::AsyncWriteExt;
5+
use tokio::process::Command;
6+
use tokio::time::timeout;
7+
8+
pub async fn external_tool_format(
9+
emmyrc_external_tool: &EmmyrcExternalTool,
10+
text: &str,
11+
file_path: &str,
12+
) -> Option<String> {
13+
let exe_path = &emmyrc_external_tool.program;
14+
let args = &emmyrc_external_tool.args;
15+
let timeout_duration = Duration::from_millis(emmyrc_external_tool.timeout as u64);
16+
17+
let mut cmd = Command::new(exe_path);
18+
19+
for arg in args {
20+
let processed_arg = arg.replace("${file}", file_path);
21+
cmd.arg(processed_arg);
22+
}
23+
24+
cmd.stdin(Stdio::piped())
25+
.stdout(Stdio::piped())
26+
.stderr(Stdio::piped());
27+
28+
let mut child = match cmd.spawn() {
29+
Ok(child) => child,
30+
Err(e) => {
31+
log::error!("Failed to spawn external formatter process: {}", e);
32+
return None;
33+
}
34+
};
35+
36+
if let Some(mut stdin) = child.stdin.take() {
37+
if let Err(e) = stdin.write_all(text.as_bytes()).await {
38+
log::error!("Failed to write to external formatter stdin: {}", e);
39+
return None;
40+
}
41+
if let Err(e) = stdin.shutdown().await {
42+
log::error!("Failed to close external formatter stdin: {}", e);
43+
return None;
44+
}
45+
}
46+
47+
let output = match timeout(timeout_duration, child.wait_with_output()).await {
48+
Ok(Ok(output)) => output,
49+
Ok(Err(e)) => {
50+
log::error!("External formatter process error: {}", e);
51+
return None;
52+
}
53+
Err(_) => {
54+
log::error!(
55+
"External formatter process timed out after {}ms",
56+
emmyrc_external_tool.timeout
57+
);
58+
return None;
59+
}
60+
};
61+
62+
if !output.status.success() {
63+
log::error!(
64+
"External formatter exited with non-zero status: {}. Stderr: {}",
65+
output.status,
66+
String::from_utf8_lossy(&output.stderr)
67+
);
68+
return None;
69+
}
70+
71+
match String::from_utf8(output.stdout) {
72+
Ok(formatted_text) => {
73+
log::debug!("External formatter completed successfully");
74+
Some(formatted_text)
75+
}
76+
Err(e) => {
77+
log::error!("External formatter output is not valid UTF-8: {}", e);
78+
None
79+
}
80+
}
81+
}

crates/emmylua_ls/src/handlers/document_formatting/mod.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
mod external_format;
2+
13
use emmylua_code_analysis::reformat_code;
24
use lsp_types::{
35
ClientCapabilities, DocumentFormattingParams, OneOf, ServerCapabilities, TextEdit,
46
};
57
use tokio_util::sync::CancellationToken;
68

7-
use crate::context::ServerContextSnapshot;
9+
use crate::{
10+
context::ServerContextSnapshot,
11+
handlers::document_formatting::external_format::external_tool_format,
12+
};
813

914
use super::RegisterCapabilities;
1015

@@ -15,8 +20,9 @@ pub async fn on_formatting_handler(
1520
) -> Option<Vec<TextEdit>> {
1621
let uri = params.text_document.uri;
1722
let analysis = context.analysis.read().await;
18-
let config_manager = context.workspace_manager.read().await;
19-
let client_id = config_manager.client_config.client_id;
23+
let workspace_manager = context.workspace_manager.read().await;
24+
let client_id = workspace_manager.client_config.client_id;
25+
let emmyrc = analysis.get_emmyrc();
2026

2127
let file_id = analysis.get_file_id(&uri)?;
2228
let syntax_tree = analysis
@@ -37,7 +43,13 @@ pub async fn on_formatting_handler(
3743
let text = document.get_text();
3844
let file_path = document.get_file_path();
3945
let normalized_path = file_path.to_string_lossy().to_string().replace("\\", "/");
40-
let mut formatted_text = reformat_code(text, &normalized_path);
46+
47+
let mut formatted_text = if let Some(external_config) = &emmyrc.format.external_tool {
48+
external_tool_format(&external_config, text, &normalized_path).await?
49+
} else {
50+
reformat_code(text, &normalized_path)
51+
};
52+
4153
if client_id.is_intellij() || client_id.is_other() {
4254
formatted_text = formatted_text.replace("\r\n", "\n");
4355
}

0 commit comments

Comments
 (0)