Detailed changes
@@ -5643,8 +5643,13 @@ version = "0.1.0"
dependencies = [
"editor",
"encoding_rs",
+ "fuzzy",
"gpui",
+ "language",
+ "picker",
+ "project",
"ui",
+ "util",
"workspace",
]
@@ -12661,6 +12666,7 @@ dependencies = [
"dap",
"dap_adapters",
"db",
+ "encoding_rs",
"extension",
"fancy-regex",
"fs",
@@ -618,6 +618,7 @@
"ctrl-?": "agent::ToggleFocus",
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
+ "ctrl-k n": "encoding_selector::Toggle",
"ctrl-k m": "language_selector::Toggle",
"ctrl-k ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
@@ -679,6 +679,7 @@
"cmd-shift-d": "debug_panel::ToggleFocus",
"cmd-?": "agent::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
+ "cmd-k n": "encoding_selector::Toggle",
"cmd-k m": "language_selector::Toggle",
"cmd-k cmd-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
@@ -609,6 +609,7 @@
"ctrl-shift-d": "debug_panel::ToggleFocus",
"ctrl-shift-/": "agent::ToggleFocus",
"ctrl-k s": "workspace::SaveAll",
+ "ctrl-k n": "encoding_selector::Toggle",
"ctrl-k m": "language_selector::Toggle",
"ctrl-m ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
@@ -19,6 +19,7 @@
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle",
+ "ctrl-shift-n": "encoding_selector::Toggle",
"cmd-|": "pane::RevealInProjectPanel",
"cmd-b": "editor::GoToDefinition",
"alt-cmd-b": "editor::GoToDefinitionSplit",
@@ -15,6 +15,11 @@ doctest = false
[dependencies]
editor.workspace = true
encoding_rs.workspace = true
+fuzzy.workspace = true
gpui.workspace = true
+language.workspace = true
+picker.workspace = true
+project.workspace = true
ui.workspace = true
+util.workspace = true
workspace.workspace = true
@@ -1,8 +1,12 @@
+use crate::{EncodingSelector, Toggle};
+
use editor::Editor;
use encoding_rs::{Encoding, UTF_8};
use gpui::{
- Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, Window, div,
+ Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window,
+ div,
};
+use project::Project;
use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip};
use workspace::{
StatusBarSettings, StatusItemView, Workspace,
@@ -11,30 +15,43 @@ use workspace::{
pub struct ActiveBufferEncoding {
active_encoding: Option<&'static Encoding>,
- //workspace: WeakEntity<Workspace>,
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
_observe_active_editor: Option<Subscription>,
has_bom: bool,
+ is_dirty: bool,
+ is_shared: bool,
+ is_via_remote_server: bool,
}
impl ActiveBufferEncoding {
- pub fn new(_workspace: &Workspace) -> Self {
+ pub fn new(workspace: &Workspace) -> Self {
Self {
active_encoding: None,
- //workspace: workspace.weak_handle(),
+ workspace: workspace.weak_handle(),
+ project: workspace.project().clone(),
_observe_active_editor: None,
has_bom: false,
+ is_dirty: false,
+ is_shared: false,
+ is_via_remote_server: false,
}
}
fn update_encoding(&mut self, editor: Entity<Editor>, _: &mut Window, cx: &mut Context<Self>) {
self.active_encoding = None;
+ self.has_bom = false;
+ self.is_dirty = false;
- let editor = editor.read(cx);
- if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
- let buffer = buffer.read(cx);
+ let project = self.project.read(cx);
+ self.is_shared = project.is_shared();
+ self.is_via_remote_server = project.is_via_remote_server();
+ if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) {
+ let buffer = buffer.read(cx);
self.active_encoding = Some(buffer.encoding());
self.has_bom = buffer.has_bom();
+ self.is_dirty = buffer.is_dirty();
}
cx.notify();
@@ -58,13 +75,36 @@ impl Render for ActiveBufferEncoding {
text.push_str(" (BOM)");
}
+ let (disabled, tooltip_text) = if self.is_dirty {
+ (true, "Save file to change encoding")
+ } else if self.is_shared {
+ (true, "Cannot change encoding during collaboration")
+ } else if self.is_via_remote_server {
+ (true, "Cannot change encoding of remote server file")
+ } else {
+ (false, "Reopen with Encoding")
+ };
+
div().child(
Button::new("change-encoding", text)
.label_size(LabelSize::Small)
- .on_click(|_, _, _cx| {
- // No-op
- })
- .tooltip(Tooltip::text("Current Encoding")),
+ .on_click(cx.listener(move |this, _, window, cx| {
+ if disabled {
+ return;
+ }
+ if let Some(workspace) = this.workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ EncodingSelector::toggle(workspace, window, cx)
+ });
+ }
+ }))
+ .tooltip(move |_window, cx| {
+ if disabled {
+ Tooltip::text(tooltip_text)(_window, cx)
+ } else {
+ Tooltip::for_action(tooltip_text, &Toggle, cx)
+ }
+ }),
)
}
}
@@ -83,6 +123,9 @@ impl StatusItemView for ActiveBufferEncoding {
} else {
self.active_encoding = None;
self.has_bom = false;
+ self.is_dirty = false;
+ self.is_shared = false;
+ self.is_via_remote_server = false;
self._observe_active_editor = None;
}
@@ -1,4 +1,327 @@
mod active_buffer_encoding;
pub use active_buffer_encoding::ActiveBufferEncoding;
-pub fn init() {}
+use editor::Editor;
+use encoding_rs::Encoding;
+use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
+use gpui::{
+ App, AppContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+ InteractiveElement, ParentElement, Render, Styled, Task, WeakEntity, Window, actions,
+};
+use language::Buffer;
+use picker::{Picker, PickerDelegate};
+use std::sync::Arc;
+use ui::{HighlightedLabel, ListItem, ListItemSpacing, Toggleable, v_flex};
+use util::ResultExt;
+use workspace::{ModalView, Toast, Workspace, notifications::NotificationId};
+
+actions!(
+ encoding_selector,
+ [
+ /// Toggles the encoding selector modal.
+ Toggle
+ ]
+);
+
+pub fn init(cx: &mut App) {
+ cx.observe_new(EncodingSelector::register).detach();
+}
+
+pub struct EncodingSelector {
+ picker: Entity<Picker<EncodingSelectorDelegate>>,
+}
+
+impl EncodingSelector {
+ fn register(
+ workspace: &mut Workspace,
+ _window: Option<&mut Window>,
+ _: &mut Context<Workspace>,
+ ) {
+ workspace.register_action(move |workspace, _: &Toggle, window, cx| {
+ Self::toggle(workspace, window, cx);
+ });
+ }
+
+ pub fn toggle(
+ workspace: &mut Workspace,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) -> Option<()> {
+ let (_, buffer, _) = workspace
+ .active_item(cx)?
+ .act_as::<Editor>(cx)?
+ .read(cx)
+ .active_excerpt(cx)?;
+
+ let buffer_handle = buffer.read(cx);
+ let project = workspace.project().read(cx);
+
+ if buffer_handle.is_dirty() {
+ workspace.show_toast(
+ Toast::new(
+ NotificationId::unique::<EncodingSelector>(),
+ "Save file to change encoding",
+ ),
+ cx,
+ );
+ return Some(());
+ }
+ if project.is_shared() {
+ workspace.show_toast(
+ Toast::new(
+ NotificationId::unique::<EncodingSelector>(),
+ "Cannot change encoding during collaboration",
+ ),
+ cx,
+ );
+ return Some(());
+ }
+ if project.is_via_remote_server() {
+ workspace.show_toast(
+ Toast::new(
+ NotificationId::unique::<EncodingSelector>(),
+ "Cannot change encoding of remote server file",
+ ),
+ cx,
+ );
+ return Some(());
+ }
+
+ workspace.toggle_modal(window, cx, move |window, cx| {
+ EncodingSelector::new(buffer, window, cx)
+ });
+ Some(())
+ }
+
+ fn new(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+ let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer);
+ let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
+ Self { picker }
+ }
+}
+
+impl Render for EncodingSelector {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
+ v_flex()
+ .key_context("EncodingSelector")
+ .w(gpui::rems(34.))
+ .child(self.picker.clone())
+ }
+}
+
+impl Focusable for EncodingSelector {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<DismissEvent> for EncodingSelector {}
+impl ModalView for EncodingSelector {}
+
+pub struct EncodingSelectorDelegate {
+ encoding_selector: WeakEntity<EncodingSelector>,
+ buffer: Entity<Buffer>,
+ encodings: Vec<&'static Encoding>,
+ match_candidates: Arc<Vec<StringMatchCandidate>>,
+ matches: Vec<StringMatch>,
+ selected_index: usize,
+}
+
+impl EncodingSelectorDelegate {
+ fn new(encoding_selector: WeakEntity<EncodingSelector>, buffer: Entity<Buffer>) -> Self {
+ let encodings = available_encodings();
+ let match_candidates = encodings
+ .iter()
+ .enumerate()
+ .map(|(id, enc)| StringMatchCandidate::new(id, enc.name()))
+ .collect::<Vec<_>>();
+ Self {
+ encoding_selector,
+ buffer,
+ encodings,
+ match_candidates: Arc::new(match_candidates),
+ matches: vec![],
+ selected_index: 0,
+ }
+ }
+
+ fn render_data_for_match(&self, mat: &StringMatch, cx: &App) -> String {
+ let candidate_encoding = self.encodings[mat.candidate_id];
+ let current_encoding = self.buffer.read(cx).encoding();
+
+ if candidate_encoding.name() == current_encoding.name() {
+ format!("{} (current)", candidate_encoding.name())
+ } else {
+ candidate_encoding.name().to_string()
+ }
+ }
+}
+
+fn available_encodings() -> Vec<&'static Encoding> {
+ let mut encodings = vec![
+ // Unicode
+ encoding_rs::UTF_8,
+ encoding_rs::UTF_16LE,
+ encoding_rs::UTF_16BE,
+ // Japanese
+ encoding_rs::SHIFT_JIS,
+ encoding_rs::EUC_JP,
+ encoding_rs::ISO_2022_JP,
+ // Chinese
+ encoding_rs::GBK,
+ encoding_rs::GB18030,
+ encoding_rs::BIG5,
+ // Korean
+ encoding_rs::EUC_KR,
+ // Windows / Single Byte Series
+ encoding_rs::WINDOWS_1252, // Western (ISO-8859-1 unified)
+ encoding_rs::WINDOWS_1250, // Central European
+ encoding_rs::WINDOWS_1251, // Cyrillic
+ encoding_rs::WINDOWS_1253, // Greek
+ encoding_rs::WINDOWS_1254, // Turkish (ISO-8859-9 unified)
+ encoding_rs::WINDOWS_1255, // Hebrew
+ encoding_rs::WINDOWS_1256, // Arabic
+ encoding_rs::WINDOWS_1257, // Baltic
+ encoding_rs::WINDOWS_1258, // Vietnamese
+ encoding_rs::WINDOWS_874, // Thai
+ // ISO-8859 Series (others)
+ encoding_rs::ISO_8859_2,
+ encoding_rs::ISO_8859_3,
+ encoding_rs::ISO_8859_4,
+ encoding_rs::ISO_8859_5,
+ encoding_rs::ISO_8859_6,
+ encoding_rs::ISO_8859_7,
+ encoding_rs::ISO_8859_8,
+ encoding_rs::ISO_8859_8_I, // Logical Hebrew
+ encoding_rs::ISO_8859_10,
+ encoding_rs::ISO_8859_13,
+ encoding_rs::ISO_8859_14,
+ encoding_rs::ISO_8859_15,
+ encoding_rs::ISO_8859_16,
+ // Cyrillic / Legacy Misc
+ encoding_rs::KOI8_R,
+ encoding_rs::KOI8_U,
+ encoding_rs::IBM866,
+ encoding_rs::MACINTOSH,
+ encoding_rs::X_MAC_CYRILLIC,
+ // NOTE: The following encodings are intentionally excluded from the list:
+ //
+ // 1. encoding_rs::REPLACEMENT
+ // Used internally for decoding errors. Not suitable for user selection.
+ //
+ // 2. encoding_rs::X_USER_DEFINED
+ // Used for binary data emulation (legacy web behavior). Not for general text editing.
+ ];
+
+ encodings.sort_by_key(|enc| enc.name());
+
+ encodings
+}
+
+impl PickerDelegate for EncodingSelectorDelegate {
+ type ListItem = ListItem;
+
+ fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+ "Reopen with encoding...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ _window: &mut Window,
+ _: &mut Context<Picker<Self>>,
+ ) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ window: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Task<()> {
+ let background = cx.background_executor().clone();
+ let candidates = self.match_candidates.clone();
+
+ cx.spawn_in(window, async move |this, cx| {
+ let matches = if query.is_empty() {
+ candidates
+ .iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string.clone(),
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ match_strings(
+ &candidates,
+ &query,
+ false,
+ true,
+ 100,
+ &Default::default(),
+ background,
+ )
+ .await
+ };
+
+ this.update(cx, |this, cx| {
+ let delegate = &mut this.delegate;
+ delegate.matches = matches;
+ delegate.selected_index = delegate
+ .selected_index
+ .min(delegate.matches.len().saturating_sub(1));
+ cx.notify();
+ })
+ .log_err();
+ })
+ }
+
+ fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ if let Some(mat) = self.matches.get(self.selected_index) {
+ let selected_encoding = self.encodings[mat.candidate_id];
+
+ self.buffer.update(cx, |buffer, cx| {
+ let _ = buffer.reload_with_encoding(selected_encoding, cx);
+ });
+ }
+ self.dismissed(window, cx);
+ }
+
+ fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+ self.encoding_selector
+ .update(cx, |_, cx| cx.emit(DismissEvent))
+ .log_err();
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let mat = &self.matches.get(ix)?;
+
+ let label = self.render_data_for_match(mat, cx);
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .child(HighlightedLabel::new(label, mat.positions.clone())),
+ )
+ }
+}
@@ -139,6 +139,7 @@ pub struct Buffer {
tree_sitter_data: Arc<TreeSitterData>,
encoding: &'static Encoding,
has_bom: bool,
+ reload_with_encoding_txns: HashMap<TransactionId, (&'static Encoding, bool)>,
}
#[derive(Debug)]
@@ -1147,6 +1148,7 @@ impl Buffer {
_subscriptions: Vec::new(),
encoding: encoding_rs::UTF_8,
has_bom: false,
+ reload_with_encoding_txns: HashMap::default(),
}
}
@@ -1535,31 +1537,86 @@ impl Buffer {
/// Reloads the contents of the buffer from disk.
pub fn reload(&mut self, cx: &Context<Self>) -> oneshot::Receiver<Option<Transaction>> {
+ self.reload_impl(None, cx)
+ }
+
+ /// Reloads the contents of the buffer from disk using the specified encoding.
+ ///
+ /// This bypasses automatic encoding detection heuristics (like BOM checks) for non-Unicode encodings,
+ /// allowing users to force a specific interpretation of the bytes.
+ pub fn reload_with_encoding(
+ &mut self,
+ encoding: &'static Encoding,
+ cx: &Context<Self>,
+ ) -> oneshot::Receiver<Option<Transaction>> {
+ self.reload_impl(Some(encoding), cx)
+ }
+
+ fn reload_impl(
+ &mut self,
+ force_encoding: Option<&'static Encoding>,
+ cx: &Context<Self>,
+ ) -> oneshot::Receiver<Option<Transaction>> {
let (tx, rx) = futures::channel::oneshot::channel();
let prev_version = self.text.version();
+
self.reload_task = Some(cx.spawn(async move |this, cx| {
- let Some((new_mtime, load_bytes_task, encoding)) = this.update(cx, |this, cx| {
- let file = this.file.as_ref()?.as_local()?;
- Some((
- file.disk_state().mtime(),
- file.load_bytes(cx),
- this.encoding,
- ))
- })?
+ let Some((new_mtime, load_bytes_task, current_encoding)) =
+ this.update(cx, |this, cx| {
+ let file = this.file.as_ref()?.as_local()?;
+ Some((
+ file.disk_state().mtime(),
+ file.load_bytes(cx),
+ this.encoding,
+ ))
+ })?
else {
return Ok(());
};
- let bytes = load_bytes_task.await?;
- let (cow, _encoding_used, _has_errors) = encoding.decode(&bytes);
- let new_text = cow.into_owned();
+ let target_encoding = force_encoding.unwrap_or(current_encoding);
+
+ let is_unicode = target_encoding == encoding_rs::UTF_8
+ || target_encoding == encoding_rs::UTF_16LE
+ || target_encoding == encoding_rs::UTF_16BE;
+
+ let (new_text, has_bom, encoding_used) = if force_encoding.is_some() && !is_unicode {
+ let bytes = load_bytes_task.await?;
+ let (cow, _had_errors) = target_encoding.decode_without_bom_handling(&bytes);
+ (cow.into_owned(), false, target_encoding)
+ } else {
+ let bytes = load_bytes_task.await?;
+ let (cow, used_enc, _had_errors) = target_encoding.decode(&bytes);
+
+ let actual_has_bom = if used_enc == encoding_rs::UTF_8 {
+ bytes.starts_with(&[0xEF, 0xBB, 0xBF])
+ } else if used_enc == encoding_rs::UTF_16LE {
+ bytes.starts_with(&[0xFF, 0xFE])
+ } else if used_enc == encoding_rs::UTF_16BE {
+ bytes.starts_with(&[0xFE, 0xFF])
+ } else {
+ false
+ };
+ (cow.into_owned(), actual_has_bom, used_enc)
+ };
let diff = this.update(cx, |this, cx| this.diff(new_text, cx))?.await;
this.update(cx, |this, cx| {
if this.version() == diff.base_version {
this.finalize_last_transaction();
+ let old_encoding = this.encoding;
+ let old_has_bom = this.has_bom;
this.apply_diff(diff, cx);
- tx.send(this.finalize_last_transaction().cloned()).ok();
+ this.encoding = encoding_used;
+ this.has_bom = has_bom;
+ let transaction = this.finalize_last_transaction().cloned();
+ if let Some(ref txn) = transaction {
+ if old_encoding != encoding_used || old_has_bom != has_bom {
+ this.reload_with_encoding_txns
+ .insert(txn.id, (old_encoding, old_has_bom));
+ }
+ }
+ tx.send(transaction).ok();
this.has_conflict = false;
this.did_reload(this.version(), this.line_ending(), new_mtime, cx);
} else {
@@ -3044,6 +3101,7 @@ impl Buffer {
if let Some((transaction_id, operation)) = self.text.undo() {
self.send_operation(Operation::Buffer(operation), true, cx);
self.did_edit(&old_version, was_dirty, cx);
+ self.restore_encoding_for_transaction(transaction_id, was_dirty);
Some(transaction_id)
} else {
None
@@ -3103,12 +3161,31 @@ impl Buffer {
if let Some((transaction_id, operation)) = self.text.redo() {
self.send_operation(Operation::Buffer(operation), true, cx);
self.did_edit(&old_version, was_dirty, cx);
+ self.restore_encoding_for_transaction(transaction_id, was_dirty);
Some(transaction_id)
} else {
None
}
}
+ fn restore_encoding_for_transaction(&mut self, transaction_id: TransactionId, was_dirty: bool) {
+ if let Some((old_encoding, old_has_bom)) =
+ self.reload_with_encoding_txns.get(&transaction_id)
+ {
+ let current_encoding = self.encoding;
+ let current_has_bom = self.has_bom;
+ self.encoding = *old_encoding;
+ self.has_bom = *old_has_bom;
+ if !was_dirty {
+ self.saved_version = self.version.clone();
+ self.has_unsaved_edits
+ .set((self.saved_version.clone(), false));
+ }
+ self.reload_with_encoding_txns
+ .insert(transaction_id, (current_encoding, current_has_bom));
+ }
+ }
+
/// Manually undoes all changes until a given transaction in the buffer's redo history.
pub fn redo_to_transaction(
&mut self,
@@ -104,6 +104,7 @@ tracing.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
+encoding_rs.workspace = true
db = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
context_server = { workspace = true, features = ["test-support"] }
@@ -25,6 +25,7 @@ use buffer_diff::{
assert_hunks,
};
use collections::{BTreeSet, HashMap, HashSet};
+use encoding_rs;
use fs::FakeFs;
use futures::{StreamExt, future};
use git::{
@@ -11113,6 +11114,70 @@ async fn search(
.collect())
}
+#[gpui::test]
+async fn test_undo_encoding_change(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+
+ // Create a file with ASCII content "Hi" - this will be detected as UTF-8
+ // When reinterpreted as UTF-16LE, the bytes 0x48 0x69 become a single character
+ let ascii_bytes: Vec<u8> = vec![0x48, 0x69];
+ fs.insert_tree(path!("/dir"), json!({})).await;
+ fs.insert_file(path!("/dir/test.txt"), ascii_bytes).await;
+
+ let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
+
+ let buffer = project
+ .update(cx, |p, cx| p.open_local_buffer(path!("/dir/test.txt"), cx))
+ .await
+ .unwrap();
+
+ let (initial_encoding, initial_text, initial_dirty) = buffer.read_with(cx, |buffer, _| {
+ (buffer.encoding(), buffer.text(), buffer.is_dirty())
+ });
+ assert_eq!(initial_encoding, encoding_rs::UTF_8);
+ assert_eq!(initial_text, "Hi");
+ assert!(!initial_dirty);
+
+ let reload_receiver = buffer.update(cx, |buffer, cx| {
+ buffer.reload_with_encoding(encoding_rs::UTF_16LE, cx)
+ });
+ cx.executor().run_until_parked();
+
+ // Wait for reload to complete
+ let _ = reload_receiver.await;
+
+ // Verify the encoding changed, text is different, and still not dirty (we reloaded from disk)
+ let (reloaded_encoding, reloaded_text, reloaded_dirty) = buffer.read_with(cx, |buffer, _| {
+ (buffer.encoding(), buffer.text(), buffer.is_dirty())
+ });
+ assert_eq!(reloaded_encoding, encoding_rs::UTF_16LE);
+ assert_eq!(reloaded_text, "ζ₯");
+ assert!(!reloaded_dirty);
+
+ // Undo the reload
+ buffer.update(cx, |buffer, cx| {
+ buffer.undo(cx);
+ });
+
+ buffer.read_with(cx, |buffer, _| {
+ assert_eq!(buffer.encoding(), encoding_rs::UTF_8);
+ assert_eq!(buffer.text(), "Hi");
+ assert!(!buffer.is_dirty());
+ });
+
+ buffer.update(cx, |buffer, cx| {
+ buffer.redo(cx);
+ });
+
+ buffer.read_with(cx, |buffer, _| {
+ assert_eq!(buffer.encoding(), encoding_rs::UTF_16LE);
+ assert_ne!(buffer.text(), "Hi");
+ assert!(!buffer.is_dirty());
+ });
+}
+
pub fn init_test(cx: &mut gpui::TestAppContext) {
zlog::init_test();
@@ -662,6 +662,7 @@ fn main() {
vim::init(cx);
terminal_view::init(cx);
journal::init(app_state.clone(), cx);
+ encoding_selector::init(cx);
language_selector::init(cx);
line_ending_selector::init(cx);
toolchain_selector::init(cx);
@@ -4826,6 +4826,7 @@ mod tests {
"diagnostics",
"edit_prediction",
"editor",
+ "encoding_selector",
"feedback",
"file_finder",
"git",