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