1use crate::{
2 editor_settings::SeedQuerySetting,
3 persistence::{SerializedEditor, DB},
4 scroll::ScrollAnchor,
5 Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget,
6 MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, ToPoint as _,
7};
8use anyhow::{anyhow, Context as _, Result};
9use collections::HashSet;
10use file_icons::FileIcons;
11use futures::future::try_join_all;
12use git::status::GitSummary;
13use gpui::{
14 point, AnyElement, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
15 IntoElement, ParentElement, Pixels, SharedString, Styled, Task, WeakEntity, Window,
16};
17use language::{
18 proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, DiskState, Point,
19 SelectionGoal,
20};
21use lsp::DiagnosticSeverity;
22use project::{
23 lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Project,
24 ProjectItem as _, ProjectPath,
25};
26use rpc::proto::{self, update_view, PeerId};
27use settings::Settings;
28use std::{
29 any::TypeId,
30 borrow::Cow,
31 cmp::{self, Ordering},
32 iter,
33 ops::Range,
34 path::Path,
35 sync::Arc,
36};
37use text::{BufferId, Selection};
38use theme::{Theme, ThemeSettings};
39use ui::{h_flex, prelude::*, IconDecorationKind, Label};
40use util::{paths::PathExt, ResultExt, TryFutureExt};
41use workspace::item::{BreadcrumbText, FollowEvent};
42use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
43use workspace::{
44 item::{FollowableItem, Item, ItemEvent, ProjectItem},
45 searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
46 ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
47};
48
49pub const MAX_TAB_TITLE_LEN: usize = 24;
50
51impl FollowableItem for Editor {
52 fn remote_id(&self) -> Option<ViewId> {
53 self.remote_id
54 }
55
56 fn from_state_proto(
57 workspace: Entity<Workspace>,
58 remote_id: ViewId,
59 state: &mut Option<proto::view::Variant>,
60 window: &mut Window,
61 cx: &mut App,
62 ) -> Option<Task<Result<Entity<Self>>>> {
63 let project = workspace.read(cx).project().to_owned();
64 let Some(proto::view::Variant::Editor(_)) = state else {
65 return None;
66 };
67 let Some(proto::view::Variant::Editor(state)) = state.take() else {
68 unreachable!()
69 };
70
71 let buffer_ids = state
72 .excerpts
73 .iter()
74 .map(|excerpt| excerpt.buffer_id)
75 .collect::<HashSet<_>>();
76 let buffers = project.update(cx, |project, cx| {
77 buffer_ids
78 .iter()
79 .map(|id| BufferId::new(*id).map(|id| project.open_buffer_by_id(id, cx)))
80 .collect::<Result<Vec<_>>>()
81 });
82
83 Some(window.spawn(cx, |mut cx| async move {
84 let mut buffers = futures::future::try_join_all(buffers?)
85 .await
86 .debug_assert_ok("leaders don't share views for unshared buffers")?;
87
88 let editor = cx.update(|window, cx| {
89 let multibuffer = cx.new(|cx| {
90 let mut multibuffer;
91 if state.singleton && buffers.len() == 1 {
92 multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
93 } else {
94 multibuffer = MultiBuffer::new(project.read(cx).capability());
95 let mut excerpts = state.excerpts.into_iter().peekable();
96 while let Some(excerpt) = excerpts.peek() {
97 let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
98 continue;
99 };
100 let buffer_excerpts = iter::from_fn(|| {
101 let excerpt = excerpts.peek()?;
102 (excerpt.buffer_id == u64::from(buffer_id))
103 .then(|| excerpts.next().unwrap())
104 });
105 let buffer =
106 buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
107 if let Some(buffer) = buffer {
108 multibuffer.push_excerpts(
109 buffer.clone(),
110 buffer_excerpts.filter_map(deserialize_excerpt_range),
111 cx,
112 );
113 }
114 }
115 };
116
117 if let Some(title) = &state.title {
118 multibuffer = multibuffer.with_title(title.clone())
119 }
120
121 multibuffer
122 });
123
124 cx.new(|cx| {
125 let mut editor = Editor::for_multibuffer(
126 multibuffer,
127 Some(project.clone()),
128 true,
129 window,
130 cx,
131 );
132 editor.remote_id = Some(remote_id);
133 editor
134 })
135 })?;
136
137 update_editor_from_message(
138 editor.downgrade(),
139 project,
140 proto::update_view::Editor {
141 selections: state.selections,
142 pending_selection: state.pending_selection,
143 scroll_top_anchor: state.scroll_top_anchor,
144 scroll_x: state.scroll_x,
145 scroll_y: state.scroll_y,
146 ..Default::default()
147 },
148 &mut cx,
149 )
150 .await?;
151
152 Ok(editor)
153 }))
154 }
155
156 fn set_leader_peer_id(
157 &mut self,
158 leader_peer_id: Option<PeerId>,
159 window: &mut Window,
160 cx: &mut Context<Self>,
161 ) {
162 self.leader_peer_id = leader_peer_id;
163 if self.leader_peer_id.is_some() {
164 self.buffer.update(cx, |buffer, cx| {
165 buffer.remove_active_selections(cx);
166 });
167 } else if self.focus_handle.is_focused(window) {
168 self.buffer.update(cx, |buffer, cx| {
169 buffer.set_active_selections(
170 &self.selections.disjoint_anchors(),
171 self.selections.line_mode,
172 self.cursor_shape,
173 cx,
174 );
175 });
176 }
177 cx.notify();
178 }
179
180 fn to_state_proto(&self, _: &Window, cx: &App) -> Option<proto::view::Variant> {
181 let buffer = self.buffer.read(cx);
182 if buffer
183 .as_singleton()
184 .and_then(|buffer| buffer.read(cx).file())
185 .map_or(false, |file| file.is_private())
186 {
187 return None;
188 }
189
190 let scroll_anchor = self.scroll_manager.anchor();
191 let excerpts = buffer
192 .read(cx)
193 .excerpts()
194 .map(|(id, buffer, range)| proto::Excerpt {
195 id: id.to_proto(),
196 buffer_id: buffer.remote_id().into(),
197 context_start: Some(serialize_text_anchor(&range.context.start)),
198 context_end: Some(serialize_text_anchor(&range.context.end)),
199 primary_start: range
200 .primary
201 .as_ref()
202 .map(|range| serialize_text_anchor(&range.start)),
203 primary_end: range
204 .primary
205 .as_ref()
206 .map(|range| serialize_text_anchor(&range.end)),
207 })
208 .collect();
209
210 Some(proto::view::Variant::Editor(proto::view::Editor {
211 singleton: buffer.is_singleton(),
212 title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
213 excerpts,
214 scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
215 scroll_x: scroll_anchor.offset.x,
216 scroll_y: scroll_anchor.offset.y,
217 selections: self
218 .selections
219 .disjoint_anchors()
220 .iter()
221 .map(serialize_selection)
222 .collect(),
223 pending_selection: self
224 .selections
225 .pending_anchor()
226 .as_ref()
227 .map(serialize_selection),
228 }))
229 }
230
231 fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
232 match event {
233 EditorEvent::Edited { .. } => Some(FollowEvent::Unfollow),
234 EditorEvent::SelectionsChanged { local }
235 | EditorEvent::ScrollPositionChanged { local, .. } => {
236 if *local {
237 Some(FollowEvent::Unfollow)
238 } else {
239 None
240 }
241 }
242 _ => None,
243 }
244 }
245
246 fn add_event_to_update_proto(
247 &self,
248 event: &EditorEvent,
249 update: &mut Option<proto::update_view::Variant>,
250 _: &Window,
251 cx: &App,
252 ) -> bool {
253 let update =
254 update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
255
256 match update {
257 proto::update_view::Variant::Editor(update) => match event {
258 EditorEvent::ExcerptsAdded {
259 buffer,
260 predecessor,
261 excerpts,
262 } => {
263 let buffer_id = buffer.read(cx).remote_id();
264 let mut excerpts = excerpts.iter();
265 if let Some((id, range)) = excerpts.next() {
266 update.inserted_excerpts.push(proto::ExcerptInsertion {
267 previous_excerpt_id: Some(predecessor.to_proto()),
268 excerpt: serialize_excerpt(buffer_id, id, range),
269 });
270 update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
271 proto::ExcerptInsertion {
272 previous_excerpt_id: None,
273 excerpt: serialize_excerpt(buffer_id, id, range),
274 }
275 }))
276 }
277 true
278 }
279 EditorEvent::ExcerptsRemoved { ids } => {
280 update
281 .deleted_excerpts
282 .extend(ids.iter().map(ExcerptId::to_proto));
283 true
284 }
285 EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => {
286 let scroll_anchor = self.scroll_manager.anchor();
287 update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
288 update.scroll_x = scroll_anchor.offset.x;
289 update.scroll_y = scroll_anchor.offset.y;
290 true
291 }
292 EditorEvent::SelectionsChanged { .. } => {
293 update.selections = self
294 .selections
295 .disjoint_anchors()
296 .iter()
297 .map(serialize_selection)
298 .collect();
299 update.pending_selection = self
300 .selections
301 .pending_anchor()
302 .as_ref()
303 .map(serialize_selection);
304 true
305 }
306 _ => false,
307 },
308 }
309 }
310
311 fn apply_update_proto(
312 &mut self,
313 project: &Entity<Project>,
314 message: update_view::Variant,
315 window: &mut Window,
316 cx: &mut Context<Self>,
317 ) -> Task<Result<()>> {
318 let update_view::Variant::Editor(message) = message;
319 let project = project.clone();
320 cx.spawn_in(window, |this, mut cx| async move {
321 update_editor_from_message(this, project, message, &mut cx).await
322 })
323 }
324
325 fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
326 true
327 }
328
329 fn dedup(&self, existing: &Self, _: &Window, cx: &App) -> Option<Dedup> {
330 let self_singleton = self.buffer.read(cx).as_singleton()?;
331 let other_singleton = existing.buffer.read(cx).as_singleton()?;
332 if self_singleton == other_singleton {
333 Some(Dedup::KeepExisting)
334 } else {
335 None
336 }
337 }
338}
339
340async fn update_editor_from_message(
341 this: WeakEntity<Editor>,
342 project: Entity<Project>,
343 message: proto::update_view::Editor,
344 cx: &mut AsyncWindowContext,
345) -> Result<()> {
346 // Open all of the buffers of which excerpts were added to the editor.
347 let inserted_excerpt_buffer_ids = message
348 .inserted_excerpts
349 .iter()
350 .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
351 .collect::<HashSet<_>>();
352 let inserted_excerpt_buffers = project.update(cx, |project, cx| {
353 inserted_excerpt_buffer_ids
354 .into_iter()
355 .map(|id| BufferId::new(id).map(|id| project.open_buffer_by_id(id, cx)))
356 .collect::<Result<Vec<_>>>()
357 })??;
358 let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
359
360 // Update the editor's excerpts.
361 this.update(cx, |editor, cx| {
362 editor.buffer.update(cx, |multibuffer, cx| {
363 let mut removed_excerpt_ids = message
364 .deleted_excerpts
365 .into_iter()
366 .map(ExcerptId::from_proto)
367 .collect::<Vec<_>>();
368 removed_excerpt_ids.sort_by({
369 let multibuffer = multibuffer.read(cx);
370 move |a, b| a.cmp(b, &multibuffer)
371 });
372
373 let mut insertions = message.inserted_excerpts.into_iter().peekable();
374 while let Some(insertion) = insertions.next() {
375 let Some(excerpt) = insertion.excerpt else {
376 continue;
377 };
378 let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
379 continue;
380 };
381 let buffer_id = BufferId::new(excerpt.buffer_id)?;
382 let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
383 continue;
384 };
385
386 let adjacent_excerpts = iter::from_fn(|| {
387 let insertion = insertions.peek()?;
388 if insertion.previous_excerpt_id.is_none()
389 && insertion.excerpt.as_ref()?.buffer_id == u64::from(buffer_id)
390 {
391 insertions.next()?.excerpt
392 } else {
393 None
394 }
395 });
396
397 multibuffer.insert_excerpts_with_ids_after(
398 ExcerptId::from_proto(previous_excerpt_id),
399 buffer,
400 [excerpt]
401 .into_iter()
402 .chain(adjacent_excerpts)
403 .filter_map(|excerpt| {
404 Some((
405 ExcerptId::from_proto(excerpt.id),
406 deserialize_excerpt_range(excerpt)?,
407 ))
408 }),
409 cx,
410 );
411 }
412
413 multibuffer.remove_excerpts(removed_excerpt_ids, cx);
414 Result::<(), anyhow::Error>::Ok(())
415 })
416 })??;
417
418 // Deserialize the editor state.
419 let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
420 let buffer = editor.buffer.read(cx).read(cx);
421 let selections = message
422 .selections
423 .into_iter()
424 .filter_map(|selection| deserialize_selection(&buffer, selection))
425 .collect::<Vec<_>>();
426 let pending_selection = message
427 .pending_selection
428 .and_then(|selection| deserialize_selection(&buffer, selection));
429 let scroll_top_anchor = message
430 .scroll_top_anchor
431 .and_then(|anchor| deserialize_anchor(&buffer, anchor));
432 anyhow::Ok((selections, pending_selection, scroll_top_anchor))
433 })??;
434
435 // Wait until the buffer has received all of the operations referenced by
436 // the editor's new state.
437 this.update(cx, |editor, cx| {
438 editor.buffer.update(cx, |buffer, cx| {
439 buffer.wait_for_anchors(
440 selections
441 .iter()
442 .chain(pending_selection.as_ref())
443 .flat_map(|selection| [selection.start, selection.end])
444 .chain(scroll_top_anchor),
445 cx,
446 )
447 })
448 })?
449 .await?;
450
451 // Update the editor's state.
452 this.update_in(cx, |editor, window, cx| {
453 if !selections.is_empty() || pending_selection.is_some() {
454 editor.set_selections_from_remote(selections, pending_selection, window, cx);
455 editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
456 } else if let Some(scroll_top_anchor) = scroll_top_anchor {
457 editor.set_scroll_anchor_remote(
458 ScrollAnchor {
459 anchor: scroll_top_anchor,
460 offset: point(message.scroll_x, message.scroll_y),
461 },
462 window,
463 cx,
464 );
465 }
466 })?;
467 Ok(())
468}
469
470fn serialize_excerpt(
471 buffer_id: BufferId,
472 id: &ExcerptId,
473 range: &ExcerptRange<language::Anchor>,
474) -> Option<proto::Excerpt> {
475 Some(proto::Excerpt {
476 id: id.to_proto(),
477 buffer_id: buffer_id.into(),
478 context_start: Some(serialize_text_anchor(&range.context.start)),
479 context_end: Some(serialize_text_anchor(&range.context.end)),
480 primary_start: range
481 .primary
482 .as_ref()
483 .map(|r| serialize_text_anchor(&r.start)),
484 primary_end: range
485 .primary
486 .as_ref()
487 .map(|r| serialize_text_anchor(&r.end)),
488 })
489}
490
491fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
492 proto::Selection {
493 id: selection.id as u64,
494 start: Some(serialize_anchor(&selection.start)),
495 end: Some(serialize_anchor(&selection.end)),
496 reversed: selection.reversed,
497 }
498}
499
500fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor {
501 proto::EditorAnchor {
502 excerpt_id: anchor.excerpt_id.to_proto(),
503 anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
504 }
505}
506
507fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<language::Anchor>> {
508 let context = {
509 let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
510 let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
511 start..end
512 };
513 let primary = excerpt
514 .primary_start
515 .zip(excerpt.primary_end)
516 .and_then(|(start, end)| {
517 let start = language::proto::deserialize_anchor(start)?;
518 let end = language::proto::deserialize_anchor(end)?;
519 Some(start..end)
520 });
521 Some(ExcerptRange { context, primary })
522}
523
524fn deserialize_selection(
525 buffer: &MultiBufferSnapshot,
526 selection: proto::Selection,
527) -> Option<Selection<Anchor>> {
528 Some(Selection {
529 id: selection.id as usize,
530 start: deserialize_anchor(buffer, selection.start?)?,
531 end: deserialize_anchor(buffer, selection.end?)?,
532 reversed: selection.reversed,
533 goal: SelectionGoal::None,
534 })
535}
536
537fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option<Anchor> {
538 let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id);
539 Some(Anchor {
540 excerpt_id,
541 text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
542 buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
543 diff_base_anchor: None,
544 })
545}
546
547impl Item for Editor {
548 type Event = EditorEvent;
549
550 fn navigate(
551 &mut self,
552 data: Box<dyn std::any::Any>,
553 window: &mut Window,
554 cx: &mut Context<Self>,
555 ) -> bool {
556 if let Ok(data) = data.downcast::<NavigationData>() {
557 let newest_selection = self.selections.newest::<Point>(cx);
558 let buffer = self.buffer.read(cx).read(cx);
559 let offset = if buffer.can_resolve(&data.cursor_anchor) {
560 data.cursor_anchor.to_point(&buffer)
561 } else {
562 buffer.clip_point(data.cursor_position, Bias::Left)
563 };
564
565 let mut scroll_anchor = data.scroll_anchor;
566 if !buffer.can_resolve(&scroll_anchor.anchor) {
567 scroll_anchor.anchor = buffer.anchor_before(
568 buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
569 );
570 }
571
572 drop(buffer);
573
574 if newest_selection.head() == offset {
575 false
576 } else {
577 let nav_history = self.nav_history.take();
578 self.set_scroll_anchor(scroll_anchor, window, cx);
579 self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
580 s.select_ranges([offset..offset])
581 });
582 self.nav_history = nav_history;
583 true
584 }
585 } else {
586 false
587 }
588 }
589
590 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
591 let file_path = self
592 .buffer()
593 .read(cx)
594 .as_singleton()?
595 .read(cx)
596 .file()
597 .and_then(|f| f.as_local())?
598 .abs_path(cx);
599
600 let file_path = file_path.compact().to_string_lossy().to_string();
601
602 Some(file_path.into())
603 }
604
605 fn telemetry_event_text(&self) -> Option<&'static str> {
606 None
607 }
608
609 fn tab_description(&self, detail: usize, cx: &App) -> Option<SharedString> {
610 let path = path_for_buffer(&self.buffer, detail, true, cx)?;
611 Some(path.to_string_lossy().to_string().into())
612 }
613
614 fn tab_icon(&self, _: &Window, cx: &App) -> Option<Icon> {
615 ItemSettings::get_global(cx)
616 .file_icons
617 .then(|| {
618 self.buffer
619 .read(cx)
620 .as_singleton()
621 .and_then(|buffer| buffer.read(cx).project_path(cx))
622 .and_then(|path| FileIcons::get_icon(path.path.as_ref(), cx))
623 })
624 .flatten()
625 .map(Icon::from_path)
626 }
627
628 fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> AnyElement {
629 let label_color = if ItemSettings::get_global(cx).git_status {
630 self.buffer()
631 .read(cx)
632 .as_singleton()
633 .and_then(|buffer| buffer.read(cx).project_path(cx))
634 .and_then(|path| {
635 let project = self.project.as_ref()?.read(cx);
636 let entry = project.entry_for_path(&path, cx)?;
637 let git_status = project
638 .worktree_for_id(path.worktree_id, cx)?
639 .read(cx)
640 .snapshot()
641 .status_for_file(path.path)?;
642
643 Some(entry_git_aware_label_color(
644 git_status.summary(),
645 entry.is_ignored,
646 params.selected,
647 ))
648 })
649 .unwrap_or_else(|| entry_label_color(params.selected))
650 } else {
651 entry_label_color(params.selected)
652 };
653
654 let description = params.detail.and_then(|detail| {
655 let path = path_for_buffer(&self.buffer, detail, false, cx)?;
656 let description = path.to_string_lossy();
657 let description = description.trim();
658
659 if description.is_empty() {
660 return None;
661 }
662
663 Some(util::truncate_and_trailoff(description, MAX_TAB_TITLE_LEN))
664 });
665
666 // Whether the file was saved in the past but is now deleted.
667 let was_deleted: bool = self
668 .buffer()
669 .read(cx)
670 .as_singleton()
671 .and_then(|buffer| buffer.read(cx).file())
672 .map_or(false, |file| file.disk_state() == DiskState::Deleted);
673
674 h_flex()
675 .gap_2()
676 .child(
677 Label::new(self.title(cx).to_string())
678 .color(label_color)
679 .italic(params.preview)
680 .strikethrough(was_deleted),
681 )
682 .when_some(description, |this, description| {
683 this.child(
684 Label::new(description)
685 .size(LabelSize::XSmall)
686 .color(Color::Muted),
687 )
688 })
689 .into_any_element()
690 }
691
692 fn for_each_project_item(
693 &self,
694 cx: &App,
695 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
696 ) {
697 self.buffer
698 .read(cx)
699 .for_each_buffer(|buffer| f(buffer.entity_id(), buffer.read(cx)));
700 }
701
702 fn is_singleton(&self, cx: &App) -> bool {
703 self.buffer.read(cx).is_singleton()
704 }
705
706 fn clone_on_split(
707 &self,
708 _workspace_id: Option<WorkspaceId>,
709 window: &mut Window,
710 cx: &mut Context<Self>,
711 ) -> Option<Entity<Editor>>
712 where
713 Self: Sized,
714 {
715 Some(cx.new(|cx| self.clone(window, cx)))
716 }
717
718 fn set_nav_history(
719 &mut self,
720 history: ItemNavHistory,
721 _window: &mut Window,
722 _: &mut Context<Self>,
723 ) {
724 self.nav_history = Some(history);
725 }
726
727 fn discarded(&self, _project: Entity<Project>, _: &mut Window, cx: &mut Context<Self>) {
728 for buffer in self.buffer().clone().read(cx).all_buffers() {
729 buffer.update(cx, |buffer, cx| buffer.discarded(cx))
730 }
731 }
732
733 fn deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
734 let selection = self.selections.newest_anchor();
735 self.push_to_nav_history(selection.head(), None, cx);
736 }
737
738 fn workspace_deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
739 self.hide_hovered_link(cx);
740 }
741
742 fn is_dirty(&self, cx: &App) -> bool {
743 self.buffer().read(cx).read(cx).is_dirty()
744 }
745
746 fn has_deleted_file(&self, cx: &App) -> bool {
747 self.buffer().read(cx).read(cx).has_deleted_file()
748 }
749
750 fn has_conflict(&self, cx: &App) -> bool {
751 self.buffer().read(cx).read(cx).has_conflict()
752 }
753
754 fn can_save(&self, cx: &App) -> bool {
755 let buffer = &self.buffer().read(cx);
756 if let Some(buffer) = buffer.as_singleton() {
757 buffer.read(cx).project_path(cx).is_some()
758 } else {
759 true
760 }
761 }
762
763 fn save(
764 &mut self,
765 format: bool,
766 project: Entity<Project>,
767 window: &mut Window,
768 cx: &mut Context<Self>,
769 ) -> Task<Result<()>> {
770 self.report_editor_event("Editor Saved", None, cx);
771 let buffers = self.buffer().clone().read(cx).all_buffers();
772 let buffers = buffers
773 .into_iter()
774 .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))
775 .collect::<HashSet<_>>();
776 cx.spawn_in(window, |this, mut cx| async move {
777 if format {
778 this.update_in(&mut cx, |editor, window, cx| {
779 editor.perform_format(
780 project.clone(),
781 FormatTrigger::Save,
782 FormatTarget::Buffers,
783 window,
784 cx,
785 )
786 })?
787 .await?;
788 }
789
790 if buffers.len() == 1 {
791 // Apply full save routine for singleton buffers, to allow to `touch` the file via the editor.
792 project
793 .update(&mut cx, |project, cx| project.save_buffers(buffers, cx))?
794 .await?;
795 } else {
796 // For multi-buffers, only format and save the buffers with changes.
797 // For clean buffers, we simulate saving by calling `Buffer::did_save`,
798 // so that language servers or other downstream listeners of save events get notified.
799 let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
800 buffer
801 .update(&mut cx, |buffer, _| {
802 buffer.is_dirty() || buffer.has_conflict()
803 })
804 .unwrap_or(false)
805 });
806
807 project
808 .update(&mut cx, |project, cx| {
809 project.save_buffers(dirty_buffers, cx)
810 })?
811 .await?;
812 for buffer in clean_buffers {
813 buffer
814 .update(&mut cx, |buffer, cx| {
815 let version = buffer.saved_version().clone();
816 let mtime = buffer.saved_mtime();
817 buffer.did_save(version, mtime, cx);
818 })
819 .ok();
820 }
821 }
822
823 Ok(())
824 })
825 }
826
827 fn save_as(
828 &mut self,
829 project: Entity<Project>,
830 path: ProjectPath,
831 _: &mut Window,
832 cx: &mut Context<Self>,
833 ) -> Task<Result<()>> {
834 let buffer = self
835 .buffer()
836 .read(cx)
837 .as_singleton()
838 .expect("cannot call save_as on an excerpt list");
839
840 let file_extension = path
841 .path
842 .extension()
843 .map(|a| a.to_string_lossy().to_string());
844 self.report_editor_event("Editor Saved", file_extension, cx);
845
846 project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
847 }
848
849 fn reload(
850 &mut self,
851 project: Entity<Project>,
852 window: &mut Window,
853 cx: &mut Context<Self>,
854 ) -> Task<Result<()>> {
855 let buffer = self.buffer().clone();
856 let buffers = self.buffer.read(cx).all_buffers();
857 let reload_buffers =
858 project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx));
859 cx.spawn_in(window, |this, mut cx| async move {
860 let transaction = reload_buffers.log_err().await;
861 this.update(&mut cx, |editor, cx| {
862 editor.request_autoscroll(Autoscroll::fit(), cx)
863 })?;
864 buffer
865 .update(&mut cx, |buffer, cx| {
866 if let Some(transaction) = transaction {
867 if !buffer.is_singleton() {
868 buffer.push_transaction(&transaction.0, cx);
869 }
870 }
871 })
872 .ok();
873 Ok(())
874 })
875 }
876
877 fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
878 Some(Box::new(handle.clone()))
879 }
880
881 fn pixel_position_of_cursor(&self, _: &App) -> Option<gpui::Point<Pixels>> {
882 self.pixel_position_of_newest_cursor
883 }
884
885 fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
886 if self.show_breadcrumbs {
887 ToolbarItemLocation::PrimaryLeft
888 } else {
889 ToolbarItemLocation::Hidden
890 }
891 }
892
893 fn breadcrumbs(&self, variant: &Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
894 let cursor = self.selections.newest_anchor().head();
895 let multibuffer = &self.buffer().read(cx);
896 let (buffer_id, symbols) =
897 multibuffer.symbols_containing(cursor, Some(variant.syntax()), cx)?;
898 let buffer = multibuffer.buffer(buffer_id)?;
899
900 let buffer = buffer.read(cx);
901 let text = self.breadcrumb_header.clone().unwrap_or_else(|| {
902 buffer
903 .snapshot()
904 .resolve_file_path(
905 cx,
906 self.project
907 .as_ref()
908 .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
909 .unwrap_or_default(),
910 )
911 .map(|path| path.to_string_lossy().to_string())
912 .unwrap_or_else(|| {
913 if multibuffer.is_singleton() {
914 multibuffer.title(cx).to_string()
915 } else {
916 "untitled".to_string()
917 }
918 })
919 });
920
921 let settings = ThemeSettings::get_global(cx);
922
923 let mut breadcrumbs = vec![BreadcrumbText {
924 text,
925 highlights: None,
926 font: Some(settings.buffer_font.clone()),
927 }];
928
929 breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
930 text: symbol.text,
931 highlights: Some(symbol.highlight_ranges),
932 font: Some(settings.buffer_font.clone()),
933 }));
934 Some(breadcrumbs)
935 }
936
937 fn added_to_workspace(
938 &mut self,
939 workspace: &mut Workspace,
940 _window: &mut Window,
941 _: &mut Context<Self>,
942 ) {
943 self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
944 }
945
946 fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
947 match event {
948 EditorEvent::Closed => f(ItemEvent::CloseItem),
949
950 EditorEvent::Saved | EditorEvent::TitleChanged => {
951 f(ItemEvent::UpdateTab);
952 f(ItemEvent::UpdateBreadcrumbs);
953 }
954
955 EditorEvent::Reparsed(_) => {
956 f(ItemEvent::UpdateBreadcrumbs);
957 }
958
959 EditorEvent::SelectionsChanged { local } if *local => {
960 f(ItemEvent::UpdateBreadcrumbs);
961 }
962
963 EditorEvent::DirtyChanged => {
964 f(ItemEvent::UpdateTab);
965 }
966
967 EditorEvent::BufferEdited => {
968 f(ItemEvent::Edit);
969 f(ItemEvent::UpdateBreadcrumbs);
970 }
971
972 EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
973 f(ItemEvent::Edit);
974 }
975
976 _ => {}
977 }
978 }
979
980 fn preserve_preview(&self, cx: &App) -> bool {
981 self.buffer.read(cx).preserve_preview(cx)
982 }
983}
984
985impl SerializableItem for Editor {
986 fn serialized_item_kind() -> &'static str {
987 "Editor"
988 }
989
990 fn cleanup(
991 workspace_id: WorkspaceId,
992 alive_items: Vec<ItemId>,
993 window: &mut Window,
994 cx: &mut App,
995 ) -> Task<Result<()>> {
996 window.spawn(cx, |_| DB.delete_unloaded_items(workspace_id, alive_items))
997 }
998
999 fn deserialize(
1000 project: Entity<Project>,
1001 workspace: WeakEntity<Workspace>,
1002 workspace_id: workspace::WorkspaceId,
1003 item_id: ItemId,
1004 window: &mut Window,
1005 cx: &mut App,
1006 ) -> Task<Result<Entity<Self>>> {
1007 let serialized_editor = match DB
1008 .get_serialized_editor(item_id, workspace_id)
1009 .context("Failed to query editor state")
1010 {
1011 Ok(Some(serialized_editor)) => {
1012 if ProjectSettings::get_global(cx)
1013 .session
1014 .restore_unsaved_buffers
1015 {
1016 serialized_editor
1017 } else {
1018 SerializedEditor {
1019 abs_path: serialized_editor.abs_path,
1020 contents: None,
1021 language: None,
1022 mtime: None,
1023 }
1024 }
1025 }
1026 Ok(None) => {
1027 return Task::ready(Err(anyhow!("No path or contents found for buffer")));
1028 }
1029 Err(error) => {
1030 return Task::ready(Err(error));
1031 }
1032 };
1033
1034 match serialized_editor {
1035 SerializedEditor {
1036 abs_path: None,
1037 contents: Some(contents),
1038 language,
1039 ..
1040 } => window.spawn(cx, |mut cx| {
1041 let project = project.clone();
1042 async move {
1043 let language_registry =
1044 project.update(&mut cx, |project, _| project.languages().clone())?;
1045
1046 let language = if let Some(language_name) = language {
1047 // We don't fail here, because we'd rather not set the language if the name changed
1048 // than fail to restore the buffer.
1049 language_registry
1050 .language_for_name(&language_name)
1051 .await
1052 .ok()
1053 } else {
1054 None
1055 };
1056
1057 // First create the empty buffer
1058 let buffer = project
1059 .update(&mut cx, |project, cx| project.create_buffer(cx))?
1060 .await?;
1061
1062 // Then set the text so that the dirty bit is set correctly
1063 buffer.update(&mut cx, |buffer, cx| {
1064 buffer.set_language_registry(language_registry);
1065 if let Some(language) = language {
1066 buffer.set_language(Some(language), cx);
1067 }
1068 buffer.set_text(contents, cx);
1069 })?;
1070
1071 cx.update(|window, cx| {
1072 cx.new(|cx| {
1073 let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
1074
1075 editor.read_scroll_position_from_db(item_id, workspace_id, window, cx);
1076 editor
1077 })
1078 })
1079 }
1080 }),
1081 SerializedEditor {
1082 abs_path: Some(abs_path),
1083 contents,
1084 mtime,
1085 ..
1086 } => {
1087 let project_item = project.update(cx, |project, cx| {
1088 let (worktree, path) = project.find_worktree(&abs_path, cx)?;
1089 let project_path = ProjectPath {
1090 worktree_id: worktree.read(cx).id(),
1091 path: path.into(),
1092 };
1093 Some(project.open_path(project_path, cx))
1094 });
1095
1096 match project_item {
1097 Some(project_item) => {
1098 window.spawn(cx, |mut cx| async move {
1099 let (_, project_item) = project_item.await?;
1100 let buffer = project_item.downcast::<Buffer>().map_err(|_| {
1101 anyhow!("Project item at stored path was not a buffer")
1102 })?;
1103
1104 // This is a bit wasteful: we're loading the whole buffer from
1105 // disk and then overwrite the content.
1106 // But for now, it keeps the implementation of the content serialization
1107 // simple, because we don't have to persist all of the metadata that we get
1108 // by loading the file (git diff base, ...).
1109 if let Some(buffer_text) = contents {
1110 buffer.update(&mut cx, |buffer, cx| {
1111 // If we did restore an mtime, we want to store it on the buffer
1112 // so that the next edit will mark the buffer as dirty/conflicted.
1113 if mtime.is_some() {
1114 buffer.did_reload(
1115 buffer.version(),
1116 buffer.line_ending(),
1117 mtime,
1118 cx,
1119 );
1120 }
1121 buffer.set_text(buffer_text, cx);
1122 })?;
1123 }
1124
1125 cx.update(|window, cx| {
1126 cx.new(|cx| {
1127 let mut editor =
1128 Editor::for_buffer(buffer, Some(project), window, cx);
1129
1130 editor.read_scroll_position_from_db(
1131 item_id,
1132 workspace_id,
1133 window,
1134 cx,
1135 );
1136 editor
1137 })
1138 })
1139 })
1140 }
1141 None => {
1142 let open_by_abs_path = workspace.update(cx, |workspace, cx| {
1143 workspace.open_abs_path(abs_path.clone(), false, window, cx)
1144 });
1145 window.spawn(cx, |mut cx| async move {
1146 let editor = open_by_abs_path?.await?.downcast::<Editor>().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?;
1147 editor.update_in(&mut cx, |editor, window, cx| {
1148 editor.read_scroll_position_from_db(item_id, workspace_id, window, cx);
1149 })?;
1150 Ok(editor)
1151 })
1152 }
1153 }
1154 }
1155 SerializedEditor {
1156 abs_path: None,
1157 contents: None,
1158 ..
1159 } => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
1160 }
1161 }
1162
1163 fn serialize(
1164 &mut self,
1165 workspace: &mut Workspace,
1166 item_id: ItemId,
1167 closing: bool,
1168 window: &mut Window,
1169 cx: &mut Context<Self>,
1170 ) -> Option<Task<Result<()>>> {
1171 let mut serialize_dirty_buffers = self.serialize_dirty_buffers;
1172
1173 let project = self.project.clone()?;
1174 if project.read(cx).visible_worktrees(cx).next().is_none() {
1175 // If we don't have a worktree, we don't serialize, because
1176 // projects without worktrees aren't deserialized.
1177 serialize_dirty_buffers = false;
1178 }
1179
1180 if closing && !serialize_dirty_buffers {
1181 return None;
1182 }
1183
1184 let workspace_id = workspace.database_id()?;
1185
1186 let buffer = self.buffer().read(cx).as_singleton()?;
1187
1188 let abs_path = buffer.read(cx).file().and_then(|file| {
1189 let worktree_id = file.worktree_id(cx);
1190 project
1191 .read(cx)
1192 .worktree_for_id(worktree_id, cx)
1193 .and_then(|worktree| worktree.read(cx).absolutize(&file.path()).ok())
1194 .or_else(|| {
1195 let full_path = file.full_path(cx);
1196 let project_path = project.read(cx).find_project_path(&full_path, cx)?;
1197 project.read(cx).absolute_path(&project_path, cx)
1198 })
1199 });
1200
1201 let is_dirty = buffer.read(cx).is_dirty();
1202 let mtime = buffer.read(cx).saved_mtime();
1203
1204 let snapshot = buffer.read(cx).snapshot();
1205
1206 Some(cx.spawn_in(window, |_this, cx| async move {
1207 cx.background_executor()
1208 .spawn(async move {
1209 let (contents, language) = if serialize_dirty_buffers && is_dirty {
1210 let contents = snapshot.text();
1211 let language = snapshot.language().map(|lang| lang.name().to_string());
1212 (Some(contents), language)
1213 } else {
1214 (None, None)
1215 };
1216
1217 let editor = SerializedEditor {
1218 abs_path,
1219 contents,
1220 language,
1221 mtime,
1222 };
1223 DB.save_serialized_editor(item_id, workspace_id, editor)
1224 .await
1225 .context("failed to save serialized editor")
1226 })
1227 .await
1228 .context("failed to save contents of buffer")?;
1229
1230 Ok(())
1231 }))
1232 }
1233
1234 fn should_serialize(&self, event: &Self::Event) -> bool {
1235 matches!(
1236 event,
1237 EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
1238 )
1239 }
1240}
1241
1242impl ProjectItem for Editor {
1243 type Item = Buffer;
1244
1245 fn for_project_item(
1246 project: Entity<Project>,
1247 buffer: Entity<Buffer>,
1248 window: &mut Window,
1249 cx: &mut Context<Self>,
1250 ) -> Self {
1251 Self::for_buffer(buffer, Some(project), window, cx)
1252 }
1253}
1254
1255impl EventEmitter<SearchEvent> for Editor {}
1256
1257pub(crate) enum BufferSearchHighlights {}
1258impl SearchableItem for Editor {
1259 type Match = Range<Anchor>;
1260
1261 fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec<Range<Anchor>> {
1262 self.background_highlights
1263 .get(&TypeId::of::<BufferSearchHighlights>())
1264 .map_or(Vec::new(), |(_color, ranges)| {
1265 ranges.iter().cloned().collect()
1266 })
1267 }
1268
1269 fn clear_matches(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1270 if self
1271 .clear_background_highlights::<BufferSearchHighlights>(cx)
1272 .is_some()
1273 {
1274 cx.emit(SearchEvent::MatchesInvalidated);
1275 }
1276 }
1277
1278 fn update_matches(
1279 &mut self,
1280 matches: &[Range<Anchor>],
1281 _: &mut Window,
1282 cx: &mut Context<Self>,
1283 ) {
1284 let existing_range = self
1285 .background_highlights
1286 .get(&TypeId::of::<BufferSearchHighlights>())
1287 .map(|(_, range)| range.as_ref());
1288 let updated = existing_range != Some(matches);
1289 self.highlight_background::<BufferSearchHighlights>(
1290 matches,
1291 |theme| theme.search_match_background,
1292 cx,
1293 );
1294 if updated {
1295 cx.emit(SearchEvent::MatchesInvalidated);
1296 }
1297 }
1298
1299 fn has_filtered_search_ranges(&mut self) -> bool {
1300 self.has_background_highlights::<SearchWithinRange>()
1301 }
1302
1303 fn toggle_filtered_search_ranges(
1304 &mut self,
1305 enabled: bool,
1306 _: &mut Window,
1307 cx: &mut Context<Self>,
1308 ) {
1309 if self.has_filtered_search_ranges() {
1310 self.previous_search_ranges = self
1311 .clear_background_highlights::<SearchWithinRange>(cx)
1312 .map(|(_, ranges)| ranges)
1313 }
1314
1315 if !enabled {
1316 return;
1317 }
1318
1319 let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
1320 if ranges.iter().any(|s| s.start != s.end) {
1321 self.set_search_within_ranges(&ranges, cx);
1322 } else if let Some(previous_search_ranges) = self.previous_search_ranges.take() {
1323 self.set_search_within_ranges(&previous_search_ranges, cx)
1324 }
1325 }
1326
1327 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1328 let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
1329 let snapshot = &self.snapshot(window, cx).buffer_snapshot;
1330 let selection = self.selections.newest::<usize>(cx);
1331
1332 match setting {
1333 SeedQuerySetting::Never => String::new(),
1334 SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
1335 let text: String = snapshot
1336 .text_for_range(selection.start..selection.end)
1337 .collect();
1338 if text.contains('\n') {
1339 String::new()
1340 } else {
1341 text
1342 }
1343 }
1344 SeedQuerySetting::Selection => String::new(),
1345 SeedQuerySetting::Always => {
1346 let (range, kind) = snapshot.surrounding_word(selection.start, true);
1347 if kind == Some(CharKind::Word) {
1348 let text: String = snapshot.text_for_range(range).collect();
1349 if !text.trim().is_empty() {
1350 return text;
1351 }
1352 }
1353 String::new()
1354 }
1355 }
1356 }
1357
1358 fn activate_match(
1359 &mut self,
1360 index: usize,
1361 matches: &[Range<Anchor>],
1362 window: &mut Window,
1363 cx: &mut Context<Self>,
1364 ) {
1365 self.unfold_ranges(&[matches[index].clone()], false, true, cx);
1366 let range = self.range_for_match(&matches[index]);
1367 self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1368 s.select_ranges([range]);
1369 })
1370 }
1371
1372 fn select_matches(
1373 &mut self,
1374 matches: &[Self::Match],
1375 window: &mut Window,
1376 cx: &mut Context<Self>,
1377 ) {
1378 self.unfold_ranges(matches, false, false, cx);
1379 let mut ranges = Vec::new();
1380 for m in matches {
1381 ranges.push(self.range_for_match(m))
1382 }
1383 self.change_selections(None, window, cx, |s| s.select_ranges(ranges));
1384 }
1385 fn replace(
1386 &mut self,
1387 identifier: &Self::Match,
1388 query: &SearchQuery,
1389 window: &mut Window,
1390 cx: &mut Context<Self>,
1391 ) {
1392 let text = self.buffer.read(cx);
1393 let text = text.snapshot(cx);
1394 let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
1395 let text: Cow<_> = if text.len() == 1 {
1396 text.first().cloned().unwrap().into()
1397 } else {
1398 let joined_chunks = text.join("");
1399 joined_chunks.into()
1400 };
1401
1402 if let Some(replacement) = query.replacement_for(&text) {
1403 self.transact(window, cx, |this, _, cx| {
1404 this.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
1405 });
1406 }
1407 }
1408 fn replace_all(
1409 &mut self,
1410 matches: &mut dyn Iterator<Item = &Self::Match>,
1411 query: &SearchQuery,
1412 window: &mut Window,
1413 cx: &mut Context<Self>,
1414 ) {
1415 let text = self.buffer.read(cx);
1416 let text = text.snapshot(cx);
1417 let mut edits = vec![];
1418 for m in matches {
1419 let text = text.text_for_range(m.clone()).collect::<Vec<_>>();
1420 let text: Cow<_> = if text.len() == 1 {
1421 text.first().cloned().unwrap().into()
1422 } else {
1423 let joined_chunks = text.join("");
1424 joined_chunks.into()
1425 };
1426
1427 if let Some(replacement) = query.replacement_for(&text) {
1428 edits.push((m.clone(), Arc::from(&*replacement)));
1429 }
1430 }
1431
1432 if !edits.is_empty() {
1433 self.transact(window, cx, |this, _, cx| {
1434 this.edit(edits, cx);
1435 });
1436 }
1437 }
1438 fn match_index_for_direction(
1439 &mut self,
1440 matches: &[Range<Anchor>],
1441 current_index: usize,
1442 direction: Direction,
1443 count: usize,
1444 _: &mut Window,
1445 cx: &mut Context<Self>,
1446 ) -> usize {
1447 let buffer = self.buffer().read(cx).snapshot(cx);
1448 let current_index_position = if self.selections.disjoint_anchors().len() == 1 {
1449 self.selections.newest_anchor().head()
1450 } else {
1451 matches[current_index].start
1452 };
1453
1454 let mut count = count % matches.len();
1455 if count == 0 {
1456 return current_index;
1457 }
1458 match direction {
1459 Direction::Next => {
1460 if matches[current_index]
1461 .start
1462 .cmp(¤t_index_position, &buffer)
1463 .is_gt()
1464 {
1465 count -= 1
1466 }
1467
1468 (current_index + count) % matches.len()
1469 }
1470 Direction::Prev => {
1471 if matches[current_index]
1472 .end
1473 .cmp(¤t_index_position, &buffer)
1474 .is_lt()
1475 {
1476 count -= 1;
1477 }
1478
1479 if current_index >= count {
1480 current_index - count
1481 } else {
1482 matches.len() - (count - current_index)
1483 }
1484 }
1485 }
1486 }
1487
1488 fn find_matches(
1489 &mut self,
1490 query: Arc<project::search::SearchQuery>,
1491 _: &mut Window,
1492 cx: &mut Context<Self>,
1493 ) -> Task<Vec<Range<Anchor>>> {
1494 let buffer = self.buffer().read(cx).snapshot(cx);
1495 let search_within_ranges = self
1496 .background_highlights
1497 .get(&TypeId::of::<SearchWithinRange>())
1498 .map_or(vec![], |(_color, ranges)| {
1499 ranges.iter().cloned().collect::<Vec<_>>()
1500 });
1501
1502 cx.background_executor().spawn(async move {
1503 let mut ranges = Vec::new();
1504
1505 let search_within_ranges = if search_within_ranges.is_empty() {
1506 vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())]
1507 } else {
1508 search_within_ranges
1509 };
1510
1511 for range in search_within_ranges {
1512 for (search_buffer, search_range, excerpt_id, deleted_hunk_anchor) in
1513 buffer.range_to_buffer_ranges_with_deleted_hunks(range)
1514 {
1515 ranges.extend(
1516 query
1517 .search(search_buffer, Some(search_range.clone()))
1518 .await
1519 .into_iter()
1520 .map(|match_range| {
1521 if let Some(deleted_hunk_anchor) = deleted_hunk_anchor {
1522 let start = search_buffer
1523 .anchor_after(search_range.start + match_range.start);
1524 let end = search_buffer
1525 .anchor_before(search_range.start + match_range.end);
1526 Anchor {
1527 diff_base_anchor: Some(start),
1528 ..deleted_hunk_anchor
1529 }..Anchor {
1530 diff_base_anchor: Some(end),
1531 ..deleted_hunk_anchor
1532 }
1533 } else {
1534 let start = search_buffer
1535 .anchor_after(search_range.start + match_range.start);
1536 let end = search_buffer
1537 .anchor_before(search_range.start + match_range.end);
1538 Anchor::range_in_buffer(
1539 excerpt_id,
1540 search_buffer.remote_id(),
1541 start..end,
1542 )
1543 }
1544 }),
1545 );
1546 }
1547 }
1548
1549 ranges
1550 })
1551 }
1552
1553 fn active_match_index(
1554 &mut self,
1555 matches: &[Range<Anchor>],
1556 _: &mut Window,
1557 cx: &mut Context<Self>,
1558 ) -> Option<usize> {
1559 active_match_index(
1560 matches,
1561 &self.selections.newest_anchor().head(),
1562 &self.buffer().read(cx).snapshot(cx),
1563 )
1564 }
1565
1566 fn search_bar_visibility_changed(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {
1567 self.expect_bounds_change = self.last_bounds;
1568 }
1569}
1570
1571pub fn active_match_index(
1572 ranges: &[Range<Anchor>],
1573 cursor: &Anchor,
1574 buffer: &MultiBufferSnapshot,
1575) -> Option<usize> {
1576 if ranges.is_empty() {
1577 None
1578 } else {
1579 match ranges.binary_search_by(|probe| {
1580 if probe.end.cmp(cursor, buffer).is_lt() {
1581 Ordering::Less
1582 } else if probe.start.cmp(cursor, buffer).is_gt() {
1583 Ordering::Greater
1584 } else {
1585 Ordering::Equal
1586 }
1587 }) {
1588 Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
1589 }
1590 }
1591}
1592
1593pub fn entry_label_color(selected: bool) -> Color {
1594 if selected {
1595 Color::Default
1596 } else {
1597 Color::Muted
1598 }
1599}
1600
1601pub fn entry_diagnostic_aware_icon_name_and_color(
1602 diagnostic_severity: Option<DiagnosticSeverity>,
1603) -> Option<(IconName, Color)> {
1604 match diagnostic_severity {
1605 Some(DiagnosticSeverity::ERROR) => Some((IconName::X, Color::Error)),
1606 Some(DiagnosticSeverity::WARNING) => Some((IconName::Triangle, Color::Warning)),
1607 _ => None,
1608 }
1609}
1610
1611pub fn entry_diagnostic_aware_icon_decoration_and_color(
1612 diagnostic_severity: Option<DiagnosticSeverity>,
1613) -> Option<(IconDecorationKind, Color)> {
1614 match diagnostic_severity {
1615 Some(DiagnosticSeverity::ERROR) => Some((IconDecorationKind::X, Color::Error)),
1616 Some(DiagnosticSeverity::WARNING) => Some((IconDecorationKind::Triangle, Color::Warning)),
1617 _ => None,
1618 }
1619}
1620
1621pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
1622 let tracked = git_status.index + git_status.worktree;
1623 if ignored {
1624 Color::Ignored
1625 } else if git_status.conflict > 0 {
1626 Color::Conflict
1627 } else if tracked.modified > 0 {
1628 Color::Modified
1629 } else if tracked.added > 0 || git_status.untracked > 0 {
1630 Color::Created
1631 } else {
1632 entry_label_color(selected)
1633 }
1634}
1635
1636fn path_for_buffer<'a>(
1637 buffer: &Entity<MultiBuffer>,
1638 height: usize,
1639 include_filename: bool,
1640 cx: &'a App,
1641) -> Option<Cow<'a, Path>> {
1642 let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
1643 path_for_file(file.as_ref(), height, include_filename, cx)
1644}
1645
1646fn path_for_file<'a>(
1647 file: &'a dyn language::File,
1648 mut height: usize,
1649 include_filename: bool,
1650 cx: &'a App,
1651) -> Option<Cow<'a, Path>> {
1652 // Ensure we always render at least the filename.
1653 height += 1;
1654
1655 let mut prefix = file.path().as_ref();
1656 while height > 0 {
1657 if let Some(parent) = prefix.parent() {
1658 prefix = parent;
1659 height -= 1;
1660 } else {
1661 break;
1662 }
1663 }
1664
1665 // Here we could have just always used `full_path`, but that is very
1666 // allocation-heavy and so we try to use a `Cow<Path>` if we haven't
1667 // traversed all the way up to the worktree's root.
1668 if height > 0 {
1669 let full_path = file.full_path(cx);
1670 if include_filename {
1671 Some(full_path.into())
1672 } else {
1673 Some(full_path.parent()?.to_path_buf().into())
1674 }
1675 } else {
1676 let mut path = file.path().strip_prefix(prefix).ok()?;
1677 if !include_filename {
1678 path = path.parent()?;
1679 }
1680 Some(path.into())
1681 }
1682}
1683
1684#[cfg(test)]
1685mod tests {
1686 use crate::editor_tests::init_test;
1687 use fs::Fs;
1688
1689 use super::*;
1690 use fs::MTime;
1691 use gpui::{App, VisualTestContext};
1692 use language::{LanguageMatcher, TestFile};
1693 use project::FakeFs;
1694 use std::path::{Path, PathBuf};
1695
1696 #[gpui::test]
1697 fn test_path_for_file(cx: &mut App) {
1698 let file = TestFile {
1699 path: Path::new("").into(),
1700 root_name: String::new(),
1701 };
1702 assert_eq!(path_for_file(&file, 0, false, cx), None);
1703 }
1704
1705 async fn deserialize_editor(
1706 item_id: ItemId,
1707 workspace_id: WorkspaceId,
1708 workspace: Entity<Workspace>,
1709 project: Entity<Project>,
1710 cx: &mut VisualTestContext,
1711 ) -> Entity<Editor> {
1712 workspace
1713 .update_in(cx, |workspace, window, cx| {
1714 let pane = workspace.active_pane();
1715 pane.update(cx, |_, cx| {
1716 Editor::deserialize(
1717 project.clone(),
1718 workspace.weak_handle(),
1719 workspace_id,
1720 item_id,
1721 window,
1722 cx,
1723 )
1724 })
1725 })
1726 .await
1727 .unwrap()
1728 }
1729
1730 fn rust_language() -> Arc<language::Language> {
1731 Arc::new(language::Language::new(
1732 language::LanguageConfig {
1733 name: "Rust".into(),
1734 matcher: LanguageMatcher {
1735 path_suffixes: vec!["rs".to_string()],
1736 ..Default::default()
1737 },
1738 ..Default::default()
1739 },
1740 Some(tree_sitter_rust::LANGUAGE.into()),
1741 ))
1742 }
1743
1744 #[gpui::test]
1745 async fn test_deserialize(cx: &mut gpui::TestAppContext) {
1746 init_test(cx, |_| {});
1747
1748 let fs = FakeFs::new(cx.executor());
1749 fs.insert_file("/file.rs", Default::default()).await;
1750
1751 // Test case 1: Deserialize with path and contents
1752 {
1753 let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
1754 let (workspace, cx) =
1755 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1756 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
1757 let item_id = 1234 as ItemId;
1758 let mtime = fs
1759 .metadata(Path::new("/file.rs"))
1760 .await
1761 .unwrap()
1762 .unwrap()
1763 .mtime;
1764
1765 let serialized_editor = SerializedEditor {
1766 abs_path: Some(PathBuf::from("/file.rs")),
1767 contents: Some("fn main() {}".to_string()),
1768 language: Some("Rust".to_string()),
1769 mtime: Some(mtime),
1770 };
1771
1772 DB.save_serialized_editor(item_id, workspace_id, serialized_editor.clone())
1773 .await
1774 .unwrap();
1775
1776 let deserialized =
1777 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
1778
1779 deserialized.update(cx, |editor, cx| {
1780 assert_eq!(editor.text(cx), "fn main() {}");
1781 assert!(editor.is_dirty(cx));
1782 assert!(!editor.has_conflict(cx));
1783 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
1784 assert!(buffer.file().is_some());
1785 });
1786 }
1787
1788 // Test case 2: Deserialize with only path
1789 {
1790 let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
1791 let (workspace, cx) =
1792 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1793
1794 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
1795
1796 let item_id = 5678 as ItemId;
1797 let serialized_editor = SerializedEditor {
1798 abs_path: Some(PathBuf::from("/file.rs")),
1799 contents: None,
1800 language: None,
1801 mtime: None,
1802 };
1803
1804 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
1805 .await
1806 .unwrap();
1807
1808 let deserialized =
1809 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
1810
1811 deserialized.update(cx, |editor, cx| {
1812 assert_eq!(editor.text(cx), ""); // The file should be empty as per our initial setup
1813 assert!(!editor.is_dirty(cx));
1814 assert!(!editor.has_conflict(cx));
1815
1816 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
1817 assert!(buffer.file().is_some());
1818 });
1819 }
1820
1821 // Test case 3: Deserialize with no path (untitled buffer, with content and language)
1822 {
1823 let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
1824 // Add Rust to the language, so that we can restore the language of the buffer
1825 project.update(cx, |project, _| project.languages().add(rust_language()));
1826
1827 let (workspace, cx) =
1828 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1829
1830 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
1831
1832 let item_id = 9012 as ItemId;
1833 let serialized_editor = SerializedEditor {
1834 abs_path: None,
1835 contents: Some("hello".to_string()),
1836 language: Some("Rust".to_string()),
1837 mtime: None,
1838 };
1839
1840 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
1841 .await
1842 .unwrap();
1843
1844 let deserialized =
1845 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
1846
1847 deserialized.update(cx, |editor, cx| {
1848 assert_eq!(editor.text(cx), "hello");
1849 assert!(editor.is_dirty(cx)); // The editor should be dirty for an untitled buffer
1850
1851 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
1852 assert_eq!(
1853 buffer.language().map(|lang| lang.name()),
1854 Some("Rust".into())
1855 ); // Language should be set to Rust
1856 assert!(buffer.file().is_none()); // The buffer should not have an associated file
1857 });
1858 }
1859
1860 // Test case 4: Deserialize with path, content, and old mtime
1861 {
1862 let project = Project::test(fs.clone(), ["/file.rs".as_ref()], cx).await;
1863 let (workspace, cx) =
1864 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1865
1866 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
1867
1868 let item_id = 9345 as ItemId;
1869 let old_mtime = MTime::from_seconds_and_nanos(0, 50);
1870 let serialized_editor = SerializedEditor {
1871 abs_path: Some(PathBuf::from("/file.rs")),
1872 contents: Some("fn main() {}".to_string()),
1873 language: Some("Rust".to_string()),
1874 mtime: Some(old_mtime),
1875 };
1876
1877 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
1878 .await
1879 .unwrap();
1880
1881 let deserialized =
1882 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
1883
1884 deserialized.update(cx, |editor, cx| {
1885 assert_eq!(editor.text(cx), "fn main() {}");
1886 assert!(editor.has_conflict(cx)); // The editor should have a conflict
1887 });
1888 }
1889 }
1890}