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