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