1mod active_buffer_encoding;
2pub use active_buffer_encoding::ActiveBufferEncoding;
3
4use editor::Editor;
5use encoding_rs::Encoding;
6use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
7use gpui::{
8 App, AppContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
9 InteractiveElement, ParentElement, Render, Styled, Task, WeakEntity, Window, actions,
10};
11use language::Buffer;
12use picker::{Picker, PickerDelegate};
13use std::sync::Arc;
14use ui::{HighlightedLabel, ListItem, ListItemSpacing, Toggleable, v_flex};
15use util::ResultExt;
16use workspace::{
17 ModalView, Toast, Workspace,
18 notifications::{NotificationId, NotificationSource},
19};
20
21actions!(
22 encoding_selector,
23 [
24 /// Toggles the encoding selector modal.
25 Toggle
26 ]
27);
28
29pub fn init(cx: &mut App) {
30 cx.observe_new(EncodingSelector::register).detach();
31}
32
33pub struct EncodingSelector {
34 picker: Entity<Picker<EncodingSelectorDelegate>>,
35}
36
37impl EncodingSelector {
38 fn register(
39 workspace: &mut Workspace,
40 _window: Option<&mut Window>,
41 _: &mut Context<Workspace>,
42 ) {
43 workspace.register_action(move |workspace, _: &Toggle, window, cx| {
44 Self::toggle(workspace, window, cx);
45 });
46 }
47
48 pub fn toggle(
49 workspace: &mut Workspace,
50 window: &mut Window,
51 cx: &mut Context<Workspace>,
52 ) -> Option<()> {
53 let (_, buffer, _) = workspace
54 .active_item(cx)?
55 .act_as::<Editor>(cx)?
56 .read(cx)
57 .active_excerpt(cx)?;
58
59 let buffer_handle = buffer.read(cx);
60 let project = workspace.project().read(cx);
61
62 if buffer_handle.is_dirty() {
63 workspace.show_toast(
64 Toast::new(
65 NotificationId::unique::<EncodingSelector>(),
66 "Save file to change encoding",
67 ),
68 NotificationSource::Editor,
69 cx,
70 );
71 return Some(());
72 }
73 if project.is_shared() {
74 workspace.show_toast(
75 Toast::new(
76 NotificationId::unique::<EncodingSelector>(),
77 "Cannot change encoding during collaboration",
78 ),
79 NotificationSource::Collab,
80 cx,
81 );
82 return Some(());
83 }
84 if project.is_via_remote_server() {
85 workspace.show_toast(
86 Toast::new(
87 NotificationId::unique::<EncodingSelector>(),
88 "Cannot change encoding of remote server file",
89 ),
90 NotificationSource::Remote,
91 cx,
92 );
93 return Some(());
94 }
95
96 workspace.toggle_modal(window, cx, move |window, cx| {
97 EncodingSelector::new(buffer, window, cx)
98 });
99 Some(())
100 }
101
102 fn new(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Context<Self>) -> Self {
103 let delegate = EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer);
104 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
105 Self { picker }
106 }
107}
108
109impl Render for EncodingSelector {
110 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
111 v_flex()
112 .key_context("EncodingSelector")
113 .w(gpui::rems(34.))
114 .child(self.picker.clone())
115 }
116}
117
118impl Focusable for EncodingSelector {
119 fn focus_handle(&self, cx: &App) -> FocusHandle {
120 self.picker.focus_handle(cx)
121 }
122}
123
124impl EventEmitter<DismissEvent> for EncodingSelector {}
125impl ModalView for EncodingSelector {}
126
127pub struct EncodingSelectorDelegate {
128 encoding_selector: WeakEntity<EncodingSelector>,
129 buffer: Entity<Buffer>,
130 encodings: Vec<&'static Encoding>,
131 match_candidates: Arc<Vec<StringMatchCandidate>>,
132 matches: Vec<StringMatch>,
133 selected_index: usize,
134}
135
136impl EncodingSelectorDelegate {
137 fn new(encoding_selector: WeakEntity<EncodingSelector>, buffer: Entity<Buffer>) -> Self {
138 let encodings = available_encodings();
139 let match_candidates = encodings
140 .iter()
141 .enumerate()
142 .map(|(id, enc)| StringMatchCandidate::new(id, enc.name()))
143 .collect::<Vec<_>>();
144 Self {
145 encoding_selector,
146 buffer,
147 encodings,
148 match_candidates: Arc::new(match_candidates),
149 matches: vec![],
150 selected_index: 0,
151 }
152 }
153
154 fn render_data_for_match(&self, mat: &StringMatch, cx: &App) -> String {
155 let candidate_encoding = self.encodings[mat.candidate_id];
156 let current_encoding = self.buffer.read(cx).encoding();
157
158 if candidate_encoding.name() == current_encoding.name() {
159 format!("{} (current)", candidate_encoding.name())
160 } else {
161 candidate_encoding.name().to_string()
162 }
163 }
164}
165
166fn available_encodings() -> Vec<&'static Encoding> {
167 let mut encodings = vec![
168 // Unicode
169 encoding_rs::UTF_8,
170 encoding_rs::UTF_16LE,
171 encoding_rs::UTF_16BE,
172 // Japanese
173 encoding_rs::SHIFT_JIS,
174 encoding_rs::EUC_JP,
175 encoding_rs::ISO_2022_JP,
176 // Chinese
177 encoding_rs::GBK,
178 encoding_rs::GB18030,
179 encoding_rs::BIG5,
180 // Korean
181 encoding_rs::EUC_KR,
182 // Windows / Single Byte Series
183 encoding_rs::WINDOWS_1252, // Western (ISO-8859-1 unified)
184 encoding_rs::WINDOWS_1250, // Central European
185 encoding_rs::WINDOWS_1251, // Cyrillic
186 encoding_rs::WINDOWS_1253, // Greek
187 encoding_rs::WINDOWS_1254, // Turkish (ISO-8859-9 unified)
188 encoding_rs::WINDOWS_1255, // Hebrew
189 encoding_rs::WINDOWS_1256, // Arabic
190 encoding_rs::WINDOWS_1257, // Baltic
191 encoding_rs::WINDOWS_1258, // Vietnamese
192 encoding_rs::WINDOWS_874, // Thai
193 // ISO-8859 Series (others)
194 encoding_rs::ISO_8859_2,
195 encoding_rs::ISO_8859_3,
196 encoding_rs::ISO_8859_4,
197 encoding_rs::ISO_8859_5,
198 encoding_rs::ISO_8859_6,
199 encoding_rs::ISO_8859_7,
200 encoding_rs::ISO_8859_8,
201 encoding_rs::ISO_8859_8_I, // Logical Hebrew
202 encoding_rs::ISO_8859_10,
203 encoding_rs::ISO_8859_13,
204 encoding_rs::ISO_8859_14,
205 encoding_rs::ISO_8859_15,
206 encoding_rs::ISO_8859_16,
207 // Cyrillic / Legacy Misc
208 encoding_rs::KOI8_R,
209 encoding_rs::KOI8_U,
210 encoding_rs::IBM866,
211 encoding_rs::MACINTOSH,
212 encoding_rs::X_MAC_CYRILLIC,
213 // NOTE: The following encodings are intentionally excluded from the list:
214 //
215 // 1. encoding_rs::REPLACEMENT
216 // Used internally for decoding errors. Not suitable for user selection.
217 //
218 // 2. encoding_rs::X_USER_DEFINED
219 // Used for binary data emulation (legacy web behavior). Not for general text editing.
220 ];
221
222 encodings.sort_by_key(|enc| enc.name());
223
224 encodings
225}
226
227impl PickerDelegate for EncodingSelectorDelegate {
228 type ListItem = ListItem;
229
230 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
231 "Reopen with encoding...".into()
232 }
233
234 fn match_count(&self) -> usize {
235 self.matches.len()
236 }
237
238 fn selected_index(&self) -> usize {
239 self.selected_index
240 }
241
242 fn set_selected_index(
243 &mut self,
244 ix: usize,
245 _window: &mut Window,
246 _: &mut Context<Picker<Self>>,
247 ) {
248 self.selected_index = ix;
249 }
250
251 fn update_matches(
252 &mut self,
253 query: String,
254 window: &mut Window,
255 cx: &mut Context<Picker<Self>>,
256 ) -> Task<()> {
257 let background = cx.background_executor().clone();
258 let candidates = self.match_candidates.clone();
259
260 cx.spawn_in(window, async move |this, cx| {
261 let matches = if query.is_empty() {
262 candidates
263 .iter()
264 .enumerate()
265 .map(|(index, candidate)| StringMatch {
266 candidate_id: index,
267 string: candidate.string.clone(),
268 positions: Vec::new(),
269 score: 0.0,
270 })
271 .collect()
272 } else {
273 match_strings(
274 &candidates,
275 &query,
276 false,
277 true,
278 100,
279 &Default::default(),
280 background,
281 )
282 .await
283 };
284
285 this.update(cx, |this, cx| {
286 let delegate = &mut this.delegate;
287 delegate.matches = matches;
288 delegate.selected_index = delegate
289 .selected_index
290 .min(delegate.matches.len().saturating_sub(1));
291 cx.notify();
292 })
293 .log_err();
294 })
295 }
296
297 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
298 if let Some(mat) = self.matches.get(self.selected_index) {
299 let selected_encoding = self.encodings[mat.candidate_id];
300
301 self.buffer.update(cx, |buffer, cx| {
302 let _ = buffer.reload_with_encoding(selected_encoding, cx);
303 });
304 }
305 self.dismissed(window, cx);
306 }
307
308 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
309 self.encoding_selector
310 .update(cx, |_, cx| cx.emit(DismissEvent))
311 .log_err();
312 }
313
314 fn render_match(
315 &self,
316 ix: usize,
317 selected: bool,
318 _: &mut Window,
319 cx: &mut Context<Picker<Self>>,
320 ) -> Option<Self::ListItem> {
321 let mat = &self.matches.get(ix)?;
322
323 let label = self.render_data_for_match(mat, cx);
324
325 Some(
326 ListItem::new(ix)
327 .inset(true)
328 .spacing(ListItemSpacing::Sparse)
329 .toggle_state(selected)
330 .child(HighlightedLabel::new(label, mat.positions.clone())),
331 )
332 }
333}