1use crate::{
2 display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
3 movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
4 MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
5};
6use anyhow::{anyhow, Result};
7use futures::FutureExt;
8use gpui::{
9 elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
10 RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
11};
12use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
13use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
14use rpc::proto::{self, update_view};
15use settings::Settings;
16use smallvec::SmallVec;
17use std::{
18 borrow::Cow,
19 cmp::{self, Ordering},
20 fmt::Write,
21 ops::Range,
22 path::{Path, PathBuf},
23};
24use text::Selection;
25use util::TryFutureExt;
26use workspace::{
27 searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
28 FollowableItem, Item, ItemEvent, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView,
29 ToolbarItemLocation,
30};
31
32pub const MAX_TAB_TITLE_LEN: usize = 24;
33
34impl FollowableItem for Editor {
35 fn from_state_proto(
36 pane: ViewHandle<workspace::Pane>,
37 project: ModelHandle<Project>,
38 state: &mut Option<proto::view::Variant>,
39 cx: &mut MutableAppContext,
40 ) -> Option<Task<Result<ViewHandle<Self>>>> {
41 let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
42 if let Some(proto::view::Variant::Editor(state)) = state.take() {
43 state
44 } else {
45 unreachable!()
46 }
47 } else {
48 return None;
49 };
50
51 let buffer = project.update(cx, |project, cx| {
52 project.open_buffer_by_id(state.buffer_id, cx)
53 });
54 Some(cx.spawn(|mut cx| async move {
55 let buffer = buffer.await?;
56 let editor = pane
57 .read_with(&cx, |pane, cx| {
58 let existing = pane.items_of_type::<Self>().find(|editor| {
59 editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
60 });
61 dbg!(&existing);
62 existing
63 })
64 .unwrap_or_else(|| {
65 pane.update(&mut cx, |_, cx| {
66 cx.add_view(|cx| Editor::for_buffer(buffer, Some(project), cx))
67 })
68 });
69 editor.update(&mut cx, |editor, cx| {
70 let excerpt_id;
71 let buffer_id;
72 {
73 let buffer = editor.buffer.read(cx).read(cx);
74 let singleton = buffer.as_singleton().unwrap();
75 excerpt_id = singleton.0.clone();
76 buffer_id = singleton.1;
77 }
78 let selections = state
79 .selections
80 .into_iter()
81 .map(|selection| {
82 deserialize_selection(&excerpt_id, buffer_id, selection)
83 .ok_or_else(|| anyhow!("invalid selection"))
84 })
85 .collect::<Result<Vec<_>>>()?;
86 if !selections.is_empty() {
87 editor.set_selections_from_remote(selections, cx);
88 }
89
90 if let Some(anchor) = state.scroll_top_anchor {
91 editor.set_scroll_top_anchor(
92 Anchor {
93 buffer_id: Some(state.buffer_id as usize),
94 excerpt_id,
95 text_anchor: language::proto::deserialize_anchor(anchor)
96 .ok_or_else(|| anyhow!("invalid scroll top"))?,
97 },
98 vec2f(state.scroll_x, state.scroll_y),
99 cx,
100 );
101 }
102
103 Ok::<_, anyhow::Error>(())
104 })?;
105 Ok(editor)
106 }))
107 }
108
109 fn set_leader_replica_id(
110 &mut self,
111 leader_replica_id: Option<u16>,
112 cx: &mut ViewContext<Self>,
113 ) {
114 self.leader_replica_id = leader_replica_id;
115 if self.leader_replica_id.is_some() {
116 self.buffer.update(cx, |buffer, cx| {
117 buffer.remove_active_selections(cx);
118 });
119 } else {
120 self.buffer.update(cx, |buffer, cx| {
121 if self.focused {
122 buffer.set_active_selections(
123 &self.selections.disjoint_anchors(),
124 self.selections.line_mode,
125 self.cursor_shape,
126 cx,
127 );
128 }
129 });
130 }
131 cx.notify();
132 }
133
134 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
135 let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
136 Some(proto::view::Variant::Editor(proto::view::Editor {
137 buffer_id,
138 scroll_top_anchor: Some(language::proto::serialize_anchor(
139 &self.scroll_top_anchor.text_anchor,
140 )),
141 scroll_x: self.scroll_position.x(),
142 scroll_y: self.scroll_position.y(),
143 selections: self
144 .selections
145 .disjoint_anchors()
146 .iter()
147 .map(serialize_selection)
148 .collect(),
149 }))
150 }
151
152 fn add_event_to_update_proto(
153 &self,
154 event: &Self::Event,
155 update: &mut Option<proto::update_view::Variant>,
156 _: &AppContext,
157 ) -> bool {
158 let update =
159 update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
160
161 match update {
162 proto::update_view::Variant::Editor(update) => match event {
163 Event::ScrollPositionChanged { .. } => {
164 update.scroll_top_anchor = Some(language::proto::serialize_anchor(
165 &self.scroll_top_anchor.text_anchor,
166 ));
167 update.scroll_x = self.scroll_position.x();
168 update.scroll_y = self.scroll_position.y();
169 true
170 }
171 Event::SelectionsChanged { .. } => {
172 update.selections = self
173 .selections
174 .disjoint_anchors()
175 .iter()
176 .chain(self.selections.pending_anchor().as_ref())
177 .map(serialize_selection)
178 .collect();
179 true
180 }
181 _ => false,
182 },
183 }
184 }
185
186 fn apply_update_proto(
187 &mut self,
188 message: update_view::Variant,
189 cx: &mut ViewContext<Self>,
190 ) -> Result<()> {
191 match message {
192 update_view::Variant::Editor(message) => {
193 let buffer = self.buffer.read(cx);
194 let buffer = buffer.read(cx);
195 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
196 let excerpt_id = excerpt_id.clone();
197 drop(buffer);
198
199 let selections = message
200 .selections
201 .into_iter()
202 .filter_map(|selection| {
203 deserialize_selection(&excerpt_id, buffer_id, selection)
204 })
205 .collect::<Vec<_>>();
206
207 if !selections.is_empty() {
208 self.set_selections_from_remote(selections, cx);
209 self.request_autoscroll_remotely(Autoscroll::newest(), cx);
210 } else if let Some(anchor) = message.scroll_top_anchor {
211 self.set_scroll_top_anchor(
212 Anchor {
213 buffer_id: Some(buffer_id),
214 excerpt_id,
215 text_anchor: language::proto::deserialize_anchor(anchor)
216 .ok_or_else(|| anyhow!("invalid scroll top"))?,
217 },
218 vec2f(message.scroll_x, message.scroll_y),
219 cx,
220 );
221 }
222 }
223 }
224 Ok(())
225 }
226
227 fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
228 match event {
229 Event::Edited => true,
230 Event::SelectionsChanged { local } => *local,
231 Event::ScrollPositionChanged { local } => *local,
232 _ => false,
233 }
234 }
235}
236
237fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
238 proto::Selection {
239 id: selection.id as u64,
240 start: Some(language::proto::serialize_anchor(
241 &selection.start.text_anchor,
242 )),
243 end: Some(language::proto::serialize_anchor(
244 &selection.end.text_anchor,
245 )),
246 reversed: selection.reversed,
247 }
248}
249
250fn deserialize_selection(
251 excerpt_id: &ExcerptId,
252 buffer_id: usize,
253 selection: proto::Selection,
254) -> Option<Selection<Anchor>> {
255 Some(Selection {
256 id: selection.id as usize,
257 start: Anchor {
258 buffer_id: Some(buffer_id),
259 excerpt_id: excerpt_id.clone(),
260 text_anchor: language::proto::deserialize_anchor(selection.start?)?,
261 },
262 end: Anchor {
263 buffer_id: Some(buffer_id),
264 excerpt_id: excerpt_id.clone(),
265 text_anchor: language::proto::deserialize_anchor(selection.end?)?,
266 },
267 reversed: selection.reversed,
268 goal: SelectionGoal::None,
269 })
270}
271
272impl Item for Editor {
273 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
274 if let Ok(data) = data.downcast::<NavigationData>() {
275 let newest_selection = self.selections.newest::<Point>(cx);
276 let buffer = self.buffer.read(cx).read(cx);
277 let offset = if buffer.can_resolve(&data.cursor_anchor) {
278 data.cursor_anchor.to_point(&buffer)
279 } else {
280 buffer.clip_point(data.cursor_position, Bias::Left)
281 };
282
283 let scroll_top_anchor = if buffer.can_resolve(&data.scroll_top_anchor) {
284 data.scroll_top_anchor
285 } else {
286 buffer.anchor_before(
287 buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
288 )
289 };
290
291 drop(buffer);
292
293 if newest_selection.head() == offset {
294 false
295 } else {
296 let nav_history = self.nav_history.take();
297 self.scroll_position = data.scroll_position;
298 self.scroll_top_anchor = scroll_top_anchor;
299 self.change_selections(Some(Autoscroll::fit()), cx, |s| {
300 s.select_ranges([offset..offset])
301 });
302 self.nav_history = nav_history;
303 true
304 }
305 } else {
306 false
307 }
308 }
309
310 fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
311 match path_for_buffer(&self.buffer, detail, true, cx)? {
312 Cow::Borrowed(path) => Some(path.to_string_lossy()),
313 Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
314 }
315 }
316
317 fn tab_content(
318 &self,
319 detail: Option<usize>,
320 style: &theme::Tab,
321 cx: &AppContext,
322 ) -> ElementBox {
323 Flex::row()
324 .with_child(
325 Label::new(self.title(cx).into(), style.label.clone())
326 .aligned()
327 .boxed(),
328 )
329 .with_children(detail.and_then(|detail| {
330 let path = path_for_buffer(&self.buffer, detail, false, cx)?;
331 let description = path.to_string_lossy();
332 Some(
333 Label::new(
334 if description.len() > MAX_TAB_TITLE_LEN {
335 description[..MAX_TAB_TITLE_LEN].to_string() + "…"
336 } else {
337 description.into()
338 },
339 style.description.text.clone(),
340 )
341 .contained()
342 .with_style(style.description.container)
343 .aligned()
344 .boxed(),
345 )
346 }))
347 .boxed()
348 }
349
350 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
351 let buffer = self.buffer.read(cx).as_singleton()?;
352 let file = buffer.read(cx).file();
353 File::from_dyn(file).map(|file| ProjectPath {
354 worktree_id: file.worktree_id(cx),
355 path: file.path().clone(),
356 })
357 }
358
359 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
360 self.buffer
361 .read(cx)
362 .files(cx)
363 .into_iter()
364 .filter_map(|file| File::from_dyn(Some(file))?.project_entry_id(cx))
365 .collect()
366 }
367
368 fn is_singleton(&self, cx: &AppContext) -> bool {
369 self.buffer.read(cx).is_singleton()
370 }
371
372 fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
373 where
374 Self: Sized,
375 {
376 Some(self.clone(cx))
377 }
378
379 fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
380 self.nav_history = Some(history);
381 }
382
383 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
384 let selection = self.selections.newest_anchor();
385 self.push_to_nav_history(selection.head(), None, cx);
386 }
387
388 fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
389 hide_link_definition(self, cx);
390 self.link_go_to_definition_state.last_mouse_location = None;
391 }
392
393 fn is_dirty(&self, cx: &AppContext) -> bool {
394 self.buffer().read(cx).read(cx).is_dirty()
395 }
396
397 fn has_conflict(&self, cx: &AppContext) -> bool {
398 self.buffer().read(cx).read(cx).has_conflict()
399 }
400
401 fn can_save(&self, cx: &AppContext) -> bool {
402 !self.buffer().read(cx).is_singleton() || self.project_path(cx).is_some()
403 }
404
405 fn save(
406 &mut self,
407 project: ModelHandle<Project>,
408 cx: &mut ViewContext<Self>,
409 ) -> Task<Result<()>> {
410 self.report_event("save editor", cx);
411
412 let buffer = self.buffer().clone();
413 let buffers = buffer.read(cx).all_buffers();
414 let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
415 let format = project.update(cx, |project, cx| {
416 project.format(buffers, true, FormatTrigger::Save, cx)
417 });
418 cx.spawn(|_, mut cx| async move {
419 let transaction = futures::select_biased! {
420 _ = timeout => {
421 log::warn!("timed out waiting for formatting");
422 None
423 }
424 transaction = format.log_err().fuse() => transaction,
425 };
426
427 buffer
428 .update(&mut cx, |buffer, cx| {
429 if let Some(transaction) = transaction {
430 if !buffer.is_singleton() {
431 buffer.push_transaction(&transaction.0);
432 }
433 }
434
435 buffer.save(cx)
436 })
437 .await?;
438 Ok(())
439 })
440 }
441
442 fn save_as(
443 &mut self,
444 project: ModelHandle<Project>,
445 abs_path: PathBuf,
446 cx: &mut ViewContext<Self>,
447 ) -> Task<Result<()>> {
448 let buffer = self
449 .buffer()
450 .read(cx)
451 .as_singleton()
452 .expect("cannot call save_as on an excerpt list");
453
454 project.update(cx, |project, cx| {
455 project.save_buffer_as(buffer, abs_path, cx)
456 })
457 }
458
459 fn reload(
460 &mut self,
461 project: ModelHandle<Project>,
462 cx: &mut ViewContext<Self>,
463 ) -> Task<Result<()>> {
464 let buffer = self.buffer().clone();
465 let buffers = self.buffer.read(cx).all_buffers();
466 let reload_buffers =
467 project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx));
468 cx.spawn(|this, mut cx| async move {
469 let transaction = reload_buffers.log_err().await;
470 this.update(&mut cx, |editor, cx| {
471 editor.request_autoscroll(Autoscroll::fit(), cx)
472 });
473 buffer.update(&mut cx, |buffer, _| {
474 if let Some(transaction) = transaction {
475 if !buffer.is_singleton() {
476 buffer.push_transaction(&transaction.0);
477 }
478 }
479 });
480 Ok(())
481 })
482 }
483
484 fn git_diff_recalc(
485 &mut self,
486 _project: ModelHandle<Project>,
487 cx: &mut ViewContext<Self>,
488 ) -> Task<Result<()>> {
489 self.buffer().update(cx, |multibuffer, cx| {
490 multibuffer.git_diff_recalc(cx);
491 });
492 Task::ready(Ok(()))
493 }
494
495 fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
496 let mut result = Vec::new();
497 match event {
498 Event::Closed => result.push(ItemEvent::CloseItem),
499 Event::Saved | Event::TitleChanged => {
500 result.push(ItemEvent::UpdateTab);
501 result.push(ItemEvent::UpdateBreadcrumbs);
502 }
503 Event::Reparsed => {
504 result.push(ItemEvent::UpdateBreadcrumbs);
505 }
506 Event::SelectionsChanged { local } if *local => {
507 result.push(ItemEvent::UpdateBreadcrumbs);
508 }
509 Event::DirtyChanged => {
510 result.push(ItemEvent::UpdateTab);
511 }
512 Event::BufferEdited => {
513 result.push(ItemEvent::Edit);
514 result.push(ItemEvent::UpdateBreadcrumbs);
515 }
516 _ => {}
517 }
518 result
519 }
520
521 fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
522 Some(Box::new(handle.clone()))
523 }
524
525 fn breadcrumb_location(&self) -> ToolbarItemLocation {
526 ToolbarItemLocation::PrimaryLeft { flex: None }
527 }
528
529 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
530 let cursor = self.selections.newest_anchor().head();
531 let multibuffer = &self.buffer().read(cx);
532 let (buffer_id, symbols) =
533 multibuffer.symbols_containing(cursor, Some(&theme.editor.syntax), cx)?;
534 let buffer = multibuffer.buffer(buffer_id)?;
535
536 let buffer = buffer.read(cx);
537 let filename = buffer
538 .snapshot()
539 .resolve_file_path(
540 cx,
541 self.project
542 .as_ref()
543 .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
544 .unwrap_or_default(),
545 )
546 .map(|path| path.to_string_lossy().to_string())
547 .unwrap_or_else(|| "untitled".to_string());
548
549 let mut breadcrumbs = vec![Label::new(filename, theme.breadcrumbs.text.clone()).boxed()];
550 breadcrumbs.extend(symbols.into_iter().map(|symbol| {
551 Text::new(symbol.text, theme.breadcrumbs.text.clone())
552 .with_highlights(symbol.highlight_ranges)
553 .boxed()
554 }));
555 Some(breadcrumbs)
556 }
557}
558
559impl ProjectItem for Editor {
560 type Item = Buffer;
561
562 fn for_project_item(
563 project: ModelHandle<Project>,
564 buffer: ModelHandle<Buffer>,
565 cx: &mut ViewContext<Self>,
566 ) -> Self {
567 Self::for_buffer(buffer, Some(project), cx)
568 }
569}
570
571enum BufferSearchHighlights {}
572impl SearchableItem for Editor {
573 type Match = Range<Anchor>;
574
575 fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
576 match event {
577 Event::BufferEdited => Some(SearchEvent::MatchesInvalidated),
578 Event::SelectionsChanged { .. } => Some(SearchEvent::ActiveMatchChanged),
579 _ => None,
580 }
581 }
582
583 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
584 self.clear_background_highlights::<BufferSearchHighlights>(cx);
585 }
586
587 fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
588 self.highlight_background::<BufferSearchHighlights>(
589 matches,
590 |theme| theme.search.match_background,
591 cx,
592 );
593 }
594
595 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
596 let display_map = self.snapshot(cx).display_snapshot;
597 let selection = self.selections.newest::<usize>(cx);
598 if selection.start == selection.end {
599 let point = selection.start.to_display_point(&display_map);
600 let range = surrounding_word(&display_map, point);
601 let range = range.start.to_offset(&display_map, Bias::Left)
602 ..range.end.to_offset(&display_map, Bias::Right);
603 let text: String = display_map.buffer_snapshot.text_for_range(range).collect();
604 if text.trim().is_empty() {
605 String::new()
606 } else {
607 text
608 }
609 } else {
610 display_map
611 .buffer_snapshot
612 .text_for_range(selection.start..selection.end)
613 .collect()
614 }
615 }
616
617 fn activate_match(
618 &mut self,
619 index: usize,
620 matches: Vec<Range<Anchor>>,
621 cx: &mut ViewContext<Self>,
622 ) {
623 self.unfold_ranges([matches[index].clone()], false, cx);
624 self.change_selections(Some(Autoscroll::fit()), cx, |s| {
625 s.select_ranges([matches[index].clone()])
626 });
627 }
628
629 fn match_index_for_direction(
630 &mut self,
631 matches: &Vec<Range<Anchor>>,
632 mut current_index: usize,
633 direction: Direction,
634 cx: &mut ViewContext<Self>,
635 ) -> usize {
636 let buffer = self.buffer().read(cx).snapshot(cx);
637 let cursor = self.selections.newest_anchor().head();
638 if matches[current_index].start.cmp(&cursor, &buffer).is_gt() {
639 if direction == Direction::Prev {
640 if current_index == 0 {
641 current_index = matches.len() - 1;
642 } else {
643 current_index -= 1;
644 }
645 }
646 } else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() {
647 if direction == Direction::Next {
648 current_index = 0;
649 }
650 } else if direction == Direction::Prev {
651 if current_index == 0 {
652 current_index = matches.len() - 1;
653 } else {
654 current_index -= 1;
655 }
656 } else if direction == Direction::Next {
657 if current_index == matches.len() - 1 {
658 current_index = 0
659 } else {
660 current_index += 1;
661 }
662 };
663 current_index
664 }
665
666 fn find_matches(
667 &mut self,
668 query: project::search::SearchQuery,
669 cx: &mut ViewContext<Self>,
670 ) -> Task<Vec<Range<Anchor>>> {
671 let buffer = self.buffer().read(cx).snapshot(cx);
672 cx.background().spawn(async move {
673 let mut ranges = Vec::new();
674 if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
675 ranges.extend(
676 query
677 .search(excerpt_buffer.as_rope())
678 .await
679 .into_iter()
680 .map(|range| {
681 buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
682 }),
683 );
684 } else {
685 for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
686 let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
687 let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone());
688 ranges.extend(query.search(&rope).await.into_iter().map(|range| {
689 let start = excerpt
690 .buffer
691 .anchor_after(excerpt_range.start + range.start);
692 let end = excerpt
693 .buffer
694 .anchor_before(excerpt_range.start + range.end);
695 buffer.anchor_in_excerpt(excerpt.id.clone(), start)
696 ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
697 }));
698 }
699 }
700 ranges
701 })
702 }
703
704 fn active_match_index(
705 &mut self,
706 matches: Vec<Range<Anchor>>,
707 cx: &mut ViewContext<Self>,
708 ) -> Option<usize> {
709 active_match_index(
710 &matches,
711 &self.selections.newest_anchor().head(),
712 &self.buffer().read(cx).snapshot(cx),
713 )
714 }
715}
716
717pub fn active_match_index(
718 ranges: &[Range<Anchor>],
719 cursor: &Anchor,
720 buffer: &MultiBufferSnapshot,
721) -> Option<usize> {
722 if ranges.is_empty() {
723 None
724 } else {
725 match ranges.binary_search_by(|probe| {
726 if probe.end.cmp(cursor, &*buffer).is_lt() {
727 Ordering::Less
728 } else if probe.start.cmp(cursor, &*buffer).is_gt() {
729 Ordering::Greater
730 } else {
731 Ordering::Equal
732 }
733 }) {
734 Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
735 }
736 }
737}
738
739pub struct CursorPosition {
740 position: Option<Point>,
741 selected_count: usize,
742 _observe_active_editor: Option<Subscription>,
743}
744
745impl Default for CursorPosition {
746 fn default() -> Self {
747 Self::new()
748 }
749}
750
751impl CursorPosition {
752 pub fn new() -> Self {
753 Self {
754 position: None,
755 selected_count: 0,
756 _observe_active_editor: None,
757 }
758 }
759
760 fn update_position(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
761 let editor = editor.read(cx);
762 let buffer = editor.buffer().read(cx).snapshot(cx);
763
764 self.selected_count = 0;
765 let mut last_selection: Option<Selection<usize>> = None;
766 for selection in editor.selections.all::<usize>(cx) {
767 self.selected_count += selection.end - selection.start;
768 if last_selection
769 .as_ref()
770 .map_or(true, |last_selection| selection.id > last_selection.id)
771 {
772 last_selection = Some(selection);
773 }
774 }
775 self.position = last_selection.map(|s| s.head().to_point(&buffer));
776
777 cx.notify();
778 }
779}
780
781impl Entity for CursorPosition {
782 type Event = ();
783}
784
785impl View for CursorPosition {
786 fn ui_name() -> &'static str {
787 "CursorPosition"
788 }
789
790 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
791 if let Some(position) = self.position {
792 let theme = &cx.global::<Settings>().theme.workspace.status_bar;
793 let mut text = format!("{},{}", position.row + 1, position.column + 1);
794 if self.selected_count > 0 {
795 write!(text, " ({} selected)", self.selected_count).unwrap();
796 }
797 Label::new(text, theme.cursor_position.clone()).boxed()
798 } else {
799 Empty::new().boxed()
800 }
801 }
802}
803
804impl StatusItemView for CursorPosition {
805 fn set_active_pane_item(
806 &mut self,
807 active_pane_item: Option<&dyn ItemHandle>,
808 cx: &mut ViewContext<Self>,
809 ) {
810 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
811 self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
812 self.update_position(editor, cx);
813 } else {
814 self.position = None;
815 self._observe_active_editor = None;
816 }
817
818 cx.notify();
819 }
820}
821
822fn path_for_buffer<'a>(
823 buffer: &ModelHandle<MultiBuffer>,
824 height: usize,
825 include_filename: bool,
826 cx: &'a AppContext,
827) -> Option<Cow<'a, Path>> {
828 let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
829 path_for_file(file, height, include_filename, cx)
830}
831
832fn path_for_file<'a>(
833 file: &'a dyn language::File,
834 mut height: usize,
835 include_filename: bool,
836 cx: &'a AppContext,
837) -> Option<Cow<'a, Path>> {
838 // Ensure we always render at least the filename.
839 height += 1;
840
841 let mut prefix = file.path().as_ref();
842 while height > 0 {
843 if let Some(parent) = prefix.parent() {
844 prefix = parent;
845 height -= 1;
846 } else {
847 break;
848 }
849 }
850
851 // Here we could have just always used `full_path`, but that is very
852 // allocation-heavy and so we try to use a `Cow<Path>` if we haven't
853 // traversed all the way up to the worktree's root.
854 if height > 0 {
855 let full_path = file.full_path(cx);
856 if include_filename {
857 Some(full_path.into())
858 } else {
859 Some(full_path.parent()?.to_path_buf().into())
860 }
861 } else {
862 let mut path = file.path().strip_prefix(prefix).ok()?;
863 if !include_filename {
864 path = path.parent()?;
865 }
866 Some(path.into())
867 }
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873 use gpui::MutableAppContext;
874 use std::{
875 path::{Path, PathBuf},
876 sync::Arc,
877 };
878
879 #[gpui::test]
880 fn test_path_for_file(cx: &mut MutableAppContext) {
881 let file = TestFile {
882 path: Path::new("").into(),
883 full_path: PathBuf::from(""),
884 };
885 assert_eq!(path_for_file(&file, 0, false, cx), None);
886 }
887
888 struct TestFile {
889 path: Arc<Path>,
890 full_path: PathBuf,
891 }
892
893 impl language::File for TestFile {
894 fn path(&self) -> &Arc<Path> {
895 &self.path
896 }
897
898 fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
899 self.full_path.clone()
900 }
901
902 fn as_local(&self) -> Option<&dyn language::LocalFile> {
903 todo!()
904 }
905
906 fn mtime(&self) -> std::time::SystemTime {
907 todo!()
908 }
909
910 fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
911 todo!()
912 }
913
914 fn is_deleted(&self) -> bool {
915 todo!()
916 }
917
918 fn save(
919 &self,
920 _: u64,
921 _: language::Rope,
922 _: clock::Global,
923 _: project::LineEnding,
924 _: &mut MutableAppContext,
925 ) -> gpui::Task<anyhow::Result<(clock::Global, String, std::time::SystemTime)>> {
926 todo!()
927 }
928
929 fn as_any(&self) -> &dyn std::any::Any {
930 todo!()
931 }
932
933 fn to_proto(&self) -> rpc::proto::File {
934 todo!()
935 }
936 }
937}