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