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