1use editor::Editor;
2use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, actions};
3use language::{Buffer, LineEnding};
4use picker::{Picker, PickerDelegate};
5use project::Project;
6use std::sync::Arc;
7use ui::{ListItem, ListItemSpacing, prelude::*};
8use util::ResultExt;
9use workspace::ModalView;
10
11actions!(
12 line_ending,
13 [
14 /// Toggles the line ending selector modal.
15 Toggle
16 ]
17);
18
19pub fn init(cx: &mut App) {
20 cx.observe_new(LineEndingSelector::register).detach();
21}
22
23pub struct LineEndingSelector {
24 picker: Entity<Picker<LineEndingSelectorDelegate>>,
25}
26
27impl LineEndingSelector {
28 fn register(editor: &mut Editor, _window: Option<&mut Window>, cx: &mut Context<Editor>) {
29 let editor_handle = cx.weak_entity();
30 editor
31 .register_action(move |_: &Toggle, window, cx| {
32 Self::toggle(&editor_handle, window, cx);
33 })
34 .detach();
35 }
36
37 fn toggle(editor: &WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
38 let Some((workspace, buffer)) = editor
39 .update(cx, |editor, cx| {
40 Some((editor.workspace()?, editor.active_excerpt(cx)?.1))
41 })
42 .ok()
43 .flatten()
44 else {
45 return;
46 };
47
48 workspace.update(cx, |workspace, cx| {
49 let project = workspace.project().clone();
50 workspace.toggle_modal(window, cx, move |window, cx| {
51 LineEndingSelector::new(buffer, project, window, cx)
52 });
53 })
54 }
55
56 fn new(
57 buffer: Entity<Buffer>,
58 project: Entity<Project>,
59 window: &mut Window,
60 cx: &mut Context<Self>,
61 ) -> Self {
62 let line_ending = buffer.read(cx).line_ending();
63 let delegate =
64 LineEndingSelectorDelegate::new(cx.entity().downgrade(), buffer, project, line_ending);
65 let picker = cx.new(|cx| Picker::nonsearchable_uniform_list(delegate, window, cx));
66 Self { picker }
67 }
68}
69
70impl Render for LineEndingSelector {
71 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
72 v_flex().w(rems(34.)).child(self.picker.clone())
73 }
74}
75
76impl Focusable for LineEndingSelector {
77 fn focus_handle(&self, cx: &App) -> FocusHandle {
78 self.picker.focus_handle(cx)
79 }
80}
81
82impl EventEmitter<DismissEvent> for LineEndingSelector {}
83impl ModalView for LineEndingSelector {}
84
85struct LineEndingSelectorDelegate {
86 line_ending_selector: WeakEntity<LineEndingSelector>,
87 buffer: Entity<Buffer>,
88 project: Entity<Project>,
89 line_ending: LineEnding,
90 matches: Vec<LineEnding>,
91 selected_index: usize,
92}
93
94impl LineEndingSelectorDelegate {
95 fn new(
96 line_ending_selector: WeakEntity<LineEndingSelector>,
97 buffer: Entity<Buffer>,
98 project: Entity<Project>,
99 line_ending: LineEnding,
100 ) -> Self {
101 Self {
102 line_ending_selector,
103 buffer,
104 project,
105 line_ending,
106 matches: vec![LineEnding::Unix, LineEnding::Windows],
107 selected_index: 0,
108 }
109 }
110}
111
112impl PickerDelegate for LineEndingSelectorDelegate {
113 type ListItem = ListItem;
114
115 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
116 "Select a line ending…".into()
117 }
118
119 fn match_count(&self) -> usize {
120 self.matches.len()
121 }
122
123 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
124 if let Some(line_ending) = self.matches.get(self.selected_index) {
125 self.buffer.update(cx, |this, cx| {
126 this.set_line_ending(*line_ending, cx);
127 });
128 let buffer = self.buffer.clone();
129 let project = self.project.clone();
130 cx.defer(move |cx| {
131 project.update(cx, |this, cx| {
132 this.save_buffer(buffer, cx).detach();
133 });
134 });
135 }
136 self.dismissed(window, cx);
137 }
138
139 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
140 self.line_ending_selector
141 .update(cx, |_, cx| cx.emit(DismissEvent))
142 .log_err();
143 }
144
145 fn selected_index(&self) -> usize {
146 self.selected_index
147 }
148
149 fn set_selected_index(
150 &mut self,
151 ix: usize,
152 _window: &mut Window,
153 _: &mut Context<Picker<Self>>,
154 ) {
155 self.selected_index = ix;
156 }
157
158 fn update_matches(
159 &mut self,
160 _query: String,
161 _window: &mut Window,
162 _cx: &mut Context<Picker<Self>>,
163 ) -> gpui::Task<()> {
164 return Task::ready(());
165 }
166
167 fn render_match(
168 &self,
169 ix: usize,
170 selected: bool,
171 _: &mut Window,
172 _: &mut Context<Picker<Self>>,
173 ) -> Option<Self::ListItem> {
174 let line_ending = self.matches.get(ix)?;
175 let label = match line_ending {
176 LineEnding::Unix => "LF",
177 LineEnding::Windows => "CRLF",
178 };
179
180 let mut list_item = ListItem::new(ix)
181 .inset(true)
182 .spacing(ListItemSpacing::Sparse)
183 .toggle_state(selected)
184 .child(Label::new(label));
185
186 if &self.line_ending == line_ending {
187 list_item = list_item.end_slot(Icon::new(IconName::Check).color(Color::Muted));
188 }
189
190 Some(list_item)
191 }
192}