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