mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
fix(editor): keep a chosen file encoding when the file changes on disk (#2792)
Reloads after an external change (`reload_from_disk`) and after undoing a workspace edit (`reload_resource_buffers`) always re-ran encoding detection, so a file the user had reloaded or converted to a legacy encoding came back as mojibake and later saves used the guessed encoding. Documents now record that the encoding was chosen and both reload paths decode with it; files whose encoding was never chosen are still detected again. Co-authored-by: rohitdanda <10144914+rohitdanda@users.noreply.github.com>
This commit is contained in:
co-authored by
rohitdanda
parent
cb5e7081fa
commit
b4bcd8cce7
@@ -149,6 +149,8 @@ struct DocState {
|
||||
binary: bool,
|
||||
dirty: bool,
|
||||
can_reload_encoding: bool,
|
||||
/// The user picked `encoding`; reloads keep it instead of detecting again.
|
||||
encoding_chosen: bool,
|
||||
/// Disk state we last loaded or wrote; changes matching it are our own.
|
||||
disk_mtime: f64,
|
||||
disk_len: u64,
|
||||
@@ -420,6 +422,7 @@ impl Documents {
|
||||
binary: loaded.binary,
|
||||
dirty: false,
|
||||
can_reload_encoding: true,
|
||||
encoding_chosen: false,
|
||||
disk_mtime: loaded.mtime_ms,
|
||||
disk_len: disk_len(path),
|
||||
closed: false,
|
||||
@@ -491,6 +494,7 @@ impl Documents {
|
||||
binary: false,
|
||||
dirty: false,
|
||||
can_reload_encoding,
|
||||
encoding_chosen: false,
|
||||
disk_mtime: 0.0,
|
||||
disk_len: 0,
|
||||
closed: false,
|
||||
@@ -866,6 +870,7 @@ impl Documents {
|
||||
let loaded = Self::load_as(&st.path, encoding, enabled)?;
|
||||
st.native_to_ascii = enabled;
|
||||
st.encoding = loaded.encoding.clone();
|
||||
st.encoding_chosen = true;
|
||||
st.has_bom = loaded.has_bom;
|
||||
st.disk_mtime = loaded.mtime_ms;
|
||||
st.disk_len = disk_len(&st.path);
|
||||
@@ -884,6 +889,7 @@ impl Documents {
|
||||
ide_vfs::encode(&st.doc.text_for_save(), encoding, has_bom)?;
|
||||
}
|
||||
st.encoding = encoding.to_owned();
|
||||
st.encoding_chosen = true;
|
||||
st.has_bom = has_bom;
|
||||
self.set_dirty(doc, st, true);
|
||||
Ok(DocumentEncoding { encoding: st.encoding.clone(), has_bom: st.has_bom })
|
||||
@@ -1020,8 +1026,13 @@ impl Documents {
|
||||
Self::load_as(&st.path, &st.encoding, st.native_to_ascii)?
|
||||
} else {
|
||||
let defaults = self.file_defaults();
|
||||
let loaded = Self::load_with_defaults(&st.path, &defaults)?;
|
||||
st.native_to_ascii = defaults.native_to_ascii_for(&st.path);
|
||||
let native_to_ascii = defaults.native_to_ascii_for(&st.path);
|
||||
let loaded = if st.encoding_chosen {
|
||||
Self::load_as(&st.path, &st.encoding, native_to_ascii)?
|
||||
} else {
|
||||
Self::load_with_defaults(&st.path, &defaults)?
|
||||
};
|
||||
st.native_to_ascii = native_to_ascii;
|
||||
loaded
|
||||
};
|
||||
st.disk_mtime = loaded.mtime_ms;
|
||||
|
||||
@@ -748,6 +748,7 @@ impl Documents {
|
||||
let result = self.with_doc(*id, |state| {
|
||||
self.replace_locked(*id, state, &before.doc.text())?;
|
||||
state.encoding = before.encoding.clone();
|
||||
state.encoding_chosen = before.encoding_chosen;
|
||||
state.has_bom = before.has_bom;
|
||||
state.doc.set_line_ending(before.doc.line_ending());
|
||||
state.disk_mtime = fs::metadata(&state.path).map(|m| ide_vfs::mtime_ms(&m)).unwrap_or(0.0);
|
||||
@@ -789,7 +790,11 @@ impl Documents {
|
||||
return Err(stale(path));
|
||||
}
|
||||
let defaults = self.file_defaults();
|
||||
let loaded = ide_vfs::load_text_with_fallback(path, defaults.fallback_for(path))?;
|
||||
let loaded = if state.encoding_chosen {
|
||||
Self::load_as(path, &state.encoding, state.native_to_ascii)?
|
||||
} else {
|
||||
ide_vfs::load_text_with_fallback(path, defaults.fallback_for(path))?
|
||||
};
|
||||
self.replace_locked(id, state, &loaded.text)?;
|
||||
state.encoding = loaded.encoding;
|
||||
state.has_bom = loaded.has_bom;
|
||||
|
||||
@@ -505,6 +505,48 @@ fn reload_and_convert_encoding() {
|
||||
assert!(f.docs.set_encoding(info.id, "gbk", true).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_changes_keep_a_chosen_encoding() {
|
||||
// Valid UTF-8 bytes, so detection alone would read them as UTF-8.
|
||||
let (first, changed) = (b"caf\xC3\xA9".as_slice(), b"caf\xC3\xA9 \xC3\xA8".as_slice());
|
||||
for (encoding, reload) in [("windows-1252", true), ("gbk", true), ("windows-1252", false)] {
|
||||
let f = fixture();
|
||||
let path = f.root.join("legacy.txt");
|
||||
fs::write(&path, first).unwrap();
|
||||
let info = f.docs.open(&path).unwrap();
|
||||
assert_eq!(info.encoding, "utf-8");
|
||||
if reload {
|
||||
f.docs.set_encoding(info.id, encoding, true).unwrap();
|
||||
} else {
|
||||
// Convert, then save: the file is now in the chosen encoding.
|
||||
f.docs.replace_all(info.id, "café").unwrap();
|
||||
f.docs.set_encoding(info.id, encoding, false).unwrap();
|
||||
f.docs.save(info.id).unwrap();
|
||||
}
|
||||
let expected =
|
||||
|bytes: &[u8]| encoding_rs::Encoding::for_label(encoding.as_bytes()).unwrap().decode(bytes).0.into_owned();
|
||||
let changed = if reload { changed.to_vec() } else { ide_vfs::encode("café è", encoding, false).unwrap() };
|
||||
fs::write(&path, &changed).unwrap();
|
||||
f.docs.on_disk_changed(std::slice::from_ref(&path));
|
||||
let snapshot = f.docs.snapshot(info.id).unwrap();
|
||||
assert_eq!(snapshot.text, expected(&changed), "{encoding} reload={reload}");
|
||||
assert_eq!(snapshot.encoding, encoding, "{encoding} reload={reload}");
|
||||
assert!(!snapshot.dirty);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_changes_redetect_an_encoding_the_user_never_chose() {
|
||||
let f = fixture();
|
||||
let path = f.root.join("plain.txt");
|
||||
fs::write(&path, "plain").unwrap();
|
||||
let info = f.docs.open(&path).unwrap();
|
||||
fs::write(&path, ide_vfs::encode("wide", "utf-16le", true).unwrap()).unwrap();
|
||||
f.docs.on_disk_changed(std::slice::from_ref(&path));
|
||||
let snapshot = f.docs.snapshot(info.id).unwrap();
|
||||
assert_eq!((snapshot.text.as_str(), snapshot.encoding.as_str()), ("wide", "utf-16le"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_bom_is_added_and_removed_on_save() {
|
||||
let f = fixture();
|
||||
|
||||
@@ -162,6 +162,21 @@ fn undo_id(events: &Mutex<Vec<IdeEvent>>) -> u32 {
|
||||
.expect("successful transaction has one undo record")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_keeps_the_encoding_chosen_for_a_file_opened_after_the_edit() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let path = tmp.path().join("legacy.txt");
|
||||
std::fs::write(&path, b"abc\xC3\xA9").unwrap();
|
||||
let (docs, events) = recorded_documents();
|
||||
docs.apply_workspace_edit(&WorkspaceEdit { files: vec![edit(&path, "new")], ..Default::default() }).unwrap();
|
||||
let info = docs.open(&path).unwrap();
|
||||
docs.set_encoding(info.id, "windows-1252", true).unwrap();
|
||||
assert_eq!(docs.snapshot(info.id).unwrap().text, "newé");
|
||||
docs.undo_workspace_edit(undo_id(&events)).unwrap();
|
||||
let snapshot = docs.snapshot(info.id).unwrap();
|
||||
assert_eq!((snapshot.text.as_str(), snapshot.encoding.as_str()), ("abcé", "windows-1252"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_document_version_rejects_the_whole_edit() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user