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