1/// This module contains the encoding selectors for saving or reopening files with a different encoding.
2/// It provides a modal view that allows the user to choose between saving with a different encoding
3/// or reopening with a different encoding, and then selecting the desired encoding from a list.
4pub mod save_or_reopen {
5 use editor::Editor;
6 use gpui::Styled;
7 use gpui::{AppContext, ParentElement};
8 use picker::Picker;
9 use picker::PickerDelegate;
10 use std::sync::atomic::AtomicBool;
11 use util::ResultExt;
12
13 use fuzzy::{StringMatch, StringMatchCandidate};
14 use gpui::{DismissEvent, Entity, EventEmitter, Focusable, WeakEntity};
15
16 use ui::{Context, HighlightedLabel, ListItem, Render, Window, rems, v_flex};
17 use workspace::{ModalView, Workspace};
18
19 use crate::selectors::encoding::{Action, EncodingSelector};
20
21 /// A modal view that allows the user to select between saving with a different encoding or
22 /// reopening with a different encoding.
23 pub struct EncodingSaveOrReopenSelector {
24 picker: Entity<Picker<EncodingSaveOrReopenDelegate>>,
25 pub current_selection: usize,
26 }
27
28 impl EncodingSaveOrReopenSelector {
29 pub fn new(
30 window: &mut Window,
31 cx: &mut Context<EncodingSaveOrReopenSelector>,
32 workspace: WeakEntity<Workspace>,
33 ) -> Self {
34 let delegate =
35 EncodingSaveOrReopenDelegate::new(cx.entity().downgrade(), workspace.clone());
36
37 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
38
39 Self {
40 picker,
41 current_selection: 0,
42 }
43 }
44
45 /// Toggle the modal view for selecting between saving with a different encoding or
46 /// reopening with a different encoding.
47 pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
48 let weak_workspace = workspace.weak_handle();
49 workspace.toggle_modal(window, cx, |window, cx| {
50 EncodingSaveOrReopenSelector::new(window, cx, weak_workspace)
51 });
52 }
53 }
54
55 impl Focusable for EncodingSaveOrReopenSelector {
56 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
57 self.picker.focus_handle(cx)
58 }
59 }
60
61 impl Render for EncodingSaveOrReopenSelector {
62 fn render(
63 &mut self,
64 _window: &mut Window,
65 _cx: &mut Context<Self>,
66 ) -> impl ui::IntoElement {
67 v_flex().w(rems(34.0)).child(self.picker.clone())
68 }
69 }
70
71 impl ModalView for EncodingSaveOrReopenSelector {}
72
73 impl EventEmitter<DismissEvent> for EncodingSaveOrReopenSelector {}
74
75 pub struct EncodingSaveOrReopenDelegate {
76 selector: WeakEntity<EncodingSaveOrReopenSelector>,
77 current_selection: usize,
78 matches: Vec<StringMatch>,
79 pub actions: Vec<StringMatchCandidate>,
80 workspace: WeakEntity<Workspace>,
81 }
82
83 impl EncodingSaveOrReopenDelegate {
84 pub fn new(
85 selector: WeakEntity<EncodingSaveOrReopenSelector>,
86 workspace: WeakEntity<Workspace>,
87 ) -> Self {
88 Self {
89 selector,
90 current_selection: 0,
91 matches: Vec::new(),
92 actions: vec![
93 StringMatchCandidate::new(0, "Save with encoding"),
94 StringMatchCandidate::new(1, "Reopen with encoding"),
95 ],
96 workspace,
97 }
98 }
99
100 pub fn get_actions(&self) -> (&str, &str) {
101 (&self.actions[0].string, &self.actions[1].string)
102 }
103
104 /// Handle the action selected by the user.
105 pub fn post_selection(
106 &self,
107 cx: &mut Context<Picker<EncodingSaveOrReopenDelegate>>,
108 window: &mut Window,
109 ) -> Option<()> {
110 if self.current_selection == 0 {
111 if let Some(workspace) = self.workspace.upgrade() {
112 let (_, buffer, _) = workspace
113 .read(cx)
114 .active_item(cx)?
115 .act_as::<Editor>(cx)?
116 .read(cx)
117 .active_excerpt(cx)?;
118
119 let weak_workspace = workspace.read(cx).weak_handle();
120
121 workspace.update(cx, |workspace, cx| {
122 workspace.toggle_modal(window, cx, |window, cx| {
123 let selector = EncodingSelector::new(
124 window,
125 cx,
126 Action::Save,
127 buffer.downgrade(),
128 weak_workspace,
129 );
130 selector
131 })
132 });
133 }
134 } else if self.current_selection == 1 {
135 if let Some(workspace) = self.workspace.upgrade() {
136 let (_, buffer, _) = workspace
137 .read(cx)
138 .active_item(cx)?
139 .act_as::<Editor>(cx)?
140 .read(cx)
141 .active_excerpt(cx)?;
142
143 let weak_workspace = workspace.read(cx).weak_handle();
144
145 workspace.update(cx, |workspace, cx| {
146 workspace.toggle_modal(window, cx, |window, cx| {
147 let selector = EncodingSelector::new(
148 window,
149 cx,
150 Action::Reopen,
151 buffer.downgrade(),
152 weak_workspace,
153 );
154 selector
155 });
156 });
157 }
158 }
159
160 Some(())
161 }
162 }
163
164 impl PickerDelegate for EncodingSaveOrReopenDelegate {
165 type ListItem = ListItem;
166
167 fn match_count(&self) -> usize {
168 self.matches.len()
169 }
170
171 fn selected_index(&self) -> usize {
172 self.current_selection
173 }
174
175 fn set_selected_index(
176 &mut self,
177 ix: usize,
178 _window: &mut Window,
179 cx: &mut Context<Picker<Self>>,
180 ) {
181 self.current_selection = ix;
182 self.selector
183 .update(cx, |selector, _cx| {
184 selector.current_selection = ix;
185 })
186 .log_err();
187 }
188
189 fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
190 "Select an action...".into()
191 }
192
193 fn update_matches(
194 &mut self,
195 query: String,
196 window: &mut Window,
197 cx: &mut Context<Picker<Self>>,
198 ) -> gpui::Task<()> {
199 let executor = cx.background_executor().clone();
200 let actions = self.actions.clone();
201
202 cx.spawn_in(window, async move |this, cx| {
203 let matches = if query.is_empty() {
204 actions
205 .into_iter()
206 .enumerate()
207 .map(|(index, value)| StringMatch {
208 candidate_id: index,
209 score: 0.0,
210 positions: vec![],
211 string: value.string,
212 })
213 .collect::<Vec<StringMatch>>()
214 } else {
215 fuzzy::match_strings(
216 &actions,
217 &query,
218 false,
219 false,
220 2,
221 &AtomicBool::new(false),
222 executor,
223 )
224 .await
225 };
226
227 this.update(cx, |picker, cx| {
228 let delegate = &mut picker.delegate;
229 delegate.matches = matches;
230 delegate.current_selection = delegate
231 .current_selection
232 .min(delegate.matches.len().saturating_sub(1));
233 delegate
234 .selector
235 .update(cx, |selector, _cx| {
236 selector.current_selection = delegate.current_selection
237 })
238 .log_err();
239 cx.notify();
240 })
241 .log_err();
242 })
243 }
244
245 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
246 self.dismissed(window, cx);
247 if self.selector.is_upgradable() {
248 self.post_selection(cx, window);
249 }
250 }
251
252 fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
253 self.selector
254 .update(cx, |_, cx| cx.emit(DismissEvent))
255 .log_err();
256 }
257
258 fn render_match(
259 &self,
260 ix: usize,
261 _: bool,
262 _: &mut Window,
263 _: &mut Context<Picker<Self>>,
264 ) -> Option<Self::ListItem> {
265 Some(
266 ListItem::new(ix)
267 .child(HighlightedLabel::new(
268 &self.matches[ix].string,
269 self.matches[ix].positions.clone(),
270 ))
271 .spacing(ui::ListItemSpacing::Sparse),
272 )
273 }
274 }
275
276 pub fn get_current_encoding() -> &'static str {
277 "UTF-8"
278 }
279}
280
281/// This module contains the encoding selector for choosing an encoding to save or reopen a file with.
282pub mod encoding {
283 use std::sync::atomic::AtomicBool;
284
285 use fuzzy::{StringMatch, StringMatchCandidate};
286 use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, WeakEntity};
287 use language::Buffer;
288 use picker::{Picker, PickerDelegate};
289 use ui::{
290 Context, HighlightedLabel, ListItem, ListItemSpacing, ParentElement, Render, Styled,
291 Window, rems, v_flex,
292 };
293 use util::{ResultExt, TryFutureExt};
294 use workspace::{ModalView, Workspace};
295
296 use crate::encoding_from_name;
297
298 /// A modal view that allows the user to select an encoding from a list of encodings.
299 pub struct EncodingSelector {
300 picker: Entity<Picker<EncodingSelectorDelegate>>,
301 workspace: WeakEntity<Workspace>,
302 }
303
304 pub struct EncodingSelectorDelegate {
305 current_selection: usize,
306 encodings: Vec<StringMatchCandidate>,
307 matches: Vec<StringMatch>,
308 selector: WeakEntity<EncodingSelector>,
309 buffer: WeakEntity<Buffer>,
310 action: Action,
311 }
312
313 impl EncodingSelectorDelegate {
314 pub fn new(
315 selector: WeakEntity<EncodingSelector>,
316 buffer: WeakEntity<Buffer>,
317 action: Action,
318 ) -> EncodingSelectorDelegate {
319 EncodingSelectorDelegate {
320 current_selection: 0,
321 encodings: vec![
322 StringMatchCandidate::new(0, "UTF-8"),
323 StringMatchCandidate::new(1, "Windows-1252"),
324 StringMatchCandidate::new(2, "Windows-1251"),
325 StringMatchCandidate::new(3, "Windows-1250"),
326 StringMatchCandidate::new(4, "ISO 8859-2"),
327 StringMatchCandidate::new(5, "ISO 8859-3"),
328 StringMatchCandidate::new(6, "ISO 8859-4"),
329 StringMatchCandidate::new(7, "ISO 8859-5"),
330 StringMatchCandidate::new(8, "ISO 8859-6"),
331 StringMatchCandidate::new(9, "ISO 8859-7"),
332 StringMatchCandidate::new(10, "ISO 8859-8"),
333 StringMatchCandidate::new(11, "ISO 8859-13"),
334 StringMatchCandidate::new(12, "ISO 8859-15"),
335 StringMatchCandidate::new(13, "KOI8-R"),
336 StringMatchCandidate::new(14, "KOI8-U"),
337 StringMatchCandidate::new(15, "MacRoman"),
338 StringMatchCandidate::new(16, "Mac Cyrillic"),
339 StringMatchCandidate::new(17, "Windows-874"),
340 StringMatchCandidate::new(18, "Windows-1253"),
341 StringMatchCandidate::new(19, "Windows-1254"),
342 StringMatchCandidate::new(20, "Windows-1255"),
343 StringMatchCandidate::new(21, "Windows-1256"),
344 StringMatchCandidate::new(22, "Windows-1257"),
345 StringMatchCandidate::new(23, "Windows-1258"),
346 StringMatchCandidate::new(24, "Windows-949"),
347 StringMatchCandidate::new(25, "EUC-JP"),
348 StringMatchCandidate::new(26, "ISO 2022-JP"),
349 StringMatchCandidate::new(27, "GBK"),
350 StringMatchCandidate::new(28, "GB18030"),
351 StringMatchCandidate::new(29, "Big5"),
352 ],
353 matches: Vec::new(),
354 selector,
355 buffer,
356 action,
357 }
358 }
359 }
360
361 impl PickerDelegate for EncodingSelectorDelegate {
362 type ListItem = ListItem;
363
364 fn match_count(&self) -> usize {
365 self.matches.len()
366 }
367
368 fn selected_index(&self) -> usize {
369 self.current_selection
370 }
371
372 fn set_selected_index(&mut self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) {
373 self.current_selection = ix;
374 }
375
376 fn placeholder_text(&self, _window: &mut Window, _cx: &mut ui::App) -> std::sync::Arc<str> {
377 "Select an encoding...".into()
378 }
379
380 fn update_matches(
381 &mut self,
382 query: String,
383 window: &mut Window,
384 cx: &mut Context<Picker<Self>>,
385 ) -> gpui::Task<()> {
386 let executor = cx.background_executor().clone();
387 let encodings = self.encodings.clone();
388
389 cx.spawn_in(window, async move |picker, cx| {
390 let matches: Vec<StringMatch>;
391
392 if query.is_empty() {
393 matches = encodings
394 .into_iter()
395 .enumerate()
396 .map(|(index, value)| StringMatch {
397 candidate_id: index,
398 score: 0.0,
399 positions: Vec::new(),
400 string: value.string,
401 })
402 .collect();
403 } else {
404 matches = fuzzy::match_strings(
405 &encodings,
406 &query,
407 true,
408 false,
409 30,
410 &AtomicBool::new(false),
411 executor,
412 )
413 .await
414 }
415
416 picker
417 .update(cx, |picker, cx| {
418 let delegate = &mut picker.delegate;
419 delegate.matches = matches;
420 delegate.current_selection = delegate
421 .current_selection
422 .min(delegate.matches.len().saturating_sub(1));
423 cx.notify();
424 })
425 .log_err();
426 })
427 }
428
429 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
430 if let Some(buffer) = self.buffer.upgrade() {
431 buffer.update(cx, |buffer, cx| {
432 buffer.encoding =
433 encoding_from_name(self.matches[self.current_selection].string.as_str());
434 if self.action == Action::Reopen {
435 let executor = cx.background_executor().clone();
436 executor.spawn(buffer.reload(cx)).detach();
437 } else if self.action == Action::Save {
438 let executor = cx.background_executor().clone();
439
440 let workspace = self
441 .selector
442 .upgrade()
443 .unwrap()
444 .read(cx)
445 .workspace
446 .upgrade()
447 .unwrap();
448
449 executor
450 .spawn(workspace.update(cx, |workspace, cx| {
451 workspace
452 .save_active_item(workspace::SaveIntent::Save, window, cx)
453 .log_err()
454 }))
455 .detach();
456 }
457 });
458 }
459 self.dismissed(window, cx);
460 }
461
462 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
463 self.selector
464 .update(cx, |_, cx| cx.emit(DismissEvent))
465 .log_err();
466 }
467
468 fn render_match(
469 &self,
470 ix: usize,
471 _: bool,
472 _: &mut Window,
473 _: &mut Context<Picker<Self>>,
474 ) -> Option<Self::ListItem> {
475 Some(
476 ListItem::new(ix)
477 .child(HighlightedLabel::new(
478 &self.matches[ix].string,
479 self.matches[ix].positions.clone(),
480 ))
481 .spacing(ListItemSpacing::Sparse),
482 )
483 }
484 }
485
486 /// The action to perform after selecting an encoding.
487 #[derive(PartialEq, Clone)]
488 pub enum Action {
489 Save,
490 Reopen,
491 }
492
493 impl EncodingSelector {
494 pub fn new(
495 window: &mut Window,
496 cx: &mut Context<EncodingSelector>,
497 action: Action,
498 buffer: WeakEntity<Buffer>,
499 workspace: WeakEntity<Workspace>,
500 ) -> EncodingSelector {
501 let delegate =
502 EncodingSelectorDelegate::new(cx.entity().downgrade(), buffer, action.clone());
503 let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
504
505 EncodingSelector { picker, workspace }
506 }
507 }
508
509 impl EventEmitter<DismissEvent> for EncodingSelector {}
510
511 impl Focusable for EncodingSelector {
512 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
513 self.picker.focus_handle(cx)
514 }
515 }
516
517 impl ModalView for EncodingSelector {}
518
519 impl Render for EncodingSelector {
520 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
521 v_flex().w(rems(34.0)).child(self.picker.clone())
522 }
523 }
524}