cursor_position.rs

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