Skip to content

Commit 4ea606c

Browse files
authored
fix: Handle Unicode characters in LSP formatting (#1044)
* fix: Handle Unicode characters in LSP formatting * chore: cargo fmt * docs: update CHANGELOG
1 parent a456a37 commit 4ea606c

File tree

2 files changed

+179
-9
lines changed

2 files changed

+179
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- 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)).
13+
1014
## [2.3.0] - 2025-09-27
1115

1216
### Added

src/cli/lsp.rs

Lines changed: 175 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,33 @@ use crate::{config::ConfigResolver, opt, stylua_ignore};
1717
fn diffop_to_textedit(
1818
op: DiffOp,
1919
document: &FullTextDocument,
20+
original_contents: &str,
2021
formatted_contents: &str,
2122
) -> Option<TextEdit> {
22-
let range = |start: usize, len: usize| Range {
23-
start: document.position_at(start.try_into().expect("usize fits into u32")),
24-
end: document.position_at((start + len).try_into().expect("usize fits into u32")),
23+
let range = |start: usize, len: usize| {
24+
let byte_start = original_contents
25+
.char_indices()
26+
.nth(start)
27+
.map(|(i, _)| i)
28+
.unwrap_or(original_contents.len());
29+
let byte_end = original_contents
30+
.char_indices()
31+
.nth(start + len)
32+
.map(|(i, _)| i)
33+
.unwrap_or(original_contents.len());
34+
Range {
35+
start: document.position_at(byte_start.try_into().expect("usize fits into u32")),
36+
end: document.position_at(byte_end.try_into().expect("usize fits into u32")),
37+
}
2538
};
2639

27-
let lookup = |start: usize, len: usize| formatted_contents[start..start + len].to_string();
40+
let lookup = |start: usize, len: usize| {
41+
formatted_contents
42+
.chars()
43+
.skip(start)
44+
.take(len)
45+
.collect::<String>()
46+
};
2847

2948
match op {
3049
DiffOp::Equal {
@@ -181,14 +200,14 @@ impl LanguageServer<'_> {
181200
return Err(FormattingError::StyLuaError);
182201
};
183202

184-
let operations =
185-
TextDiff::from_chars(contents.as_bytes(), formatted_contents.as_bytes()).grouped_ops(0);
203+
let operations = TextDiff::from_chars(contents, &formatted_contents).grouped_ops(0);
204+
186205
let edits = operations
187206
.into_iter()
188207
.flat_map(|operations| {
189-
operations
190-
.into_iter()
191-
.filter_map(|op| diffop_to_textedit(op, document, &formatted_contents))
208+
operations.into_iter().filter_map(|op| {
209+
diffop_to_textedit(op, document, contents, &formatted_contents)
210+
})
192211
})
193212
.collect();
194213
Ok(edits)
@@ -657,6 +676,153 @@ mod tests {
657676
assert!(client.receiver.is_empty());
658677
}
659678

679+
#[test]
680+
fn test_lsp_document_formatting_with_unicode() {
681+
let uri = Uri::from_str("file:///home/documents/file.lua").unwrap();
682+
let contents = "local x = 1 -- 测试\nlocal y =2";
683+
684+
let opt = Opt::parse_from(vec!["BINARY_NAME"]);
685+
let mut config_resolver = ConfigResolver::new(&opt).unwrap();
686+
687+
let (server, client) = Connection::memory();
688+
client.sender.send(initialize(1, None)).unwrap();
689+
client.sender.send(initialized()).unwrap();
690+
client
691+
.sender
692+
.send(open_text_document(uri.clone(), contents.to_string()))
693+
.unwrap();
694+
client
695+
.sender
696+
.send(format_document(
697+
2,
698+
uri.clone(),
699+
FormattingOptions::default(),
700+
))
701+
.unwrap();
702+
client.sender.send(shutdown(3)).unwrap();
703+
client.sender.send(exit()).unwrap();
704+
705+
main_loop(server, false, &mut config_resolver).unwrap();
706+
707+
expect_server_initialized(&client.receiver, 1);
708+
709+
let edits: Vec<TextEdit> = expect_response(&client.receiver, 2);
710+
assert_eq!(
711+
edits,
712+
[
713+
TextEdit {
714+
range: Range {
715+
start: Position {
716+
line: 0,
717+
character: 6
718+
},
719+
end: Position {
720+
line: 0,
721+
character: 7
722+
}
723+
},
724+
new_text: "".to_string()
725+
},
726+
TextEdit {
727+
range: Range {
728+
start: Position {
729+
line: 0,
730+
character: 8
731+
},
732+
end: Position {
733+
line: 0,
734+
character: 9
735+
}
736+
},
737+
new_text: "".to_string()
738+
},
739+
TextEdit {
740+
range: Range {
741+
start: Position {
742+
line: 0,
743+
character: 11
744+
},
745+
end: Position {
746+
line: 0,
747+
character: 12
748+
}
749+
},
750+
new_text: "".to_string()
751+
},
752+
TextEdit {
753+
range: Range {
754+
start: Position {
755+
line: 1,
756+
character: 5
757+
},
758+
end: Position {
759+
line: 1,
760+
character: 7
761+
}
762+
},
763+
new_text: "".to_string()
764+
},
765+
TextEdit {
766+
range: Range {
767+
start: Position {
768+
line: 1,
769+
character: 8
770+
},
771+
end: Position {
772+
line: 1,
773+
character: 9
774+
}
775+
},
776+
new_text: "".to_string()
777+
},
778+
TextEdit {
779+
range: Range {
780+
start: Position {
781+
line: 1,
782+
character: 10
783+
},
784+
end: Position {
785+
line: 1,
786+
character: 12
787+
}
788+
},
789+
new_text: "".to_string()
790+
},
791+
TextEdit {
792+
range: Range {
793+
start: Position {
794+
line: 1,
795+
character: 14
796+
},
797+
end: Position {
798+
line: 1,
799+
character: 14
800+
}
801+
},
802+
new_text: " ".to_string()
803+
},
804+
TextEdit {
805+
range: Range {
806+
start: Position {
807+
line: 1,
808+
character: 15
809+
},
810+
end: Position {
811+
line: 1,
812+
character: 15
813+
}
814+
},
815+
new_text: "\n".to_string()
816+
}
817+
]
818+
);
819+
let formatted = apply_text_edits_to(contents, edits);
820+
assert_eq!(formatted, "local x = 1 -- 测试\nlocal y = 2\n");
821+
822+
expect_server_shutdown(&client.receiver, 3);
823+
assert!(client.receiver.is_empty());
824+
}
825+
660826
#[test]
661827
fn test_lsp_range_formatting() {
662828
let uri = Uri::from_str("file:///home/documents/file.luau").unwrap();

0 commit comments

Comments
 (0)