From 930db6143ef441007124001efbbaefeb3323549f Mon Sep 17 00:00:00 2001 From: Davidyz Date: Thu, 9 Oct 2025 21:32:28 +0800 Subject: [PATCH 1/3] fix: Handle Unicode characters in LSP formatting --- src/cli/lsp.rs | 82 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index 0d516a7a..4137f331 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -17,14 +17,27 @@ use crate::{config::ConfigResolver, opt, stylua_ignore}; fn diffop_to_textedit( op: DiffOp, document: &FullTextDocument, + original_contents: &str, formatted_contents: &str, ) -> Option { - let range = |start: usize, len: usize| Range { - start: document.position_at(start.try_into().expect("usize fits into u32")), - end: document.position_at((start + len).try_into().expect("usize fits into u32")), + let range = |start: usize, len: usize| { + let byte_start = original_contents + .char_indices() + .nth(start) + .map(|(i, _)| i) + .unwrap_or(original_contents.len()); + let byte_end = original_contents + .char_indices() + .nth(start + len) + .map(|(i, _)| i) + .unwrap_or(original_contents.len()); + Range { + start: document.position_at(byte_start.try_into().expect("usize fits into u32")), + end: document.position_at(byte_end.try_into().expect("usize fits into u32")), + } }; - let lookup = |start: usize, len: usize| formatted_contents[start..start + len].to_string(); + let lookup = |start: usize, len: usize| formatted_contents.chars().skip(start).take(len).collect::(); match op { DiffOp::Equal { @@ -181,14 +194,14 @@ impl LanguageServer<'_> { return Err(FormattingError::StyLuaError); }; - let operations = - TextDiff::from_chars(contents.as_bytes(), formatted_contents.as_bytes()).grouped_ops(0); + let operations = TextDiff::from_chars(contents, &formatted_contents).grouped_ops(0); + let edits = operations .into_iter() .flat_map(|operations| { - operations - .into_iter() - .filter_map(|op| diffop_to_textedit(op, document, &formatted_contents)) + operations.into_iter().filter_map(|op| { + diffop_to_textedit(op, document, contents, &formatted_contents) + }) }) .collect(); Ok(edits) @@ -657,6 +670,57 @@ mod tests { assert!(client.receiver.is_empty()); } + #[test] + fn test_lsp_document_formatting_with_unicode() { + let uri = Uri::from_str("file:///home/documents/file.lua").unwrap(); + let contents = "local x = 1 -- 测试\nlocal y =2"; + + let opt = Opt::parse_from(vec!["BINARY_NAME"]); + let mut config_resolver = ConfigResolver::new(&opt).unwrap(); + + let (server, client) = Connection::memory(); + client.sender.send(initialize(1, None)).unwrap(); + client.sender.send(initialized()).unwrap(); + client + .sender + .send(open_text_document(uri.clone(), contents.to_string())) + .unwrap(); + client + .sender + .send(format_document( + 2, + uri.clone(), + FormattingOptions::default(), + )) + .unwrap(); + client.sender.send(shutdown(3)).unwrap(); + client.sender.send(exit()).unwrap(); + + main_loop(server, false, &mut config_resolver).unwrap(); + + expect_server_initialized(&client.receiver, 1); + + let edits: Vec = expect_response(&client.receiver, 2); + assert_eq!( + edits, + [ + TextEdit { range: Range { start: Position { line: 0, character: 6 }, end: Position { line: 0, character: 7 } }, new_text: "".to_string() }, + TextEdit { range: Range { start: Position { line: 0, character: 8 }, end: Position { line: 0, character: 9 } }, new_text: "".to_string() }, + TextEdit { range: Range { start: Position { line: 0, character: 11 }, end: Position { line: 0, character: 12 } }, new_text: "".to_string() }, + TextEdit { range: Range { start: Position { line: 1, character: 5 }, end: Position { line: 1, character: 7 } }, new_text: "".to_string() }, + TextEdit { range: Range { start: Position { line: 1, character: 8 }, end: Position { line: 1, character: 9 } }, new_text: "".to_string() }, + TextEdit { range: Range { start: Position { line: 1, character: 10 }, end: Position { line: 1, character: 12 } }, new_text: "".to_string() }, + TextEdit { range: Range { start: Position { line: 1, character: 14 }, end: Position { line: 1, character: 14 } }, new_text: " ".to_string() }, + TextEdit { range: Range { start: Position { line: 1, character: 15 }, end: Position { line: 1, character: 15 } }, new_text: "\n".to_string() } + ] + ); + let formatted = apply_text_edits_to(contents, edits); + assert_eq!(formatted, "local x = 1 -- 测试\nlocal y = 2\n"); + + expect_server_shutdown(&client.receiver, 3); + assert!(client.receiver.is_empty()); + } + #[test] fn test_lsp_range_formatting() { let uri = Uri::from_str("file:///home/documents/file.luau").unwrap(); From 92738114e88eb7db6a2978e3fede96bd06111554 Mon Sep 17 00:00:00 2001 From: Davidyz Date: Sat, 11 Oct 2025 13:37:00 +0800 Subject: [PATCH 2/3] chore: cargo fmt --- src/cli/lsp.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 9 deletions(-) diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index 4137f331..675fadb2 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -37,7 +37,13 @@ fn diffop_to_textedit( } }; - let lookup = |start: usize, len: usize| formatted_contents.chars().skip(start).take(len).collect::(); + let lookup = |start: usize, len: usize| { + formatted_contents + .chars() + .skip(start) + .take(len) + .collect::() + }; match op { DiffOp::Equal { @@ -704,14 +710,110 @@ mod tests { assert_eq!( edits, [ - TextEdit { range: Range { start: Position { line: 0, character: 6 }, end: Position { line: 0, character: 7 } }, new_text: "".to_string() }, - TextEdit { range: Range { start: Position { line: 0, character: 8 }, end: Position { line: 0, character: 9 } }, new_text: "".to_string() }, - TextEdit { range: Range { start: Position { line: 0, character: 11 }, end: Position { line: 0, character: 12 } }, new_text: "".to_string() }, - TextEdit { range: Range { start: Position { line: 1, character: 5 }, end: Position { line: 1, character: 7 } }, new_text: "".to_string() }, - TextEdit { range: Range { start: Position { line: 1, character: 8 }, end: Position { line: 1, character: 9 } }, new_text: "".to_string() }, - TextEdit { range: Range { start: Position { line: 1, character: 10 }, end: Position { line: 1, character: 12 } }, new_text: "".to_string() }, - TextEdit { range: Range { start: Position { line: 1, character: 14 }, end: Position { line: 1, character: 14 } }, new_text: " ".to_string() }, - TextEdit { range: Range { start: Position { line: 1, character: 15 }, end: Position { line: 1, character: 15 } }, new_text: "\n".to_string() } + TextEdit { + range: Range { + start: Position { + line: 0, + character: 6 + }, + end: Position { + line: 0, + character: 7 + } + }, + new_text: "".to_string() + }, + TextEdit { + range: Range { + start: Position { + line: 0, + character: 8 + }, + end: Position { + line: 0, + character: 9 + } + }, + new_text: "".to_string() + }, + TextEdit { + range: Range { + start: Position { + line: 0, + character: 11 + }, + end: Position { + line: 0, + character: 12 + } + }, + new_text: "".to_string() + }, + TextEdit { + range: Range { + start: Position { + line: 1, + character: 5 + }, + end: Position { + line: 1, + character: 7 + } + }, + new_text: "".to_string() + }, + TextEdit { + range: Range { + start: Position { + line: 1, + character: 8 + }, + end: Position { + line: 1, + character: 9 + } + }, + new_text: "".to_string() + }, + TextEdit { + range: Range { + start: Position { + line: 1, + character: 10 + }, + end: Position { + line: 1, + character: 12 + } + }, + new_text: "".to_string() + }, + TextEdit { + range: Range { + start: Position { + line: 1, + character: 14 + }, + end: Position { + line: 1, + character: 14 + } + }, + new_text: " ".to_string() + }, + TextEdit { + range: Range { + start: Position { + line: 1, + character: 15 + }, + end: Position { + line: 1, + character: 15 + } + }, + new_text: "\n".to_string() + } ] ); let formatted = apply_text_edits_to(contents, edits); From ddd228e34624720a1f90fd8bfeeb71088fad603e Mon Sep 17 00:00:00 2001 From: Davidyz Date: Sat, 11 Oct 2025 13:38:44 +0800 Subject: [PATCH 3/3] docs: update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e774d8c..05f91c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Use character-wised diff instead of byte-wise diff in the LSP server so that it can handle multi-byte characters ([#1042](https://github.com/JohnnyMorganz/StyLua/issues/1042), [#1043](https://github.com/JohnnyMorganz/StyLua/issues/1043)). + ## [2.3.0] - 2025-09-27 ### Added