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