line_ending_selector.rs

  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}