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