1use editor::{Editor, EditorEvent, MultiBufferSnapshot};
  2use gpui::{App, Entity, FocusHandle, Focusable, Styled, Subscription, Task, WeakEntity};
  3use settings::Settings;
  4use std::{fmt::Write, num::NonZeroU32, time::Duration};
  5use text::{Point, Selection};
  6use ui::{
  7    Button, ButtonCommon, Clickable, Context, FluentBuilder, IntoElement, LabelSize, ParentElement,
  8    Render, Tooltip, Window, div,
  9};
 10use util::paths::FILE_ROW_COLUMN_DELIMITER;
 11use workspace::{StatusBarSettings, StatusItemView, Workspace, item::ItemHandle};
 12
 13#[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)]
 14pub(crate) struct SelectionStats {
 15    pub lines: usize,
 16    pub characters: usize,
 17    pub selections: usize,
 18}
 19
 20pub struct CursorPosition {
 21    position: Option<UserCaretPosition>,
 22    selected_count: SelectionStats,
 23    context: Option<FocusHandle>,
 24    workspace: WeakEntity<Workspace>,
 25    update_position: Task<()>,
 26    _observe_active_editor: Option<Subscription>,
 27}
 28
 29/// A position in the editor, where user's caret is located at.
 30/// Lines are never zero as there is always at least one line in the editor.
 31/// Characters may start with zero as the caret may be at the beginning of a line, but all editors start counting characters from 1,
 32/// where "1" will mean "before the first character".
 33#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 34pub struct UserCaretPosition {
 35    pub line: NonZeroU32,
 36    pub character: NonZeroU32,
 37}
 38
 39impl UserCaretPosition {
 40    pub(crate) fn at_selection_end(
 41        selection: &Selection<Point>,
 42        snapshot: &MultiBufferSnapshot,
 43    ) -> Self {
 44        let selection_end = selection.head();
 45        let (line, character) = if let Some((buffer_snapshot, point, _)) =
 46            snapshot.point_to_buffer_point(selection_end)
 47        {
 48            let line_start = Point::new(point.row, 0);
 49
 50            let chars_to_last_position = buffer_snapshot
 51                .text_summary_for_range::<text::TextSummary, _>(line_start..point)
 52                .chars as u32;
 53            (line_start.row, chars_to_last_position)
 54        } else {
 55            let line_start = Point::new(selection_end.row, 0);
 56
 57            let chars_to_last_position = snapshot
 58                .text_summary_for_range::<text::TextSummary, _>(line_start..selection_end)
 59                .chars as u32;
 60            (selection_end.row, chars_to_last_position)
 61        };
 62
 63        Self {
 64            line: NonZeroU32::new(line + 1).expect("added 1"),
 65            character: NonZeroU32::new(character + 1).expect("added 1"),
 66        }
 67    }
 68}
 69
 70impl CursorPosition {
 71    pub fn new(workspace: &Workspace) -> Self {
 72        Self {
 73            position: None,
 74            context: None,
 75            selected_count: Default::default(),
 76            workspace: workspace.weak_handle(),
 77            update_position: Task::ready(()),
 78            _observe_active_editor: None,
 79        }
 80    }
 81
 82    fn update_position(
 83        &mut self,
 84        editor: &Entity<Editor>,
 85        debounce: Option<Duration>,
 86        window: &mut Window,
 87        cx: &mut Context<Self>,
 88    ) {
 89        let editor = editor.downgrade();
 90        self.update_position = cx.spawn_in(window, async move |cursor_position, cx| {
 91            let is_singleton = editor
 92                .update(cx, |editor, cx| editor.buffer().read(cx).is_singleton())
 93                .ok()
 94                .unwrap_or(true);
 95
 96            if !is_singleton && let Some(debounce) = debounce {
 97                cx.background_executor().timer(debounce).await;
 98            }
 99
100            editor
101                .update(cx, |editor, cx| {
102                    cursor_position.update(cx, |cursor_position, cx| {
103                        cursor_position.selected_count = SelectionStats::default();
104                        cursor_position.selected_count.selections = editor.selections.count();
105                        match editor.mode() {
106                            editor::EditorMode::AutoHeight { .. }
107                            | editor::EditorMode::SingleLine
108                            | editor::EditorMode::Minimap { .. } => {
109                                cursor_position.position = None;
110                                cursor_position.context = None;
111                            }
112                            editor::EditorMode::Full { .. } => {
113                                let mut last_selection = None::<Selection<Point>>;
114                                let snapshot = editor.display_snapshot(cx);
115                                if snapshot.buffer_snapshot().excerpts().count() > 0 {
116                                    for selection in editor.selections.all_adjusted(&snapshot) {
117                                        let selection_summary = snapshot
118                                            .buffer_snapshot()
119                                            .text_summary_for_range::<text::TextSummary, _>(
120                                            selection.start..selection.end,
121                                        );
122                                        cursor_position.selected_count.characters +=
123                                            selection_summary.chars;
124                                        if selection.end != selection.start {
125                                            cursor_position.selected_count.lines +=
126                                                (selection.end.row - selection.start.row) as usize;
127                                            if selection.end.column != 0 {
128                                                cursor_position.selected_count.lines += 1;
129                                            }
130                                        }
131                                        if last_selection.as_ref().is_none_or(|last_selection| {
132                                            selection.id > last_selection.id
133                                        }) {
134                                            last_selection = Some(selection);
135                                        }
136                                    }
137                                }
138                                cursor_position.position = last_selection.map(|s| {
139                                    UserCaretPosition::at_selection_end(
140                                        &s,
141                                        snapshot.buffer_snapshot(),
142                                    )
143                                });
144                                cursor_position.context = Some(editor.focus_handle(cx));
145                            }
146                        }
147
148                        cx.notify();
149                    })
150                })
151                .ok()
152                .transpose()
153                .ok()
154                .flatten();
155        });
156    }
157
158    fn write_position(&self, text: &mut String, cx: &App) {
159        if self.selected_count
160            <= (SelectionStats {
161                selections: 1,
162                ..Default::default()
163            })
164        {
165            // Do not write out anything if we have just one empty selection.
166            return;
167        }
168        let SelectionStats {
169            lines,
170            characters,
171            selections,
172        } = self.selected_count;
173        let format = LineIndicatorFormat::get(None, cx);
174        let is_short_format = format == &LineIndicatorFormat::Short;
175        let lines = (lines > 1).then_some((lines, "line"));
176        let selections = (selections > 1).then_some((selections, "selection"));
177        let characters = (characters > 0).then_some((characters, "character"));
178        if (None, None, None) == (characters, selections, lines) {
179            // Nothing to display.
180            return;
181        }
182        write!(text, " (").unwrap();
183        let mut wrote_once = false;
184        for (count, name) in [selections, lines, characters].into_iter().flatten() {
185            if wrote_once {
186                write!(text, ", ").unwrap();
187            }
188            let name = if is_short_format { &name[..1] } else { name };
189            let plural_suffix = if count > 1 && !is_short_format {
190                "s"
191            } else {
192                ""
193            };
194            write!(text, "{count} {name}{plural_suffix}").unwrap();
195            wrote_once = true;
196        }
197        text.push(')');
198    }
199
200    #[cfg(test)]
201    pub(crate) fn selection_stats(&self) -> &SelectionStats {
202        &self.selected_count
203    }
204
205    #[cfg(test)]
206    pub(crate) fn position(&self) -> Option<UserCaretPosition> {
207        self.position
208    }
209}
210
211impl Render for CursorPosition {
212    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
213        if !StatusBarSettings::get_global(cx).cursor_position_button {
214            return div().hidden();
215        }
216
217        div().when_some(self.position, |el, position| {
218            let mut text = format!(
219                "{}{FILE_ROW_COLUMN_DELIMITER}{}",
220                position.line, position.character,
221            );
222            self.write_position(&mut text, cx);
223
224            let context = self.context.clone();
225
226            el.child(
227                Button::new("go-to-line-column", text)
228                    .label_size(LabelSize::Small)
229                    .on_click(cx.listener(|this, _, window, cx| {
230                        if let Some(workspace) = this.workspace.upgrade() {
231                            workspace.update(cx, |workspace, cx| {
232                                if let Some(editor) = workspace
233                                    .active_item(cx)
234                                    .and_then(|item| item.act_as::<Editor>(cx))
235                                    && let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx)
236                                {
237                                    workspace.toggle_modal(window, cx, |window, cx| {
238                                        crate::GoToLine::new(editor, buffer, window, cx)
239                                    })
240                                }
241                            });
242                        }
243                    }))
244                    .tooltip(move |_window, cx| match context.as_ref() {
245                        Some(context) => Tooltip::for_action_in(
246                            "Go to Line/Column",
247                            &editor::actions::ToggleGoToLine,
248                            context,
249                            cx,
250                        ),
251                        None => Tooltip::for_action(
252                            "Go to Line/Column",
253                            &editor::actions::ToggleGoToLine,
254                            cx,
255                        ),
256                    }),
257            )
258        })
259    }
260}
261
262const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
263
264impl StatusItemView for CursorPosition {
265    fn set_active_pane_item(
266        &mut self,
267        active_pane_item: Option<&dyn ItemHandle>,
268        window: &mut Window,
269        cx: &mut Context<Self>,
270    ) {
271        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
272            self._observe_active_editor = Some(cx.subscribe_in(
273                &editor,
274                window,
275                |cursor_position, editor, event, window, cx| match event {
276                    EditorEvent::SelectionsChanged { .. } => Self::update_position(
277                        cursor_position,
278                        editor,
279                        Some(UPDATE_DEBOUNCE),
280                        window,
281                        cx,
282                    ),
283                    _ => {}
284                },
285            ));
286            self.update_position(&editor, None, window, cx);
287        } else {
288            self.position = None;
289            self._observe_active_editor = None;
290        }
291
292        cx.notify();
293    }
294}
295
296#[derive(Clone, Copy, PartialEq, Eq)]
297pub enum LineIndicatorFormat {
298    Short,
299    Long,
300}
301
302impl From<settings::LineIndicatorFormat> for LineIndicatorFormat {
303    fn from(format: settings::LineIndicatorFormat) -> Self {
304        match format {
305            settings::LineIndicatorFormat::Short => LineIndicatorFormat::Short,
306            settings::LineIndicatorFormat::Long => LineIndicatorFormat::Long,
307        }
308    }
309}
310
311impl Settings for LineIndicatorFormat {
312    fn from_settings(content: &settings::SettingsContent) -> Self {
313        content.line_indicator_format.unwrap().into()
314    }
315}