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_metadata_from_db(item_id, workspace_id, window, cx);
1082 editor
1083 })
1084 })
1085 }
1086 }),
1087 SerializedEditor {
1088 abs_path: Some(abs_path),
1089 contents,
1090 mtime,
1091 ..
1092 } => {
1093 let project_item = project.update(cx, |project, cx| {
1094 let (worktree, path) = project.find_worktree(&abs_path, cx)?;
1095 let project_path = ProjectPath {
1096 worktree_id: worktree.read(cx).id(),
1097 path: path.into(),
1098 };
1099 Some(project.open_path(project_path, cx))
1100 });
1101
1102 match project_item {
1103 Some(project_item) => {
1104 window.spawn(cx, async move |cx| {
1105 let (_, project_item) = project_item.await?;
1106 let buffer = project_item.downcast::<Buffer>().map_err(|_| {
1107 anyhow!("Project item at stored path was not a buffer")
1108 })?;
1109
1110 // This is a bit wasteful: we're loading the whole buffer from
1111 // disk and then overwrite the content.
1112 // But for now, it keeps the implementation of the content serialization
1113 // simple, because we don't have to persist all of the metadata that we get
1114 // by loading the file (git diff base, ...).
1115 if let Some(buffer_text) = contents {
1116 buffer.update(cx, |buffer, cx| {
1117 // If we did restore an mtime, we want to store it on the buffer
1118 // so that the next edit will mark the buffer as dirty/conflicted.
1119 if mtime.is_some() {
1120 buffer.did_reload(
1121 buffer.version(),
1122 buffer.line_ending(),
1123 mtime,
1124 cx,
1125 );
1126 }
1127 buffer.set_text(buffer_text, cx);
1128 if let Some(entry) = buffer.peek_undo_stack() {
1129 buffer.forget_transaction(entry.transaction_id());
1130 }
1131 })?;
1132 }
1133
1134 cx.update(|window, cx| {
1135 cx.new(|cx| {
1136 let mut editor =
1137 Editor::for_buffer(buffer, Some(project), window, cx);
1138
1139 editor.read_metadata_from_db(item_id, workspace_id, window, cx);
1140 editor
1141 })
1142 })
1143 })
1144 }
1145 None => {
1146 let open_by_abs_path = workspace.update(cx, |workspace, cx| {
1147 workspace.open_abs_path(
1148 abs_path.clone(),
1149 OpenOptions {
1150 visible: Some(OpenVisible::None),
1151 ..Default::default()
1152 },
1153 window,
1154 cx,
1155 )
1156 });
1157 window.spawn(cx, async move |cx| {
1158 let editor = open_by_abs_path?.await?.downcast::<Editor>().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?;
1159 editor.update_in(cx, |editor, window, cx| {
1160 editor.read_metadata_from_db(item_id, workspace_id, window, cx);
1161 })?;
1162 Ok(editor)
1163 })
1164 }
1165 }
1166 }
1167 SerializedEditor {
1168 abs_path: None,
1169 contents: None,
1170 ..
1171 } => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
1172 }
1173 }
1174
1175 fn serialize(
1176 &mut self,
1177 workspace: &mut Workspace,
1178 item_id: ItemId,
1179 closing: bool,
1180 window: &mut Window,
1181 cx: &mut Context<Self>,
1182 ) -> Option<Task<Result<()>>> {
1183 let mut serialize_dirty_buffers = self.serialize_dirty_buffers;
1184
1185 let project = self.project.clone()?;
1186 if project.read(cx).visible_worktrees(cx).next().is_none() {
1187 // If we don't have a worktree, we don't serialize, because
1188 // projects without worktrees aren't deserialized.
1189 serialize_dirty_buffers = false;
1190 }
1191
1192 if closing && !serialize_dirty_buffers {
1193 return None;
1194 }
1195
1196 let workspace_id = workspace.database_id()?;
1197
1198 let buffer = self.buffer().read(cx).as_singleton()?;
1199
1200 let abs_path = buffer.read(cx).file().and_then(|file| {
1201 let worktree_id = file.worktree_id(cx);
1202 project
1203 .read(cx)
1204 .worktree_for_id(worktree_id, cx)
1205 .and_then(|worktree| worktree.read(cx).absolutize(&file.path()).ok())
1206 .or_else(|| {
1207 let full_path = file.full_path(cx);
1208 let project_path = project.read(cx).find_project_path(&full_path, cx)?;
1209 project.read(cx).absolute_path(&project_path, cx)
1210 })
1211 });
1212
1213 let is_dirty = buffer.read(cx).is_dirty();
1214 let mtime = buffer.read(cx).saved_mtime();
1215
1216 let snapshot = buffer.read(cx).snapshot();
1217
1218 Some(cx.spawn_in(window, async move |_this, cx| {
1219 cx.background_spawn(async move {
1220 let (contents, language) = if serialize_dirty_buffers && is_dirty {
1221 let contents = snapshot.text();
1222 let language = snapshot.language().map(|lang| lang.name().to_string());
1223 (Some(contents), language)
1224 } else {
1225 (None, None)
1226 };
1227
1228 let editor = SerializedEditor {
1229 abs_path,
1230 contents,
1231 language,
1232 mtime,
1233 };
1234 DB.save_serialized_editor(item_id, workspace_id, editor)
1235 .await
1236 .context("failed to save serialized editor")
1237 })
1238 .await
1239 .context("failed to save contents of buffer")?;
1240
1241 Ok(())
1242 }))
1243 }
1244
1245 fn should_serialize(&self, event: &Self::Event) -> bool {
1246 matches!(
1247 event,
1248 EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
1249 )
1250 }
1251}
1252
1253impl ProjectItem for Editor {
1254 type Item = Buffer;
1255
1256 fn for_project_item(
1257 project: Entity<Project>,
1258 buffer: Entity<Buffer>,
1259 window: &mut Window,
1260 cx: &mut Context<Self>,
1261 ) -> Self {
1262 Self::for_buffer(buffer, Some(project), window, cx)
1263 }
1264}
1265
1266impl EventEmitter<SearchEvent> for Editor {}
1267
1268pub(crate) enum BufferSearchHighlights {}
1269impl SearchableItem for Editor {
1270 type Match = Range<Anchor>;
1271
1272 fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec<Range<Anchor>> {
1273 self.background_highlights
1274 .get(&TypeId::of::<BufferSearchHighlights>())
1275 .map_or(Vec::new(), |(_color, ranges)| {
1276 ranges.iter().cloned().collect()
1277 })
1278 }
1279
1280 fn clear_matches(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1281 if self
1282 .clear_background_highlights::<BufferSearchHighlights>(cx)
1283 .is_some()
1284 {
1285 cx.emit(SearchEvent::MatchesInvalidated);
1286 }
1287 }
1288
1289 fn update_matches(
1290 &mut self,
1291 matches: &[Range<Anchor>],
1292 _: &mut Window,
1293 cx: &mut Context<Self>,
1294 ) {
1295 let existing_range = self
1296 .background_highlights
1297 .get(&TypeId::of::<BufferSearchHighlights>())
1298 .map(|(_, range)| range.as_ref());
1299 let updated = existing_range != Some(matches);
1300 self.highlight_background::<BufferSearchHighlights>(
1301 matches,
1302 |theme| theme.search_match_background,
1303 cx,
1304 );
1305 if updated {
1306 cx.emit(SearchEvent::MatchesInvalidated);
1307 }
1308 }
1309
1310 fn has_filtered_search_ranges(&mut self) -> bool {
1311 self.has_background_highlights::<SearchWithinRange>()
1312 }
1313
1314 fn toggle_filtered_search_ranges(
1315 &mut self,
1316 enabled: bool,
1317 _: &mut Window,
1318 cx: &mut Context<Self>,
1319 ) {
1320 if self.has_filtered_search_ranges() {
1321 self.previous_search_ranges = self
1322 .clear_background_highlights::<SearchWithinRange>(cx)
1323 .map(|(_, ranges)| ranges)
1324 }
1325
1326 if !enabled {
1327 return;
1328 }
1329
1330 let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
1331 if ranges.iter().any(|s| s.start != s.end) {
1332 self.set_search_within_ranges(&ranges, cx);
1333 } else if let Some(previous_search_ranges) = self.previous_search_ranges.take() {
1334 self.set_search_within_ranges(&previous_search_ranges, cx)
1335 }
1336 }
1337
1338 fn supported_options(&self) -> SearchOptions {
1339 if self.in_project_search {
1340 SearchOptions {
1341 case: true,
1342 word: true,
1343 regex: true,
1344 replacement: false,
1345 selection: false,
1346 find_in_results: true,
1347 }
1348 } else {
1349 SearchOptions {
1350 case: true,
1351 word: true,
1352 regex: true,
1353 replacement: true,
1354 selection: true,
1355 find_in_results: false,
1356 }
1357 }
1358 }
1359
1360 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1361 let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
1362 let snapshot = &self.snapshot(window, cx).buffer_snapshot;
1363 let selection = self.selections.newest::<usize>(cx);
1364
1365 match setting {
1366 SeedQuerySetting::Never => String::new(),
1367 SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
1368 let text: String = snapshot
1369 .text_for_range(selection.start..selection.end)
1370 .collect();
1371 if text.contains('\n') {
1372 String::new()
1373 } else {
1374 text
1375 }
1376 }
1377 SeedQuerySetting::Selection => String::new(),
1378 SeedQuerySetting::Always => {
1379 let (range, kind) = snapshot.surrounding_word(selection.start, true);
1380 if kind == Some(CharKind::Word) {
1381 let text: String = snapshot.text_for_range(range).collect();
1382 if !text.trim().is_empty() {
1383 return text;
1384 }
1385 }
1386 String::new()
1387 }
1388 }
1389 }
1390
1391 fn activate_match(
1392 &mut self,
1393 index: usize,
1394 matches: &[Range<Anchor>],
1395 window: &mut Window,
1396 cx: &mut Context<Self>,
1397 ) {
1398 self.unfold_ranges(&[matches[index].clone()], false, true, cx);
1399 let range = self.range_for_match(&matches[index]);
1400 self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
1401 s.select_ranges([range]);
1402 })
1403 }
1404
1405 fn select_matches(
1406 &mut self,
1407 matches: &[Self::Match],
1408 window: &mut Window,
1409 cx: &mut Context<Self>,
1410 ) {
1411 self.unfold_ranges(matches, false, false, cx);
1412 self.change_selections(None, window, cx, |s| {
1413 s.select_ranges(matches.iter().cloned())
1414 });
1415 }
1416 fn replace(
1417 &mut self,
1418 identifier: &Self::Match,
1419 query: &SearchQuery,
1420 window: &mut Window,
1421 cx: &mut Context<Self>,
1422 ) {
1423 let text = self.buffer.read(cx);
1424 let text = text.snapshot(cx);
1425 let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
1426 let text: Cow<_> = if text.len() == 1 {
1427 text.first().cloned().unwrap().into()
1428 } else {
1429 let joined_chunks = text.join("");
1430 joined_chunks.into()
1431 };
1432
1433 if let Some(replacement) = query.replacement_for(&text) {
1434 self.transact(window, cx, |this, _, cx| {
1435 this.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
1436 });
1437 }
1438 }
1439 fn replace_all(
1440 &mut self,
1441 matches: &mut dyn Iterator<Item = &Self::Match>,
1442 query: &SearchQuery,
1443 window: &mut Window,
1444 cx: &mut Context<Self>,
1445 ) {
1446 let text = self.buffer.read(cx);
1447 let text = text.snapshot(cx);
1448 let mut edits = vec![];
1449 for m in matches {
1450 let text = text.text_for_range(m.clone()).collect::<Vec<_>>();
1451 let text: Cow<_> = if text.len() == 1 {
1452 text.first().cloned().unwrap().into()
1453 } else {
1454 let joined_chunks = text.join("");
1455 joined_chunks.into()
1456 };
1457
1458 if let Some(replacement) = query.replacement_for(&text) {
1459 edits.push((m.clone(), Arc::from(&*replacement)));
1460 }
1461 }
1462
1463 if !edits.is_empty() {
1464 self.transact(window, cx, |this, _, cx| {
1465 this.edit(edits, cx);
1466 });
1467 }
1468 }
1469 fn match_index_for_direction(
1470 &mut self,
1471 matches: &[Range<Anchor>],
1472 current_index: usize,
1473 direction: Direction,
1474 count: usize,
1475 _: &mut Window,
1476 cx: &mut Context<Self>,
1477 ) -> usize {
1478 let buffer = self.buffer().read(cx).snapshot(cx);
1479 let current_index_position = if self.selections.disjoint_anchors().len() == 1 {
1480 self.selections.newest_anchor().head()
1481 } else {
1482 matches[current_index].start
1483 };
1484
1485 let mut count = count % matches.len();
1486 if count == 0 {
1487 return current_index;
1488 }
1489 match direction {
1490 Direction::Next => {
1491 if matches[current_index]
1492 .start
1493 .cmp(¤t_index_position, &buffer)
1494 .is_gt()
1495 {
1496 count -= 1
1497 }
1498
1499 (current_index + count) % matches.len()
1500 }
1501 Direction::Prev => {
1502 if matches[current_index]
1503 .end
1504 .cmp(¤t_index_position, &buffer)
1505 .is_lt()
1506 {
1507 count -= 1;
1508 }
1509
1510 if current_index >= count {
1511 current_index - count
1512 } else {
1513 matches.len() - (count - current_index)
1514 }
1515 }
1516 }
1517 }
1518
1519 fn find_matches(
1520 &mut self,
1521 query: Arc<project::search::SearchQuery>,
1522 _: &mut Window,
1523 cx: &mut Context<Self>,
1524 ) -> Task<Vec<Range<Anchor>>> {
1525 let buffer = self.buffer().read(cx).snapshot(cx);
1526 let search_within_ranges = self
1527 .background_highlights
1528 .get(&TypeId::of::<SearchWithinRange>())
1529 .map_or(vec![], |(_color, ranges)| {
1530 ranges.iter().cloned().collect::<Vec<_>>()
1531 });
1532
1533 cx.background_spawn(async move {
1534 let mut ranges = Vec::new();
1535
1536 let search_within_ranges = if search_within_ranges.is_empty() {
1537 vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())]
1538 } else {
1539 search_within_ranges
1540 };
1541
1542 for range in search_within_ranges {
1543 for (search_buffer, search_range, excerpt_id, deleted_hunk_anchor) in
1544 buffer.range_to_buffer_ranges_with_deleted_hunks(range)
1545 {
1546 ranges.extend(
1547 query
1548 .search(search_buffer, Some(search_range.clone()))
1549 .await
1550 .into_iter()
1551 .map(|match_range| {
1552 if let Some(deleted_hunk_anchor) = deleted_hunk_anchor {
1553 let start = search_buffer
1554 .anchor_after(search_range.start + match_range.start);
1555 let end = search_buffer
1556 .anchor_before(search_range.start + match_range.end);
1557 Anchor {
1558 diff_base_anchor: Some(start),
1559 ..deleted_hunk_anchor
1560 }..Anchor {
1561 diff_base_anchor: Some(end),
1562 ..deleted_hunk_anchor
1563 }
1564 } else {
1565 let start = search_buffer
1566 .anchor_after(search_range.start + match_range.start);
1567 let end = search_buffer
1568 .anchor_before(search_range.start + match_range.end);
1569 Anchor::range_in_buffer(
1570 excerpt_id,
1571 search_buffer.remote_id(),
1572 start..end,
1573 )
1574 }
1575 }),
1576 );
1577 }
1578 }
1579
1580 ranges
1581 })
1582 }
1583
1584 fn active_match_index(
1585 &mut self,
1586 direction: Direction,
1587 matches: &[Range<Anchor>],
1588 _: &mut Window,
1589 cx: &mut Context<Self>,
1590 ) -> Option<usize> {
1591 active_match_index(
1592 direction,
1593 matches,
1594 &self.selections.newest_anchor().head(),
1595 &self.buffer().read(cx).snapshot(cx),
1596 )
1597 }
1598
1599 fn search_bar_visibility_changed(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {
1600 self.expect_bounds_change = self.last_bounds;
1601 }
1602}
1603
1604pub fn active_match_index(
1605 direction: Direction,
1606 ranges: &[Range<Anchor>],
1607 cursor: &Anchor,
1608 buffer: &MultiBufferSnapshot,
1609) -> Option<usize> {
1610 if ranges.is_empty() {
1611 None
1612 } else {
1613 let r = ranges.binary_search_by(|probe| {
1614 if probe.end.cmp(cursor, buffer).is_lt() {
1615 Ordering::Less
1616 } else if probe.start.cmp(cursor, buffer).is_gt() {
1617 Ordering::Greater
1618 } else {
1619 Ordering::Equal
1620 }
1621 });
1622 match direction {
1623 Direction::Prev => match r {
1624 Ok(i) => Some(i),
1625 Err(i) => Some(i.saturating_sub(1)),
1626 },
1627 Direction::Next => match r {
1628 Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
1629 },
1630 }
1631 }
1632}
1633
1634pub fn entry_label_color(selected: bool) -> Color {
1635 if selected {
1636 Color::Default
1637 } else {
1638 Color::Muted
1639 }
1640}
1641
1642pub fn entry_diagnostic_aware_icon_name_and_color(
1643 diagnostic_severity: Option<DiagnosticSeverity>,
1644) -> Option<(IconName, Color)> {
1645 match diagnostic_severity {
1646 Some(DiagnosticSeverity::ERROR) => Some((IconName::X, Color::Error)),
1647 Some(DiagnosticSeverity::WARNING) => Some((IconName::Triangle, Color::Warning)),
1648 _ => None,
1649 }
1650}
1651
1652pub fn entry_diagnostic_aware_icon_decoration_and_color(
1653 diagnostic_severity: Option<DiagnosticSeverity>,
1654) -> Option<(IconDecorationKind, Color)> {
1655 match diagnostic_severity {
1656 Some(DiagnosticSeverity::ERROR) => Some((IconDecorationKind::X, Color::Error)),
1657 Some(DiagnosticSeverity::WARNING) => Some((IconDecorationKind::Triangle, Color::Warning)),
1658 _ => None,
1659 }
1660}
1661
1662pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
1663 let tracked = git_status.index + git_status.worktree;
1664 if ignored {
1665 Color::Ignored
1666 } else if git_status.conflict > 0 {
1667 Color::Conflict
1668 } else if tracked.modified > 0 {
1669 Color::Modified
1670 } else if tracked.added > 0 || git_status.untracked > 0 {
1671 Color::Created
1672 } else {
1673 entry_label_color(selected)
1674 }
1675}
1676
1677fn path_for_buffer<'a>(
1678 buffer: &Entity<MultiBuffer>,
1679 height: usize,
1680 include_filename: bool,
1681 cx: &'a App,
1682) -> Option<Cow<'a, Path>> {
1683 let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
1684 path_for_file(file.as_ref(), height, include_filename, cx)
1685}
1686
1687fn path_for_file<'a>(
1688 file: &'a dyn language::File,
1689 mut height: usize,
1690 include_filename: bool,
1691 cx: &'a App,
1692) -> Option<Cow<'a, Path>> {
1693 // Ensure we always render at least the filename.
1694 height += 1;
1695
1696 let mut prefix = file.path().as_ref();
1697 while height > 0 {
1698 if let Some(parent) = prefix.parent() {
1699 prefix = parent;
1700 height -= 1;
1701 } else {
1702 break;
1703 }
1704 }
1705
1706 // Here we could have just always used `full_path`, but that is very
1707 // allocation-heavy and so we try to use a `Cow<Path>` if we haven't
1708 // traversed all the way up to the worktree's root.
1709 if height > 0 {
1710 let full_path = file.full_path(cx);
1711 if include_filename {
1712 Some(full_path.into())
1713 } else {
1714 Some(full_path.parent()?.to_path_buf().into())
1715 }
1716 } else {
1717 let mut path = file.path().strip_prefix(prefix).ok()?;
1718 if !include_filename {
1719 path = path.parent()?;
1720 }
1721 Some(path.into())
1722 }
1723}
1724
1725#[cfg(test)]
1726mod tests {
1727 use crate::editor_tests::init_test;
1728 use fs::Fs;
1729
1730 use super::*;
1731 use fs::MTime;
1732 use gpui::{App, VisualTestContext};
1733 use language::{LanguageMatcher, TestFile};
1734 use project::FakeFs;
1735 use std::path::{Path, PathBuf};
1736 use util::path;
1737
1738 #[gpui::test]
1739 fn test_path_for_file(cx: &mut App) {
1740 let file = TestFile {
1741 path: Path::new("").into(),
1742 root_name: String::new(),
1743 local_root: None,
1744 };
1745 assert_eq!(path_for_file(&file, 0, false, cx), None);
1746 }
1747
1748 async fn deserialize_editor(
1749 item_id: ItemId,
1750 workspace_id: WorkspaceId,
1751 workspace: Entity<Workspace>,
1752 project: Entity<Project>,
1753 cx: &mut VisualTestContext,
1754 ) -> Entity<Editor> {
1755 workspace
1756 .update_in(cx, |workspace, window, cx| {
1757 let pane = workspace.active_pane();
1758 pane.update(cx, |_, cx| {
1759 Editor::deserialize(
1760 project.clone(),
1761 workspace.weak_handle(),
1762 workspace_id,
1763 item_id,
1764 window,
1765 cx,
1766 )
1767 })
1768 })
1769 .await
1770 .unwrap()
1771 }
1772
1773 fn rust_language() -> Arc<language::Language> {
1774 Arc::new(language::Language::new(
1775 language::LanguageConfig {
1776 name: "Rust".into(),
1777 matcher: LanguageMatcher {
1778 path_suffixes: vec!["rs".to_string()],
1779 ..Default::default()
1780 },
1781 ..Default::default()
1782 },
1783 Some(tree_sitter_rust::LANGUAGE.into()),
1784 ))
1785 }
1786
1787 #[gpui::test]
1788 async fn test_deserialize(cx: &mut gpui::TestAppContext) {
1789 init_test(cx, |_| {});
1790
1791 let fs = FakeFs::new(cx.executor());
1792 fs.insert_file(path!("/file.rs"), Default::default()).await;
1793
1794 // Test case 1: Deserialize with path and contents
1795 {
1796 let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
1797 let (workspace, cx) =
1798 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1799 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
1800 let item_id = 1234 as ItemId;
1801 let mtime = fs
1802 .metadata(Path::new(path!("/file.rs")))
1803 .await
1804 .unwrap()
1805 .unwrap()
1806 .mtime;
1807
1808 let serialized_editor = SerializedEditor {
1809 abs_path: Some(PathBuf::from(path!("/file.rs"))),
1810 contents: Some("fn main() {}".to_string()),
1811 language: Some("Rust".to_string()),
1812 mtime: Some(mtime),
1813 };
1814
1815 DB.save_serialized_editor(item_id, workspace_id, serialized_editor.clone())
1816 .await
1817 .unwrap();
1818
1819 let deserialized =
1820 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
1821
1822 deserialized.update(cx, |editor, cx| {
1823 assert_eq!(editor.text(cx), "fn main() {}");
1824 assert!(editor.is_dirty(cx));
1825 assert!(!editor.has_conflict(cx));
1826 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
1827 assert!(buffer.file().is_some());
1828 });
1829 }
1830
1831 // Test case 2: Deserialize with only path
1832 {
1833 let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
1834 let (workspace, cx) =
1835 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1836
1837 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
1838
1839 let item_id = 5678 as ItemId;
1840 let serialized_editor = SerializedEditor {
1841 abs_path: Some(PathBuf::from(path!("/file.rs"))),
1842 contents: None,
1843 language: None,
1844 mtime: None,
1845 };
1846
1847 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
1848 .await
1849 .unwrap();
1850
1851 let deserialized =
1852 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
1853
1854 deserialized.update(cx, |editor, cx| {
1855 assert_eq!(editor.text(cx), ""); // The file should be empty as per our initial setup
1856 assert!(!editor.is_dirty(cx));
1857 assert!(!editor.has_conflict(cx));
1858
1859 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
1860 assert!(buffer.file().is_some());
1861 });
1862 }
1863
1864 // Test case 3: Deserialize with no path (untitled buffer, with content and language)
1865 {
1866 let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
1867 // Add Rust to the language, so that we can restore the language of the buffer
1868 project.update(cx, |project, _| project.languages().add(rust_language()));
1869
1870 let (workspace, cx) =
1871 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1872
1873 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
1874
1875 let item_id = 9012 as ItemId;
1876 let serialized_editor = SerializedEditor {
1877 abs_path: None,
1878 contents: Some("hello".to_string()),
1879 language: Some("Rust".to_string()),
1880 mtime: None,
1881 };
1882
1883 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
1884 .await
1885 .unwrap();
1886
1887 let deserialized =
1888 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
1889
1890 deserialized.update(cx, |editor, cx| {
1891 assert_eq!(editor.text(cx), "hello");
1892 assert!(editor.is_dirty(cx)); // The editor should be dirty for an untitled buffer
1893
1894 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
1895 assert_eq!(
1896 buffer.language().map(|lang| lang.name()),
1897 Some("Rust".into())
1898 ); // Language should be set to Rust
1899 assert!(buffer.file().is_none()); // The buffer should not have an associated file
1900 });
1901 }
1902
1903 // Test case 4: Deserialize with path, content, and old mtime
1904 {
1905 let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
1906 let (workspace, cx) =
1907 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1908
1909 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
1910
1911 let item_id = 9345 as ItemId;
1912 let old_mtime = MTime::from_seconds_and_nanos(0, 50);
1913 let serialized_editor = SerializedEditor {
1914 abs_path: Some(PathBuf::from(path!("/file.rs"))),
1915 contents: Some("fn main() {}".to_string()),
1916 language: Some("Rust".to_string()),
1917 mtime: Some(old_mtime),
1918 };
1919
1920 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
1921 .await
1922 .unwrap();
1923
1924 let deserialized =
1925 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
1926
1927 deserialized.update(cx, |editor, cx| {
1928 assert_eq!(editor.text(cx), "fn main() {}");
1929 assert!(editor.has_conflict(cx)); // The editor should have a conflict
1930 });
1931 }
1932 }
1933}