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