Skip to content
This repository was archived by the owner on Dec 29, 2022. It is now read-only.

Commit 0e7432a

Browse files
authored
Merge pull request #1112 from Xanewok/fix-format-range
Fix crashes when editing wide characters spanning 2 `u16`s (UTF-16)
2 parents 8ddec63 + 1ff3a38 commit 0e7432a

File tree

6 files changed

+141
-20
lines changed

6 files changed

+141
-20
lines changed

Cargo.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ rls-blacklist = "0.1.2"
3030
rls-data = { version = "0.18.1", features = ["serialize-serde", "serialize-rustc"] }
3131
rls-rustc = "0.5.0"
3232
rls-span = { version = "0.4", features = ["serialize-serde"] }
33-
rls-vfs = "0.6"
33+
rls-vfs = "0.7"
3434
rustc_tools_util = { git = "https://github.com/rust-lang-nursery/rust-clippy", rev = "d8b426901a75b1eb975f52b4537f2736f2b94436" }
3535
rustfmt-nightly = "0.99.6"
3636
rustc-serialize = "0.3"

src/actions/notifications.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::actions::{FileWatch, InitActionContext, VersionOrdering};
1414
use crate::config::Config;
1515
use crate::Span;
1616
use log::{debug, trace, warn};
17-
use rls_vfs::Change;
17+
use rls_vfs::{Change, VfsSpan};
1818
use serde::de::Error;
1919
use serde::Deserialize;
2020
use serde_json;
@@ -113,8 +113,11 @@ impl BlockingNotificationAction for DidChangeTextDocument {
113113
if let Some(range) = i.range {
114114
let range = ls_util::range_to_rls(range);
115115
Change::ReplaceText {
116-
span: Span::from_range(range, file_path.clone()),
117-
len: i.range_length,
116+
// LSP sends UTF-16 code units based offsets and length
117+
span: VfsSpan::from_utf16(
118+
Span::from_range(range, file_path.clone()),
119+
i.range_length
120+
),
118121
text: i.text.clone(),
119122
}
120123
} else {

src/actions/requests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ fn reformat(
746746
}
747747
};
748748

749-
let range_whole_file = ls_util::range_from_vfs_file(&ctx.vfs, &path);
749+
let range_whole_file = ls_util::range_from_file_string(&input);
750750
let mut config = ctx.fmt_config().get_rustfmt_config().clone();
751751
if !config.was_set().hard_tabs() {
752752
config.set().hard_tabs(!opts.insert_spaces);

src/lsp_data.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ use languageserver_types as ls_types;
1818
use racer;
1919
use rls_analysis::DefKind;
2020
use rls_span as span;
21-
use rls_vfs::FileContents;
2221
use serde_derive::{Deserialize, Serialize};
2322
use url::Url;
2423

@@ -87,10 +86,9 @@ pub mod ls_util {
8786
use super::*;
8887
use crate::Span;
8988

90-
use rls_vfs::Vfs;
91-
use std::path::Path;
92-
9389
/// Convert a language server protocol range into an RLS range.
90+
/// NOTE: This does not translate LSP UTF-16 code units offsets into Unicode
91+
/// Scalar Value offsets as expected by RLS/Rust.
9492
pub fn range_to_rls(r: Range) -> span::Range<span::ZeroIndexed> {
9593
span::Range::from_positions(position_to_rls(r.start), position_to_rls(r.end))
9694
}
@@ -146,13 +144,9 @@ pub mod ls_util {
146144
/// Creates a `Range` spanning the whole file as currently known by `Vfs`
147145
///
148146
/// Panics if `Vfs` cannot load the file.
149-
pub fn range_from_vfs_file(vfs: &Vfs, fname: &Path) -> Range {
150-
// FIXME load_file clones the entire file text, this could be much more
151-
// efficient by adding a `with_file` fn to the VFS.
152-
let content = match vfs.load_file(fname).unwrap() {
153-
FileContents::Text(t) => t,
154-
_ => panic!("unexpected binary file: {:?}", fname),
155-
};
147+
pub fn range_from_file_string(content: impl AsRef<str>) -> Range {
148+
let content = content.as_ref();
149+
156150
if content.is_empty() {
157151
Range {
158152
start: Position::new(0, 0),
@@ -169,7 +163,9 @@ pub mod ls_util {
169163
.last()
170164
.expect("String is not empty.")
171165
.chars()
172-
.count() as u64
166+
// LSP uses UTF-16 code units offset
167+
.map(|chr| chr.len_utf16() as u64)
168+
.sum()
173169
};
174170
// range is zero-based and the end position is exclusive
175171
Range {

tests/tests.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,3 +1033,124 @@ fn cmd_invalid_member_dependency_resolution() {
10331033

10341034
rls.shutdown(rls_timeout());
10351035
}
1036+
1037+
#[test]
1038+
fn cmd_handle_utf16_unit_text_edits() {
1039+
let project = project("cmd_handle_utf16_unit_text_edits")
1040+
.file(
1041+
"Cargo.toml",
1042+
r#"[package]
1043+
name = "cmd_handle_utf16_unit_text_edits"
1044+
version = "0.1.0"
1045+
authors = ["[email protected]"]
1046+
"#,
1047+
)
1048+
.file("src/main.rs", "fn main() {}")
1049+
.file("src/unrelated.rs", "😢")
1050+
.build();
1051+
let root_path = project.root();
1052+
let mut rls = project.spawn_rls();
1053+
1054+
rls.request(
1055+
0,
1056+
"initialize",
1057+
Some(json!({
1058+
"rootPath": root_path,
1059+
"capabilities": {}
1060+
})),
1061+
)
1062+
.unwrap();
1063+
1064+
rls.wait_until_done_indexing(rls_timeout());
1065+
1066+
rls.notify(
1067+
"textDocument/didChange",
1068+
Some(json!(
1069+
{"textDocument": {
1070+
"uri": format!("file://{}/src/unrelated.rs", root_path.as_path().display()),
1071+
"version": 1
1072+
},
1073+
// "😢" -> ""
1074+
"contentChanges": [
1075+
{
1076+
"range": {
1077+
"start": {
1078+
"line":0,
1079+
"character":0
1080+
},
1081+
"end":{
1082+
"line":0,
1083+
"character":2
1084+
}
1085+
},
1086+
"rangeLength":2,
1087+
"text":""
1088+
}
1089+
]
1090+
}))
1091+
).unwrap();
1092+
1093+
rls.shutdown(rls_timeout());
1094+
}
1095+
1096+
/// Ensures that wide characters do not prevent RLS from calculating correct
1097+
/// 'whole file' LSP range.
1098+
#[test]
1099+
fn cmd_format_utf16_range() {
1100+
let project = project("cmd_format_utf16_range")
1101+
.file(
1102+
"Cargo.toml",
1103+
r#"[package]
1104+
name = "cmd_format_utf16_range"
1105+
version = "0.1.0"
1106+
authors = ["[email protected]"]
1107+
"#,
1108+
)
1109+
.file("src/main.rs", "/* 😢😢😢😢😢😢😢 */ fn main() { }")
1110+
.build();
1111+
let root_path = project.root();
1112+
let mut rls = project.spawn_rls();
1113+
1114+
rls.request(
1115+
0,
1116+
"initialize",
1117+
Some(json!({
1118+
"rootPath": root_path,
1119+
"capabilities": {}
1120+
})),
1121+
)
1122+
.unwrap();
1123+
1124+
rls.wait_until_done_indexing(rls_timeout());
1125+
1126+
let request_id = 66;
1127+
rls.request(
1128+
request_id,
1129+
"textDocument/formatting",
1130+
Some(json!(
1131+
{
1132+
"textDocument": {
1133+
"uri": format!("file://{}/src/main.rs", root_path.as_path().display()),
1134+
"version": 1
1135+
},
1136+
"options": {
1137+
"tabSize": 4,
1138+
"insertSpaces": true
1139+
}
1140+
}))
1141+
).unwrap();
1142+
1143+
let json = rls.wait_until_json_id(request_id, rls_timeout());
1144+
eprintln!("{:#?}", json);
1145+
1146+
let result = json["result"].as_array().unwrap();
1147+
let new_text: Vec<_> = result
1148+
.into_iter()
1149+
.map(|o| o["newText"].as_str().unwrap())
1150+
.collect();
1151+
// Actual formatting isn't important - what is, is that the buffer isn't
1152+
// malformed and code stays semantically equivalent.
1153+
assert_eq!(new_text, vec!["/* 😢😢😢😢😢😢😢 */\nfn main() {}\n"]);
1154+
1155+
rls.shutdown(rls_timeout());
1156+
}

0 commit comments

Comments
 (0)