1//! A crate for handling file encodings in the text editor.
2
3use crate::selectors::encoding::Action;
4use editor::Editor;
5use encoding_rs::Encoding;
6use gpui::{ClickEvent, Entity, Subscription, WeakEntity};
7use language::Buffer;
8use ui::{App, Button, ButtonCommon, Context, LabelSize, Render, Tooltip, Window, div};
9use ui::{Clickable, ParentElement};
10use util::ResultExt;
11use workspace::{
12 CloseActiveItem, ItemHandle, OpenOptions, StatusItemView, Workspace,
13 with_active_or_new_workspace,
14};
15use zed_actions::encodings_ui::{ForceOpen, Toggle};
16
17use crate::selectors::encoding::EncodingSelector;
18use crate::selectors::save_or_reopen::EncodingSaveOrReopenSelector;
19
20/// A status bar item that shows the current file encoding and allows changing it.
21pub struct EncodingIndicator {
22 pub encoding: Option<&'static Encoding>,
23 pub workspace: WeakEntity<Workspace>,
24
25 /// Subscription to observe changes in the active editor
26 observe_editor: Option<Subscription>,
27
28 /// Subscription to observe changes in the `encoding` field of the `Buffer` struct
29 observe_buffer_encoding: Option<Subscription>,
30
31 /// Whether to show the indicator or not, based on whether an editor is active
32 show: bool,
33
34 /// Whether to show `EncodingSaveOrReopenSelector`. It will be shown only when
35 /// the current buffer is associated with a file.
36 show_save_or_reopen_selector: bool,
37}
38
39pub mod selectors;
40
41impl Render for EncodingIndicator {
42 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
43 let status_element = div();
44 let show_save_or_reopen_selector = self.show_save_or_reopen_selector;
45
46 if !self.show {
47 return status_element;
48 }
49
50 status_element.child(
51 Button::new(
52 "encoding",
53 encoding_name(self.encoding.unwrap_or(encoding_rs::UTF_8)),
54 )
55 .label_size(LabelSize::Small)
56 .tooltip(Tooltip::text("Select Encoding"))
57 .on_click(cx.listener(move |indicator, _: &ClickEvent, window, cx| {
58 if let Some(workspace) = indicator.workspace.upgrade() {
59 workspace.update(cx, move |workspace, cx| {
60 // Open the `EncodingSaveOrReopenSelector` if the buffer is associated with a file,
61 if show_save_or_reopen_selector {
62 EncodingSaveOrReopenSelector::toggle(workspace, window, cx)
63 }
64 // otherwise, open the `EncodingSelector` directly.
65 else {
66 let (_, buffer, _) = workspace
67 .active_item(cx)
68 .unwrap()
69 .act_as::<Editor>(cx)
70 .unwrap()
71 .read(cx)
72 .active_excerpt(cx)
73 .unwrap();
74
75 let weak_workspace = workspace.weak_handle();
76
77 if let Some(path) = buffer.read(cx).file() {
78 let path = path.clone().path().to_rel_path_buf();
79 workspace.toggle_modal(window, cx, |window, cx| {
80 let selector = EncodingSelector::new(
81 window,
82 cx,
83 Action::Save,
84 Some(buffer.downgrade()),
85 weak_workspace,
86 Some(path.as_std_path().to_path_buf()),
87 );
88 selector
89 });
90 }
91 }
92 })
93 }
94 })),
95 )
96 }
97}
98
99impl EncodingIndicator {
100 pub fn new(
101 encoding: Option<&'static Encoding>,
102 workspace: WeakEntity<Workspace>,
103 observe_editor: Option<Subscription>,
104 observe_buffer_encoding: Option<Subscription>,
105 ) -> EncodingIndicator {
106 EncodingIndicator {
107 encoding,
108 workspace,
109 observe_editor,
110 show: false,
111 observe_buffer_encoding,
112 show_save_or_reopen_selector: false,
113 }
114 }
115
116 /// Update the encoding when the active editor is switched.
117 pub fn update_when_editor_is_switched(
118 &mut self,
119 editor: Entity<Editor>,
120 _: &mut Window,
121 cx: &mut Context<EncodingIndicator>,
122 ) {
123 let editor = editor.read(cx);
124 if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
125 let encoding = buffer.read(cx).encoding.clone();
126 self.encoding = Some(encoding.get());
127
128 if let Some(_) = buffer.read(cx).file() {
129 self.show_save_or_reopen_selector = true;
130 } else {
131 self.show_save_or_reopen_selector = false;
132 }
133 }
134
135 cx.notify();
136 }
137
138 /// Update the encoding when the `encoding` field of the `Buffer` struct changes.
139 pub fn update_when_buffer_encoding_changes(
140 &mut self,
141 buffer: Entity<Buffer>,
142 _: &mut Window,
143 cx: &mut Context<EncodingIndicator>,
144 ) {
145 let encoding = buffer.read(cx).encoding.clone();
146 self.encoding = Some(encoding.get());
147 cx.notify();
148 }
149}
150
151impl StatusItemView for EncodingIndicator {
152 fn set_active_pane_item(
153 &mut self,
154 active_pane_item: Option<&dyn ItemHandle>,
155 window: &mut Window,
156 cx: &mut Context<Self>,
157 ) {
158 match active_pane_item.and_then(|item| item.downcast::<Editor>()) {
159 Some(editor) => {
160 self.observe_editor =
161 Some(cx.observe_in(&editor, window, Self::update_when_editor_is_switched));
162 if let Some((_, buffer, _)) = &editor.read(cx).active_excerpt(cx) {
163 self.observe_buffer_encoding = Some(cx.observe_in(
164 buffer,
165 window,
166 Self::update_when_buffer_encoding_changes,
167 ));
168 }
169 self.update_when_editor_is_switched(editor, window, cx);
170 self.show = true;
171 }
172 None => {
173 self.encoding = None;
174 self.observe_editor = None;
175 self.show = false;
176 }
177 }
178 }
179}
180
181/// Get a human-readable name for the given encoding.
182pub fn encoding_name(encoding: &'static Encoding) -> String {
183 let name = encoding.name();
184
185 match name {
186 "UTF-8" => "UTF-8",
187 "UTF-16LE" => "UTF-16 LE",
188 "UTF-16BE" => "UTF-16 BE",
189 "windows-1252" => "Windows-1252",
190 "windows-1251" => "Windows-1251",
191 "windows-1250" => "Windows-1250",
192 "ISO-8859-2" => "ISO 8859-2",
193 "ISO-8859-3" => "ISO 8859-3",
194 "ISO-8859-4" => "ISO 8859-4",
195 "ISO-8859-5" => "ISO 8859-5",
196 "ISO-8859-6" => "ISO 8859-6",
197 "ISO-8859-7" => "ISO 8859-7",
198 "ISO-8859-8" => "ISO 8859-8",
199 "ISO-8859-13" => "ISO 8859-13",
200 "ISO-8859-15" => "ISO 8859-15",
201 "KOI8-R" => "KOI8-R",
202 "KOI8-U" => "KOI8-U",
203 "macintosh" => "MacRoman",
204 "x-mac-cyrillic" => "Mac Cyrillic",
205 "windows-874" => "Windows-874",
206 "windows-1253" => "Windows-1253",
207 "windows-1254" => "Windows-1254",
208 "windows-1255" => "Windows-1255",
209 "windows-1256" => "Windows-1256",
210 "windows-1257" => "Windows-1257",
211 "windows-1258" => "Windows-1258",
212 "EUC-KR" => "Windows-949",
213 "EUC-JP" => "EUC-JP",
214 "ISO-2022-JP" => "ISO 2022-JP",
215 "GBK" => "GBK",
216 "gb18030" => "GB18030",
217 "Big5" => "Big5",
218 _ => name,
219 }
220 .to_string()
221}
222
223/// Get an encoding from its index in the predefined list.
224/// If the index is out of range, UTF-8 is returned as a default.
225pub fn encoding_from_index(index: usize) -> &'static Encoding {
226 match index {
227 0 => encoding_rs::UTF_8,
228 1 => encoding_rs::UTF_16LE,
229 2 => encoding_rs::UTF_16BE,
230 3 => encoding_rs::WINDOWS_1252,
231 4 => encoding_rs::WINDOWS_1251,
232 5 => encoding_rs::WINDOWS_1250,
233 6 => encoding_rs::ISO_8859_2,
234 7 => encoding_rs::ISO_8859_3,
235 8 => encoding_rs::ISO_8859_4,
236 9 => encoding_rs::ISO_8859_5,
237 10 => encoding_rs::ISO_8859_6,
238 11 => encoding_rs::ISO_8859_7,
239 12 => encoding_rs::ISO_8859_8,
240 13 => encoding_rs::ISO_8859_13,
241 14 => encoding_rs::ISO_8859_15,
242 15 => encoding_rs::KOI8_R,
243 16 => encoding_rs::KOI8_U,
244 17 => encoding_rs::MACINTOSH,
245 18 => encoding_rs::X_MAC_CYRILLIC,
246 19 => encoding_rs::WINDOWS_874,
247 20 => encoding_rs::WINDOWS_1253,
248 21 => encoding_rs::WINDOWS_1254,
249 22 => encoding_rs::WINDOWS_1255,
250 23 => encoding_rs::WINDOWS_1256,
251 24 => encoding_rs::WINDOWS_1257,
252 25 => encoding_rs::WINDOWS_1258,
253 26 => encoding_rs::EUC_KR,
254 27 => encoding_rs::EUC_JP,
255 28 => encoding_rs::ISO_2022_JP,
256 29 => encoding_rs::GBK,
257 30 => encoding_rs::GB18030,
258 31 => encoding_rs::BIG5,
259 _ => encoding_rs::UTF_8,
260 }
261}
262
263/// Get an encoding from its name.
264pub fn encoding_from_name(name: &str) -> &'static Encoding {
265 match name {
266 "UTF-8" => encoding_rs::UTF_8,
267 "UTF-16 LE" => encoding_rs::UTF_16LE,
268 "UTF-16 BE" => encoding_rs::UTF_16BE,
269 "Windows-1252" => encoding_rs::WINDOWS_1252,
270 "Windows-1251" => encoding_rs::WINDOWS_1251,
271 "Windows-1250" => encoding_rs::WINDOWS_1250,
272 "ISO 8859-2" => encoding_rs::ISO_8859_2,
273 "ISO 8859-3" => encoding_rs::ISO_8859_3,
274 "ISO 8859-4" => encoding_rs::ISO_8859_4,
275 "ISO 8859-5" => encoding_rs::ISO_8859_5,
276 "ISO 8859-6" => encoding_rs::ISO_8859_6,
277 "ISO 8859-7" => encoding_rs::ISO_8859_7,
278 "ISO 8859-8" => encoding_rs::ISO_8859_8,
279 "ISO 8859-13" => encoding_rs::ISO_8859_13,
280 "ISO 8859-15" => encoding_rs::ISO_8859_15,
281 "KOI8-R" => encoding_rs::KOI8_R,
282 "KOI8-U" => encoding_rs::KOI8_U,
283 "MacRoman" => encoding_rs::MACINTOSH,
284 "Mac Cyrillic" => encoding_rs::X_MAC_CYRILLIC,
285 "Windows-874" => encoding_rs::WINDOWS_874,
286 "Windows-1253" => encoding_rs::WINDOWS_1253,
287 "Windows-1254" => encoding_rs::WINDOWS_1254,
288 "Windows-1255" => encoding_rs::WINDOWS_1255,
289 "Windows-1256" => encoding_rs::WINDOWS_1256,
290 "Windows-1257" => encoding_rs::WINDOWS_1257,
291 "Windows-1258" => encoding_rs::WINDOWS_1258,
292 "Windows-949" => encoding_rs::EUC_KR,
293 "EUC-JP" => encoding_rs::EUC_JP,
294 "ISO 2022-JP" => encoding_rs::ISO_2022_JP,
295 "GBK" => encoding_rs::GBK,
296 "GB18030" => encoding_rs::GB18030,
297 "Big5" => encoding_rs::BIG5,
298 _ => encoding_rs::UTF_8, // Default to UTF-8 for unknown names
299 }
300}
301
302pub fn init(cx: &mut App) {
303 cx.on_action(|action: &Toggle, cx: &mut App| {
304 let Toggle(path) = action.clone();
305 let path = path.to_path_buf();
306
307 with_active_or_new_workspace(cx, |workspace, window, cx| {
308 let weak_workspace = workspace.weak_handle();
309 workspace.toggle_modal(window, cx, |window, cx| {
310 EncodingSelector::new(window, cx, Action::Reopen, None, weak_workspace, Some(path))
311 });
312 });
313 });
314
315 cx.on_action(|action: &ForceOpen, cx: &mut App| {
316 let ForceOpen(path) = action.clone();
317 let path = path.to_path_buf();
318
319 with_active_or_new_workspace(cx, |workspace, window, cx| {
320 workspace.active_pane().update(cx, |pane, cx| {
321 pane.close_active_item(&CloseActiveItem::default(), window, cx)
322 .detach();
323 });
324
325 {
326 let force = workspace.encoding_options.force.get_mut();
327
328 *force = true;
329 }
330
331 let open_task = workspace.open_abs_path(path, OpenOptions::default(), window, cx);
332 let weak_workspace = workspace.weak_handle();
333
334 cx.spawn(async move |_, cx| {
335 let workspace = weak_workspace.upgrade().unwrap();
336 open_task.await.log_err();
337 workspace
338 .update(cx, |workspace: &mut Workspace, _| {
339 *workspace.encoding_options.force.get_mut() = false;
340 })
341 .log_err();
342 })
343 .detach();
344 });
345 });
346}