line_ending_selector.rs

  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}