diff --git a/kclvm/tools/src/LSP/src/capabilities.rs b/kclvm/tools/src/LSP/src/capabilities.rs index 9938bcbed..dfd3c417b 100644 --- a/kclvm/tools/src/LSP/src/capabilities.rs +++ b/kclvm/tools/src/LSP/src/capabilities.rs @@ -36,6 +36,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti }) }), ), + document_formatting_provider: Some(OneOf::Left(true)), ..Default::default() } } diff --git a/kclvm/tools/src/LSP/src/formatting.rs b/kclvm/tools/src/LSP/src/formatting.rs new file mode 100644 index 000000000..d991a0c2b --- /dev/null +++ b/kclvm/tools/src/LSP/src/formatting.rs @@ -0,0 +1,25 @@ +use kclvm_tools::format::{format_source, FormatOptions}; +use lsp_types::{Position, Range, TextEdit}; + +pub(crate) fn format_single_file( + file: String, + src: String, +) -> anyhow::Result>> { + let (source, is_formatted) = format_source( + &file, + &src, + &FormatOptions { + omit_errors: true, + ..Default::default() + }, + ) + .map_err(|err| anyhow::anyhow!("Formmatting failed: {}", err))?; + if is_formatted { + Ok(Some(vec![TextEdit { + range: Range::new(Position::new(0, 0), Position::new(u32::MAX, u32::MAX)), + new_text: source, + }])) + } else { + Ok(None) + } +} diff --git a/kclvm/tools/src/LSP/src/lib.rs b/kclvm/tools/src/LSP/src/lib.rs index 3af069b3c..75677bf7f 100644 --- a/kclvm/tools/src/LSP/src/lib.rs +++ b/kclvm/tools/src/LSP/src/lib.rs @@ -4,6 +4,7 @@ mod config; mod db; mod dispatcher; mod find_ref; +mod formatting; mod from_lsp; mod notification; mod state; diff --git a/kclvm/tools/src/LSP/src/main.rs b/kclvm/tools/src/LSP/src/main.rs index 5d092e12c..196c693ac 100644 --- a/kclvm/tools/src/LSP/src/main.rs +++ b/kclvm/tools/src/LSP/src/main.rs @@ -19,6 +19,7 @@ mod state; mod to_lsp; mod util; +mod formatting; #[cfg(test)] mod tests; diff --git a/kclvm/tools/src/LSP/src/request.rs b/kclvm/tools/src/LSP/src/request.rs index 69836f88d..e85d8add6 100644 --- a/kclvm/tools/src/LSP/src/request.rs +++ b/kclvm/tools/src/LSP/src/request.rs @@ -1,15 +1,15 @@ -use std::{collections::HashMap, time::Instant}; - -use anyhow::Ok; use crossbeam_channel::Sender; -use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand, TextEdit}; + +use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand, Position, Range, TextEdit}; use ra_ap_vfs::VfsPath; +use std::{collections::HashMap, time::Instant}; use crate::{ completion::completion, db::AnalysisDatabase, dispatcher::RequestDispatcher, document_symbol::document_symbol, + formatting::format_single_file, from_lsp::{self, file_path_from_url, kcl_pos}, goto_def::goto_definition, hover, quick_fix, @@ -47,6 +47,7 @@ impl LanguageServerState { .on::(handle_hover)? .on::(handle_document_symbol)? .on::(handle_code_action)? + .on::(handle_formatting)? .finish(); Ok(()) @@ -67,6 +68,16 @@ impl LanguageServerSnapshot { } } +pub(crate) fn handle_formatting( + _snap: LanguageServerSnapshot, + params: lsp_types::DocumentFormattingParams, + sender: Sender, +) -> anyhow::Result>> { + let file = file_path_from_url(¶ms.text_document.uri)?; + let src = std::fs::read_to_string(file.clone())?; + format_single_file(file, src) +} + /// Called when a `GotoDefinition` request was received. pub(crate) fn handle_code_action( _snap: LanguageServerSnapshot, diff --git a/kclvm/tools/src/LSP/src/tests.rs b/kclvm/tools/src/LSP/src/tests.rs index dc83c909a..e6185a737 100644 --- a/kclvm/tools/src/LSP/src/tests.rs +++ b/kclvm/tools/src/LSP/src/tests.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::env; use std::path::PathBuf; use std::process::Command; @@ -33,6 +32,7 @@ use parking_lot::RwLock; use crate::completion::KCLCompletionItem; use crate::document_symbol::document_symbol; +use crate::formatting::format_single_file; use crate::from_lsp::file_path_from_url; use crate::hover::hover; use crate::quick_fix::quick_fix; @@ -1241,3 +1241,75 @@ fn goto_import_external_file_test() { let res = goto_definition(&program, &pos, &prog_scope); assert!(res.is_some()); } + +#[test] +fn formmat_signle_file_test() { + const FILE_INPUT_SUFFIX: &str = ".input"; + const FILE_OUTPUT_SUFFIX: &str = ".golden"; + const TEST_CASES: &[&str; 17] = &[ + "assert", + "check", + "blankline", + "breakline", + "codelayout", + "collection_if", + "comment", + "comp_for", + // "empty", + "import", + "indent", + "inline_comment", + "lambda", + "quant", + "schema", + "string", + "type_alias", + "unary", + ]; + + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let test_file = path; + let test_dir = test_file + .parent() + .unwrap() + .join("format") + .join("test_data") + .join("format_data"); + for case in TEST_CASES { + let test_file = test_dir + .join(format!("{}{}", case, FILE_INPUT_SUFFIX)) + .to_str() + .unwrap() + .to_string(); + let test_src = std::fs::read_to_string(&test_file).unwrap(); + let got = format_single_file(test_file, test_src).unwrap().unwrap(); + let data_output = std::fs::read_to_string( + &test_dir + .join(format!("{}{}", case, FILE_OUTPUT_SUFFIX)) + .to_str() + .unwrap() + .to_string(), + ) + .unwrap(); + + #[cfg(target_os = "windows")] + let data_output = data_output.replace("\r\n", "\n"); + + let expect = vec![TextEdit { + range: Range::new(Position::new(0, 0), Position::new(u32::MAX, u32::MAX)), + new_text: data_output, + }]; + + assert_eq!(expect, got); + } + + // empty test case, without change after fmt + let test_file = test_dir + .join(format!("{}{}", "empty", FILE_INPUT_SUFFIX)) + .to_str() + .unwrap() + .to_string(); + let test_src = std::fs::read_to_string(&test_file).unwrap(); + let got = format_single_file(test_file, test_src).unwrap(); + assert_eq!(got, None) +}