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