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}