1use crate::{
2 Anchor, Autoscroll, BufferSerialization, Capability, Editor, EditorEvent, EditorSettings,
3 ExcerptId, ExcerptRange, FormatTarget, MultiBuffer, MultiBufferSnapshot, NavigationData,
4 ReportEditorEvent, SelectionEffects, ToPoint as _,
5 display_map::HighlightKey,
6 editor_settings::SeedQuerySetting,
7 persistence::{DB, SerializedEditor},
8 scroll::{ScrollAnchor, ScrollOffset},
9};
10use anyhow::{Context as _, Result, anyhow};
11use collections::{HashMap, HashSet};
12use file_icons::FileIcons;
13use fs::MTime;
14use futures::future::try_join_all;
15use git::status::GitSummary;
16use gpui::{
17 AnyElement, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, IntoElement,
18 ParentElement, Pixels, SharedString, Styled, Task, WeakEntity, Window, point,
19};
20use language::{
21 Bias, Buffer, BufferRow, CharKind, CharScopeContext, LocalFile, Point, SelectionGoal,
22 proto::serialize_anchor as serialize_text_anchor,
23};
24use lsp::DiagnosticSeverity;
25use multi_buffer::MultiBufferOffset;
26use project::{
27 File, Project, ProjectItem as _, ProjectPath, lsp_store::FormatTrigger,
28 project_settings::ProjectSettings, search::SearchQuery,
29};
30use rpc::proto::{self, update_view};
31use settings::Settings;
32use std::{
33 any::{Any, TypeId},
34 borrow::Cow,
35 cmp::{self, Ordering},
36 iter,
37 ops::Range,
38 path::{Path, PathBuf},
39 sync::Arc,
40};
41use text::{BufferId, BufferSnapshot, Selection};
42use ui::{IconDecorationKind, prelude::*};
43use util::{ResultExt, TryFutureExt, paths::PathExt};
44use workspace::{
45 CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
46 invalid_item_view::InvalidItemView,
47 item::{FollowableItem, Item, ItemBufferKind, ItemEvent, ProjectItem, SaveOptions},
48 searchable::{
49 Direction, FilteredSearchRange, SearchEvent, SearchableItem, SearchableItemHandle,
50 },
51};
52use workspace::{
53 OpenOptions,
54 item::{Dedup, ItemSettings, SerializableItem, TabContentParams},
55};
56use workspace::{
57 OpenVisible, Pane, WorkspaceSettings,
58 item::{BreadcrumbText, FollowEvent, ProjectItemKind},
59 searchable::SearchOptions,
60};
61use zed_actions::preview::{
62 markdown::OpenPreview as OpenMarkdownPreview, svg::OpenPreview as OpenSvgPreview,
63};
64
65pub const MAX_TAB_TITLE_LEN: usize = 24;
66
67impl FollowableItem for Editor {
68 fn remote_id(&self) -> Option<ViewId> {
69 self.remote_id
70 }
71
72 fn from_state_proto(
73 workspace: Entity<Workspace>,
74 remote_id: ViewId,
75 state: &mut Option<proto::view::Variant>,
76 window: &mut Window,
77 cx: &mut App,
78 ) -> Option<Task<Result<Entity<Self>>>> {
79 let project = workspace.read(cx).project().to_owned();
80 let Some(proto::view::Variant::Editor(_)) = state else {
81 return None;
82 };
83 let Some(proto::view::Variant::Editor(state)) = state.take() else {
84 unreachable!()
85 };
86
87 let buffer_ids = state
88 .excerpts
89 .iter()
90 .map(|excerpt| excerpt.buffer_id)
91 .collect::<HashSet<_>>();
92 let buffers = project.update(cx, |project, cx| {
93 buffer_ids
94 .iter()
95 .map(|id| BufferId::new(*id).map(|id| project.open_buffer_by_id(id, cx)))
96 .collect::<Result<Vec<_>>>()
97 });
98
99 Some(window.spawn(cx, async move |cx| {
100 let mut buffers = futures::future::try_join_all(buffers?)
101 .await
102 .debug_assert_ok("leaders don't share views for unshared buffers")?;
103
104 let editor = cx.update(|window, cx| {
105 let multibuffer = cx.new(|cx| {
106 let mut multibuffer;
107 if state.singleton && buffers.len() == 1 {
108 multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
109 } else {
110 multibuffer = MultiBuffer::new(project.read(cx).capability());
111 let mut sorted_excerpts = state.excerpts.clone();
112 sorted_excerpts.sort_by_key(|e| e.id);
113 let sorted_excerpts = sorted_excerpts.into_iter().peekable();
114
115 for excerpt in sorted_excerpts {
116 let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
117 continue;
118 };
119
120 let mut insert_position = ExcerptId::min();
121 for e in &state.excerpts {
122 if e.id == excerpt.id {
123 break;
124 }
125 if e.id < excerpt.id {
126 insert_position = ExcerptId::from_proto(e.id);
127 }
128 }
129
130 let buffer =
131 buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
132
133 let Some(excerpt) = deserialize_excerpt_range(excerpt) else {
134 continue;
135 };
136
137 let Some(buffer) = buffer else { continue };
138
139 multibuffer.insert_excerpts_with_ids_after(
140 insert_position,
141 buffer.clone(),
142 [excerpt],
143 cx,
144 );
145 }
146 };
147
148 if let Some(title) = &state.title {
149 multibuffer = multibuffer.with_title(title.clone())
150 }
151
152 multibuffer
153 });
154
155 cx.new(|cx| {
156 let mut editor =
157 Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx);
158 editor.remote_id = Some(remote_id);
159 editor
160 })
161 })?;
162
163 update_editor_from_message(
164 editor.downgrade(),
165 project,
166 proto::update_view::Editor {
167 selections: state.selections,
168 pending_selection: state.pending_selection,
169 scroll_top_anchor: state.scroll_top_anchor,
170 scroll_x: state.scroll_x,
171 scroll_y: state.scroll_y,
172 ..Default::default()
173 },
174 cx,
175 )
176 .await?;
177
178 Ok(editor)
179 }))
180 }
181
182 fn set_leader_id(
183 &mut self,
184 leader_id: Option<CollaboratorId>,
185 window: &mut Window,
186 cx: &mut Context<Self>,
187 ) {
188 self.leader_id = leader_id;
189 if self.leader_id.is_some() {
190 self.buffer.update(cx, |buffer, cx| {
191 buffer.remove_active_selections(cx);
192 });
193 } else if self.focus_handle.is_focused(window) {
194 self.buffer.update(cx, |buffer, cx| {
195 buffer.set_active_selections(
196 &self.selections.disjoint_anchors_arc(),
197 self.selections.line_mode(),
198 self.cursor_shape,
199 cx,
200 );
201 });
202 }
203 cx.notify();
204 }
205
206 fn to_state_proto(&self, _: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
207 let is_private = self
208 .buffer
209 .read(cx)
210 .as_singleton()
211 .and_then(|buffer| buffer.read(cx).file())
212 .is_some_and(|file| file.is_private());
213 if is_private {
214 return None;
215 }
216
217 let display_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
218 let scroll_anchor = self.scroll_manager.native_anchor(&display_snapshot, cx);
219 let buffer = self.buffer.read(cx);
220 let excerpts = buffer
221 .read(cx)
222 .excerpts()
223 .map(|(id, buffer, range)| proto::Excerpt {
224 id: id.to_proto(),
225 buffer_id: buffer.remote_id().into(),
226 context_start: Some(serialize_text_anchor(&range.context.start)),
227 context_end: Some(serialize_text_anchor(&range.context.end)),
228 primary_start: Some(serialize_text_anchor(&range.primary.start)),
229 primary_end: Some(serialize_text_anchor(&range.primary.end)),
230 })
231 .collect();
232 let snapshot = buffer.snapshot(cx);
233
234 Some(proto::view::Variant::Editor(proto::view::Editor {
235 singleton: buffer.is_singleton(),
236 title: buffer.explicit_title().map(ToOwned::to_owned),
237 excerpts,
238 scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor, &snapshot)),
239 scroll_x: scroll_anchor.offset.x,
240 scroll_y: scroll_anchor.offset.y,
241 selections: self
242 .selections
243 .disjoint_anchors_arc()
244 .iter()
245 .map(|s| serialize_selection(s, &snapshot))
246 .collect(),
247 pending_selection: self
248 .selections
249 .pending_anchor()
250 .as_ref()
251 .map(|s| serialize_selection(s, &snapshot)),
252 }))
253 }
254
255 fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
256 match event {
257 EditorEvent::Edited { .. } => Some(FollowEvent::Unfollow),
258 EditorEvent::SelectionsChanged { local }
259 | EditorEvent::ScrollPositionChanged { local, .. } => {
260 if *local {
261 Some(FollowEvent::Unfollow)
262 } else {
263 None
264 }
265 }
266 _ => None,
267 }
268 }
269
270 fn add_event_to_update_proto(
271 &self,
272 event: &EditorEvent,
273 update: &mut Option<proto::update_view::Variant>,
274 _: &mut Window,
275 cx: &mut App,
276 ) -> bool {
277 let update =
278 update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
279
280 match update {
281 proto::update_view::Variant::Editor(update) => match event {
282 EditorEvent::ExcerptsAdded {
283 buffer,
284 predecessor,
285 excerpts,
286 } => {
287 let buffer_id = buffer.read(cx).remote_id();
288 let mut excerpts = excerpts.iter();
289 if let Some((id, range)) = excerpts.next() {
290 update.inserted_excerpts.push(proto::ExcerptInsertion {
291 previous_excerpt_id: Some(predecessor.to_proto()),
292 excerpt: serialize_excerpt(buffer_id, id, range),
293 });
294 update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
295 proto::ExcerptInsertion {
296 previous_excerpt_id: None,
297 excerpt: serialize_excerpt(buffer_id, id, range),
298 }
299 }))
300 }
301 true
302 }
303 EditorEvent::ExcerptsRemoved { ids, .. } => {
304 update
305 .deleted_excerpts
306 .extend(ids.iter().copied().map(ExcerptId::to_proto));
307 true
308 }
309 EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => {
310 let display_snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
311 let snapshot = self.buffer.read(cx).snapshot(cx);
312 let scroll_anchor = self.scroll_manager.native_anchor(&display_snapshot, cx);
313 update.scroll_top_anchor =
314 Some(serialize_anchor(&scroll_anchor.anchor, &snapshot));
315 update.scroll_x = scroll_anchor.offset.x;
316 update.scroll_y = scroll_anchor.offset.y;
317 true
318 }
319 EditorEvent::SelectionsChanged { .. } => {
320 let snapshot = self.buffer.read(cx).snapshot(cx);
321 update.selections = self
322 .selections
323 .disjoint_anchors_arc()
324 .iter()
325 .map(|s| serialize_selection(s, &snapshot))
326 .collect();
327 update.pending_selection = self
328 .selections
329 .pending_anchor()
330 .as_ref()
331 .map(|s| serialize_selection(s, &snapshot));
332 true
333 }
334 _ => false,
335 },
336 }
337 }
338
339 fn apply_update_proto(
340 &mut self,
341 project: &Entity<Project>,
342 message: update_view::Variant,
343 window: &mut Window,
344 cx: &mut Context<Self>,
345 ) -> Task<Result<()>> {
346 let update_view::Variant::Editor(message) = message;
347 let project = project.clone();
348 cx.spawn_in(window, async move |this, cx| {
349 update_editor_from_message(this, project, message, cx).await
350 })
351 }
352
353 fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
354 true
355 }
356
357 fn dedup(&self, existing: &Self, _: &Window, cx: &App) -> Option<Dedup> {
358 let self_singleton = self.buffer.read(cx).as_singleton()?;
359 let other_singleton = existing.buffer.read(cx).as_singleton()?;
360 if self_singleton == other_singleton {
361 Some(Dedup::KeepExisting)
362 } else {
363 None
364 }
365 }
366
367 fn update_agent_location(
368 &mut self,
369 location: language::Anchor,
370 window: &mut Window,
371 cx: &mut Context<Self>,
372 ) {
373 let buffer = self.buffer.read(cx);
374 let buffer = buffer.read(cx);
375 let Some(position) = buffer.as_singleton_anchor(location) else {
376 return;
377 };
378 let selection = Selection {
379 id: 0,
380 reversed: false,
381 start: position,
382 end: position,
383 goal: SelectionGoal::None,
384 };
385 drop(buffer);
386 self.set_selections_from_remote(vec![selection], None, window, cx);
387 self.request_autoscroll_remotely(Autoscroll::fit(), cx);
388 }
389}
390
391async fn update_editor_from_message(
392 this: WeakEntity<Editor>,
393 project: Entity<Project>,
394 message: proto::update_view::Editor,
395 cx: &mut AsyncWindowContext,
396) -> Result<()> {
397 // Open all of the buffers of which excerpts were added to the editor.
398 let inserted_excerpt_buffer_ids = message
399 .inserted_excerpts
400 .iter()
401 .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
402 .collect::<HashSet<_>>();
403 let inserted_excerpt_buffers = project.update(cx, |project, cx| {
404 inserted_excerpt_buffer_ids
405 .into_iter()
406 .map(|id| BufferId::new(id).map(|id| project.open_buffer_by_id(id, cx)))
407 .collect::<Result<Vec<_>>>()
408 })?;
409 let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
410
411 // Update the editor's excerpts.
412 this.update(cx, |editor, cx| {
413 editor.buffer.update(cx, |multibuffer, cx| {
414 let mut removed_excerpt_ids = message
415 .deleted_excerpts
416 .into_iter()
417 .map(ExcerptId::from_proto)
418 .collect::<Vec<_>>();
419 removed_excerpt_ids.sort_by({
420 let multibuffer = multibuffer.read(cx);
421 move |a, b| a.cmp(b, &multibuffer)
422 });
423
424 let mut insertions = message.inserted_excerpts.into_iter().peekable();
425 while let Some(insertion) = insertions.next() {
426 let Some(excerpt) = insertion.excerpt else {
427 continue;
428 };
429 let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
430 continue;
431 };
432 let buffer_id = BufferId::new(excerpt.buffer_id)?;
433 let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
434 continue;
435 };
436
437 let adjacent_excerpts = iter::from_fn(|| {
438 let insertion = insertions.peek()?;
439 if insertion.previous_excerpt_id.is_none()
440 && insertion.excerpt.as_ref()?.buffer_id == u64::from(buffer_id)
441 {
442 insertions.next()?.excerpt
443 } else {
444 None
445 }
446 });
447
448 multibuffer.insert_excerpts_with_ids_after(
449 ExcerptId::from_proto(previous_excerpt_id),
450 buffer,
451 [excerpt]
452 .into_iter()
453 .chain(adjacent_excerpts)
454 .filter_map(deserialize_excerpt_range),
455 cx,
456 );
457 }
458
459 multibuffer.remove_excerpts(removed_excerpt_ids, cx);
460 anyhow::Ok(())
461 })
462 })??;
463
464 // Deserialize the editor state.
465 let selections = message
466 .selections
467 .into_iter()
468 .filter_map(deserialize_selection)
469 .collect::<Vec<_>>();
470 let pending_selection = message.pending_selection.and_then(deserialize_selection);
471 let scroll_top_anchor = message.scroll_top_anchor.and_then(deserialize_anchor);
472
473 // Wait until the buffer has received all of the operations referenced by
474 // the editor's new state.
475 this.update(cx, |editor, cx| {
476 editor.buffer.update(cx, |buffer, cx| {
477 buffer.wait_for_anchors(
478 selections
479 .iter()
480 .chain(pending_selection.as_ref())
481 .flat_map(|selection| [selection.start, selection.end])
482 .chain(scroll_top_anchor),
483 cx,
484 )
485 })
486 })?
487 .await?;
488
489 // Update the editor's state.
490 this.update_in(cx, |editor, window, cx| {
491 if !selections.is_empty() || pending_selection.is_some() {
492 editor.set_selections_from_remote(selections, pending_selection, window, cx);
493 editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
494 } else if let Some(scroll_top_anchor) = scroll_top_anchor {
495 editor.set_scroll_anchor_remote(
496 ScrollAnchor {
497 anchor: scroll_top_anchor,
498 offset: point(message.scroll_x, message.scroll_y),
499 },
500 window,
501 cx,
502 );
503 }
504 })?;
505 Ok(())
506}
507
508fn serialize_excerpt(
509 buffer_id: BufferId,
510 id: &ExcerptId,
511 range: &ExcerptRange<language::Anchor>,
512) -> Option<proto::Excerpt> {
513 Some(proto::Excerpt {
514 id: id.to_proto(),
515 buffer_id: buffer_id.into(),
516 context_start: Some(serialize_text_anchor(&range.context.start)),
517 context_end: Some(serialize_text_anchor(&range.context.end)),
518 primary_start: Some(serialize_text_anchor(&range.primary.start)),
519 primary_end: Some(serialize_text_anchor(&range.primary.end)),
520 })
521}
522
523fn serialize_selection(
524 selection: &Selection<Anchor>,
525 buffer: &MultiBufferSnapshot,
526) -> proto::Selection {
527 proto::Selection {
528 id: selection.id as u64,
529 start: Some(serialize_anchor(&selection.start, buffer)),
530 end: Some(serialize_anchor(&selection.end, buffer)),
531 reversed: selection.reversed,
532 }
533}
534
535fn serialize_anchor(anchor: &Anchor, buffer: &MultiBufferSnapshot) -> proto::EditorAnchor {
536 proto::EditorAnchor {
537 excerpt_id: buffer.latest_excerpt_id(anchor.excerpt_id).to_proto(),
538 anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
539 }
540}
541
542fn deserialize_excerpt_range(
543 excerpt: proto::Excerpt,
544) -> Option<(ExcerptId, ExcerptRange<language::Anchor>)> {
545 let context = {
546 let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
547 let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
548 start..end
549 };
550 let primary = excerpt
551 .primary_start
552 .zip(excerpt.primary_end)
553 .and_then(|(start, end)| {
554 let start = language::proto::deserialize_anchor(start)?;
555 let end = language::proto::deserialize_anchor(end)?;
556 Some(start..end)
557 })
558 .unwrap_or_else(|| context.clone());
559 Some((
560 ExcerptId::from_proto(excerpt.id),
561 ExcerptRange { context, primary },
562 ))
563}
564
565fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
566 Some(Selection {
567 id: selection.id as usize,
568 start: deserialize_anchor(selection.start?)?,
569 end: deserialize_anchor(selection.end?)?,
570 reversed: selection.reversed,
571 goal: SelectionGoal::None,
572 })
573}
574
575fn deserialize_anchor(anchor: proto::EditorAnchor) -> Option<Anchor> {
576 let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id);
577 Some(Anchor::in_buffer(
578 excerpt_id,
579 language::proto::deserialize_anchor(anchor.anchor?)?,
580 ))
581}
582
583impl Item for Editor {
584 type Event = EditorEvent;
585
586 fn act_as_type<'a>(
587 &'a self,
588 type_id: TypeId,
589 self_handle: &'a Entity<Self>,
590 cx: &'a App,
591 ) -> Option<gpui::AnyEntity> {
592 if TypeId::of::<Self>() == type_id {
593 Some(self_handle.clone().into())
594 } else if TypeId::of::<MultiBuffer>() == type_id {
595 Some(self_handle.read(cx).buffer.clone().into())
596 } else {
597 None
598 }
599 }
600
601 fn navigate(
602 &mut self,
603 data: Arc<dyn Any + Send>,
604 window: &mut Window,
605 cx: &mut Context<Self>,
606 ) -> bool {
607 if let Some(data) = data.downcast_ref::<NavigationData>() {
608 let newest_selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
609 let buffer = self.buffer.read(cx).read(cx);
610 let offset = if buffer.can_resolve(&data.cursor_anchor) {
611 data.cursor_anchor.to_point(&buffer)
612 } else {
613 buffer.clip_point(data.cursor_position, Bias::Left)
614 };
615
616 let mut scroll_anchor = data.scroll_anchor;
617 if !buffer.can_resolve(&scroll_anchor.anchor) {
618 scroll_anchor.anchor = buffer.anchor_before(
619 buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
620 );
621 }
622
623 drop(buffer);
624
625 if newest_selection.head() == offset {
626 false
627 } else {
628 self.set_scroll_anchor(scroll_anchor, window, cx);
629 self.change_selections(
630 SelectionEffects::default().nav_history(false),
631 window,
632 cx,
633 |s| s.select_ranges([offset..offset]),
634 );
635 true
636 }
637 } else {
638 false
639 }
640 }
641
642 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
643 self.buffer()
644 .read(cx)
645 .as_singleton()
646 .and_then(|buffer| buffer.read(cx).file())
647 .and_then(|file| File::from_dyn(Some(file)))
648 .map(|file| {
649 file.worktree
650 .read(cx)
651 .absolutize(&file.path)
652 .compact()
653 .to_string_lossy()
654 .into_owned()
655 .into()
656 })
657 }
658
659 fn telemetry_event_text(&self) -> Option<&'static str> {
660 None
661 }
662
663 fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
664 if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) {
665 path.to_string().into()
666 } else {
667 // Use the same logic as the displayed title for consistency
668 self.buffer.read(cx).title(cx).to_string().into()
669 }
670 }
671
672 fn suggested_filename(&self, cx: &App) -> SharedString {
673 self.buffer.read(cx).title(cx).to_string().into()
674 }
675
676 fn tab_icon(&self, _: &Window, cx: &App) -> Option<Icon> {
677 ItemSettings::get_global(cx)
678 .file_icons
679 .then(|| {
680 path_for_buffer(&self.buffer, 0, true, cx)
681 .and_then(|path| FileIcons::get_icon(Path::new(&*path), cx))
682 })
683 .flatten()
684 .map(Icon::from_path)
685 }
686
687 fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> AnyElement {
688 let label_color = if ItemSettings::get_global(cx).git_status {
689 self.buffer()
690 .read(cx)
691 .as_singleton()
692 .and_then(|buffer| {
693 let buffer = buffer.read(cx);
694 let path = buffer.project_path(cx)?;
695 let buffer_id = buffer.remote_id();
696 let project = self.project()?.read(cx);
697 let entry = project.entry_for_path(&path, cx)?;
698 let (repo, repo_path) = project
699 .git_store()
700 .read(cx)
701 .repository_and_path_for_buffer_id(buffer_id, cx)?;
702 let status = repo.read(cx).status_for_path(&repo_path)?.status;
703
704 Some(entry_git_aware_label_color(
705 status.summary(),
706 entry.is_ignored,
707 params.selected,
708 ))
709 })
710 .unwrap_or_else(|| entry_label_color(params.selected))
711 } else {
712 entry_label_color(params.selected)
713 };
714
715 let description = params.detail.and_then(|detail| {
716 let path = path_for_buffer(&self.buffer, detail, false, cx)?;
717 let description = path.trim();
718
719 if description.is_empty() {
720 return None;
721 }
722
723 Some(util::truncate_and_trailoff(description, MAX_TAB_TITLE_LEN))
724 });
725
726 // Whether the file was saved in the past but is now deleted.
727 let was_deleted: bool = self
728 .buffer()
729 .read(cx)
730 .as_singleton()
731 .and_then(|buffer| buffer.read(cx).file())
732 .is_some_and(|file| file.disk_state().is_deleted());
733
734 h_flex()
735 .gap_2()
736 .child(
737 Label::new(self.title(cx).to_string())
738 .color(label_color)
739 .when(params.preview, |this| this.italic())
740 .when(was_deleted, |this| this.strikethrough()),
741 )
742 .when_some(description, |this, description| {
743 this.child(
744 Label::new(description)
745 .size(LabelSize::XSmall)
746 .color(Color::Muted),
747 )
748 })
749 .into_any_element()
750 }
751
752 fn for_each_project_item(
753 &self,
754 cx: &App,
755 f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem),
756 ) {
757 self.buffer
758 .read(cx)
759 .for_each_buffer(|buffer| f(buffer.entity_id(), buffer.read(cx)));
760 }
761
762 fn buffer_kind(&self, cx: &App) -> ItemBufferKind {
763 match self.buffer.read(cx).is_singleton() {
764 true => ItemBufferKind::Singleton,
765 false => ItemBufferKind::Multibuffer,
766 }
767 }
768
769 fn can_save_as(&self, cx: &App) -> bool {
770 self.buffer.read(cx).is_singleton()
771 }
772
773 fn can_split(&self) -> bool {
774 true
775 }
776
777 fn clone_on_split(
778 &self,
779 _workspace_id: Option<WorkspaceId>,
780 window: &mut Window,
781 cx: &mut Context<Self>,
782 ) -> Task<Option<Entity<Editor>>>
783 where
784 Self: Sized,
785 {
786 Task::ready(Some(cx.new(|cx| self.clone(window, cx))))
787 }
788
789 fn set_nav_history(
790 &mut self,
791 history: ItemNavHistory,
792 _window: &mut Window,
793 _: &mut Context<Self>,
794 ) {
795 self.nav_history = Some(history);
796 }
797
798 fn on_removed(&self, cx: &mut Context<Self>) {
799 self.report_editor_event(ReportEditorEvent::Closed, None, cx);
800 }
801
802 fn deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
803 let selection = self.selections.newest_anchor();
804 self.push_to_nav_history(selection.head(), None, true, false, cx);
805 }
806
807 fn workspace_deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
808 self.hide_hovered_link(cx);
809 }
810
811 fn is_dirty(&self, cx: &App) -> bool {
812 self.buffer().read(cx).read(cx).is_dirty()
813 }
814
815 fn capability(&self, cx: &App) -> Capability {
816 self.capability(cx)
817 }
818
819 // Note: this mirrors the logic in `Editor::toggle_read_only`, but is reachable
820 // without relying on focus-based action dispatch.
821 fn toggle_read_only(&mut self, window: &mut Window, cx: &mut Context<Self>) {
822 if let Some(buffer) = self.buffer.read(cx).as_singleton() {
823 buffer.update(cx, |buffer, cx| {
824 buffer.set_capability(
825 match buffer.capability() {
826 Capability::ReadWrite => Capability::Read,
827 Capability::Read => Capability::ReadWrite,
828 Capability::ReadOnly => Capability::ReadOnly,
829 },
830 cx,
831 );
832 });
833 }
834 cx.notify();
835 window.refresh();
836 }
837
838 fn has_deleted_file(&self, cx: &App) -> bool {
839 self.buffer().read(cx).read(cx).has_deleted_file()
840 }
841
842 fn has_conflict(&self, cx: &App) -> bool {
843 self.buffer().read(cx).read(cx).has_conflict()
844 }
845
846 fn can_save(&self, cx: &App) -> bool {
847 let buffer = &self.buffer().read(cx);
848 if let Some(buffer) = buffer.as_singleton() {
849 buffer.read(cx).project_path(cx).is_some()
850 } else {
851 true
852 }
853 }
854
855 fn save(
856 &mut self,
857 options: SaveOptions,
858 project: Entity<Project>,
859 window: &mut Window,
860 cx: &mut Context<Self>,
861 ) -> Task<Result<()>> {
862 // Add meta data tracking # of auto saves
863 if options.autosave {
864 self.report_editor_event(ReportEditorEvent::Saved { auto_saved: true }, None, cx);
865 } else {
866 self.report_editor_event(ReportEditorEvent::Saved { auto_saved: false }, None, cx);
867 }
868
869 let buffers = self.buffer().clone().read(cx).all_buffers();
870 let buffers = buffers
871 .into_iter()
872 .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))
873 .collect::<HashSet<_>>();
874
875 let buffers_to_save = if self.buffer.read(cx).is_singleton() && !options.autosave {
876 buffers
877 } else {
878 buffers
879 .into_iter()
880 .filter(|buffer| buffer.read(cx).is_dirty())
881 .collect()
882 };
883
884 cx.spawn_in(window, async move |this, cx| {
885 if options.format {
886 this.update_in(cx, |editor, window, cx| {
887 editor.perform_format(
888 project.clone(),
889 FormatTrigger::Save,
890 FormatTarget::Buffers(buffers_to_save.clone()),
891 window,
892 cx,
893 )
894 })?
895 .await?;
896 }
897
898 if !buffers_to_save.is_empty() {
899 project
900 .update(cx, |project, cx| {
901 project.save_buffers(buffers_to_save.clone(), cx)
902 })
903 .await?;
904 }
905
906 Ok(())
907 })
908 }
909
910 fn save_as(
911 &mut self,
912 project: Entity<Project>,
913 path: ProjectPath,
914 _: &mut Window,
915 cx: &mut Context<Self>,
916 ) -> Task<Result<()>> {
917 let buffer = self
918 .buffer()
919 .read(cx)
920 .as_singleton()
921 .expect("cannot call save_as on an excerpt list");
922
923 let file_extension = path.path.extension().map(|a| a.to_string());
924 self.report_editor_event(
925 ReportEditorEvent::Saved { auto_saved: false },
926 file_extension,
927 cx,
928 );
929
930 project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
931 }
932
933 fn reload(
934 &mut self,
935 project: Entity<Project>,
936 window: &mut Window,
937 cx: &mut Context<Self>,
938 ) -> Task<Result<()>> {
939 let buffer = self.buffer().clone();
940 let buffers = self.buffer.read(cx).all_buffers();
941 let reload_buffers =
942 project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx));
943 cx.spawn_in(window, async move |this, cx| {
944 let transaction = reload_buffers.log_err().await;
945 this.update(cx, |editor, cx| {
946 editor.request_autoscroll(Autoscroll::fit(), cx)
947 })?;
948 buffer.update(cx, |buffer, cx| {
949 if let Some(transaction) = transaction
950 && !buffer.is_singleton()
951 {
952 buffer.push_transaction(&transaction.0, cx);
953 }
954 });
955 Ok(())
956 })
957 }
958
959 fn as_searchable(
960 &self,
961 handle: &Entity<Self>,
962 _: &App,
963 ) -> Option<Box<dyn SearchableItemHandle>> {
964 Some(Box::new(handle.clone()))
965 }
966
967 fn pixel_position_of_cursor(&self, _: &App) -> Option<gpui::Point<Pixels>> {
968 self.pixel_position_of_newest_cursor
969 }
970
971 fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
972 if self.show_breadcrumbs && self.buffer().read(cx).is_singleton() {
973 ToolbarItemLocation::PrimaryLeft
974 } else {
975 ToolbarItemLocation::Hidden
976 }
977 }
978
979 // In a non-singleton case, the breadcrumbs are actually shown on sticky file headers of the multibuffer.
980 fn breadcrumbs(&self, cx: &App) -> Option<Vec<BreadcrumbText>> {
981 if self.buffer.read(cx).is_singleton() {
982 self.breadcrumbs_inner(cx)
983 } else {
984 None
985 }
986 }
987
988 fn added_to_workspace(
989 &mut self,
990 workspace: &mut Workspace,
991 _window: &mut Window,
992 cx: &mut Context<Self>,
993 ) {
994 self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
995 if let Some(workspace) = &workspace.weak_handle().upgrade() {
996 cx.subscribe(workspace, |editor, _, event: &workspace::Event, _cx| {
997 if let workspace::Event::ModalOpened = event {
998 editor.mouse_context_menu.take();
999 editor.inline_blame_popover.take();
1000 }
1001 })
1002 .detach();
1003 }
1004 }
1005
1006 fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
1007 match event {
1008 EditorEvent::Saved | EditorEvent::TitleChanged => {
1009 f(ItemEvent::UpdateTab);
1010 f(ItemEvent::UpdateBreadcrumbs);
1011 }
1012
1013 EditorEvent::Reparsed(_) => {
1014 f(ItemEvent::UpdateBreadcrumbs);
1015 }
1016
1017 EditorEvent::SelectionsChanged { local } if *local => {
1018 f(ItemEvent::UpdateBreadcrumbs);
1019 }
1020
1021 EditorEvent::BreadcrumbsChanged => {
1022 f(ItemEvent::UpdateBreadcrumbs);
1023 }
1024
1025 EditorEvent::DirtyChanged => {
1026 f(ItemEvent::UpdateTab);
1027 }
1028
1029 EditorEvent::BufferEdited => {
1030 f(ItemEvent::Edit);
1031 f(ItemEvent::UpdateBreadcrumbs);
1032 }
1033
1034 EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
1035 f(ItemEvent::Edit);
1036 }
1037
1038 _ => {}
1039 }
1040 }
1041
1042 fn tab_extra_context_menu_actions(
1043 &self,
1044 _window: &mut Window,
1045 cx: &mut Context<Self>,
1046 ) -> Vec<(SharedString, Box<dyn gpui::Action>)> {
1047 let mut actions = Vec::new();
1048
1049 let is_markdown = self
1050 .buffer()
1051 .read(cx)
1052 .as_singleton()
1053 .and_then(|buffer| buffer.read(cx).language())
1054 .is_some_and(|language| language.name().as_ref() == "Markdown");
1055
1056 let is_svg = self
1057 .buffer()
1058 .read(cx)
1059 .as_singleton()
1060 .and_then(|buffer| buffer.read(cx).file())
1061 .is_some_and(|file| {
1062 std::path::Path::new(file.file_name(cx))
1063 .extension()
1064 .is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
1065 });
1066
1067 if is_markdown {
1068 actions.push((
1069 "Open Markdown Preview".into(),
1070 Box::new(OpenMarkdownPreview) as Box<dyn gpui::Action>,
1071 ));
1072 }
1073
1074 if is_svg {
1075 actions.push((
1076 "Open SVG Preview".into(),
1077 Box::new(OpenSvgPreview) as Box<dyn gpui::Action>,
1078 ));
1079 }
1080
1081 actions
1082 }
1083
1084 fn preserve_preview(&self, cx: &App) -> bool {
1085 self.buffer.read(cx).preserve_preview(cx)
1086 }
1087}
1088
1089impl SerializableItem for Editor {
1090 fn serialized_item_kind() -> &'static str {
1091 "Editor"
1092 }
1093
1094 fn cleanup(
1095 workspace_id: WorkspaceId,
1096 alive_items: Vec<ItemId>,
1097 _window: &mut Window,
1098 cx: &mut App,
1099 ) -> Task<Result<()>> {
1100 workspace::delete_unloaded_items(alive_items, workspace_id, "editors", &DB, cx)
1101 }
1102
1103 fn deserialize(
1104 project: Entity<Project>,
1105 workspace: WeakEntity<Workspace>,
1106 workspace_id: workspace::WorkspaceId,
1107 item_id: ItemId,
1108 window: &mut Window,
1109 cx: &mut App,
1110 ) -> Task<Result<Entity<Self>>> {
1111 let serialized_editor = match DB
1112 .get_serialized_editor(item_id, workspace_id)
1113 .context("Failed to query editor state")
1114 {
1115 Ok(Some(serialized_editor)) => {
1116 if ProjectSettings::get_global(cx)
1117 .session
1118 .restore_unsaved_buffers
1119 {
1120 serialized_editor
1121 } else {
1122 SerializedEditor {
1123 abs_path: serialized_editor.abs_path,
1124 contents: None,
1125 language: None,
1126 mtime: None,
1127 }
1128 }
1129 }
1130 Ok(None) => {
1131 return Task::ready(Err(anyhow!(
1132 "Unable to deserialize editor: No entry in database for item_id: {item_id} and workspace_id {workspace_id:?}"
1133 )));
1134 }
1135 Err(error) => {
1136 return Task::ready(Err(error));
1137 }
1138 };
1139 log::debug!(
1140 "Deserialized editor {item_id:?} in workspace {workspace_id:?}, {serialized_editor:?}"
1141 );
1142
1143 match serialized_editor {
1144 SerializedEditor {
1145 abs_path: None,
1146 contents: Some(contents),
1147 language,
1148 ..
1149 } => window.spawn(cx, {
1150 let project = project.clone();
1151 async move |cx| {
1152 let language_registry =
1153 project.read_with(cx, |project, _| project.languages().clone());
1154
1155 let language = if let Some(language_name) = language {
1156 // We don't fail here, because we'd rather not set the language if the name changed
1157 // than fail to restore the buffer.
1158 language_registry
1159 .language_for_name(&language_name)
1160 .await
1161 .ok()
1162 } else {
1163 None
1164 };
1165
1166 // First create the empty buffer
1167 let buffer = project
1168 .update(cx, |project, cx| project.create_buffer(language, true, cx))
1169 .await
1170 .context("Failed to create buffer while deserializing editor")?;
1171
1172 // Then set the text so that the dirty bit is set correctly
1173 buffer.update(cx, |buffer, cx| {
1174 buffer.set_language_registry(language_registry);
1175 buffer.set_text(contents, cx);
1176 if let Some(entry) = buffer.peek_undo_stack() {
1177 buffer.forget_transaction(entry.transaction_id());
1178 }
1179 });
1180
1181 cx.update(|window, cx| {
1182 cx.new(|cx| {
1183 let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
1184
1185 editor.read_metadata_from_db(item_id, workspace_id, window, cx);
1186 editor
1187 })
1188 })
1189 }
1190 }),
1191 SerializedEditor {
1192 abs_path: Some(abs_path),
1193 contents,
1194 mtime,
1195 ..
1196 } => {
1197 let opened_buffer = project.update(cx, |project, cx| {
1198 let (worktree, path) = project.find_worktree(&abs_path, cx)?;
1199 let project_path = ProjectPath {
1200 worktree_id: worktree.read(cx).id(),
1201 path: path,
1202 };
1203 Some(project.open_path(project_path, cx))
1204 });
1205
1206 match opened_buffer {
1207 Some(opened_buffer) => window.spawn(cx, async move |cx| {
1208 let (_, buffer) = opened_buffer
1209 .await
1210 .context("Failed to open path in project")?;
1211
1212 if let Some(contents) = contents {
1213 buffer.update(cx, |buffer, cx| {
1214 restore_serialized_buffer_contents(buffer, contents, mtime, cx);
1215 });
1216 }
1217
1218 cx.update(|window, cx| {
1219 cx.new(|cx| {
1220 let mut editor =
1221 Editor::for_buffer(buffer, Some(project), window, cx);
1222
1223 editor.read_metadata_from_db(item_id, workspace_id, window, cx);
1224 editor
1225 })
1226 })
1227 }),
1228 None => {
1229 // File is not in any worktree (e.g., opened as a standalone file)
1230 // We need to open it via workspace and then restore dirty contents
1231 window.spawn(cx, async move |cx| {
1232 let open_by_abs_path =
1233 workspace.update_in(cx, |workspace, window, cx| {
1234 workspace.open_abs_path(
1235 abs_path.clone(),
1236 OpenOptions {
1237 visible: Some(OpenVisible::None),
1238 ..Default::default()
1239 },
1240 window,
1241 cx,
1242 )
1243 })?;
1244 let editor =
1245 open_by_abs_path.await?.downcast::<Editor>().with_context(
1246 || format!("path {abs_path:?} cannot be opened as an Editor"),
1247 )?;
1248
1249 if let Some(contents) = contents {
1250 editor.update_in(cx, |editor, _window, cx| {
1251 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
1252 buffer.update(cx, |buffer, cx| {
1253 restore_serialized_buffer_contents(
1254 buffer, contents, mtime, cx,
1255 );
1256 });
1257 }
1258 })?;
1259 }
1260
1261 editor.update_in(cx, |editor, window, cx| {
1262 editor.read_metadata_from_db(item_id, workspace_id, window, cx);
1263 })?;
1264 Ok(editor)
1265 })
1266 }
1267 }
1268 }
1269 SerializedEditor {
1270 abs_path: None,
1271 contents: None,
1272 ..
1273 } => window.spawn(cx, async move |cx| {
1274 let buffer = project
1275 .update(cx, |project, cx| project.create_buffer(None, true, cx))
1276 .await
1277 .context("Failed to create buffer")?;
1278
1279 cx.update(|window, cx| {
1280 cx.new(|cx| {
1281 let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
1282
1283 editor.read_metadata_from_db(item_id, workspace_id, window, cx);
1284 editor
1285 })
1286 })
1287 }),
1288 }
1289 }
1290
1291 fn serialize(
1292 &mut self,
1293 workspace: &mut Workspace,
1294 item_id: ItemId,
1295 closing: bool,
1296 window: &mut Window,
1297 cx: &mut Context<Self>,
1298 ) -> Option<Task<Result<()>>> {
1299 let buffer_serialization = self.buffer_serialization?;
1300 let project = self.project.clone()?;
1301
1302 let serialize_dirty_buffers = match buffer_serialization {
1303 // Always serialize dirty buffers, including for worktree-less windows.
1304 // This enables hot-exit functionality for empty windows and single files.
1305 BufferSerialization::All => true,
1306 BufferSerialization::NonDirtyBuffers => false,
1307 };
1308
1309 if closing && !serialize_dirty_buffers {
1310 return None;
1311 }
1312
1313 let workspace_id = workspace.database_id()?;
1314
1315 let buffer = self.buffer().read(cx).as_singleton()?;
1316
1317 let abs_path = buffer.read(cx).file().and_then(|file| {
1318 let worktree_id = file.worktree_id(cx);
1319 project
1320 .read(cx)
1321 .worktree_for_id(worktree_id, cx)
1322 .map(|worktree| worktree.read(cx).absolutize(file.path()))
1323 .or_else(|| {
1324 let full_path = file.full_path(cx);
1325 let project_path = project.read(cx).find_project_path(&full_path, cx)?;
1326 project.read(cx).absolute_path(&project_path, cx)
1327 })
1328 });
1329
1330 let is_dirty = buffer.read(cx).is_dirty();
1331 let mtime = buffer.read(cx).saved_mtime();
1332
1333 let snapshot = buffer.read(cx).snapshot();
1334
1335 Some(cx.spawn_in(window, async move |_this, cx| {
1336 cx.background_spawn(async move {
1337 let (contents, language) = if serialize_dirty_buffers && is_dirty {
1338 let contents = snapshot.text();
1339 let language = snapshot.language().map(|lang| lang.name().to_string());
1340 (Some(contents), language)
1341 } else {
1342 (None, None)
1343 };
1344
1345 let editor = SerializedEditor {
1346 abs_path,
1347 contents,
1348 language,
1349 mtime,
1350 };
1351 log::debug!("Serializing editor {item_id:?} in workspace {workspace_id:?}");
1352 DB.save_serialized_editor(item_id, workspace_id, editor)
1353 .await
1354 .context("failed to save serialized editor")
1355 })
1356 .await
1357 .context("failed to save contents of buffer")?;
1358
1359 Ok(())
1360 }))
1361 }
1362
1363 fn should_serialize(&self, event: &Self::Event) -> bool {
1364 self.should_serialize_buffer()
1365 && matches!(
1366 event,
1367 EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
1368 )
1369 }
1370}
1371
1372#[derive(Debug, Default)]
1373struct EditorRestorationData {
1374 entries: HashMap<PathBuf, RestorationData>,
1375}
1376
1377#[derive(Default, Debug)]
1378pub struct RestorationData {
1379 pub scroll_position: (BufferRow, gpui::Point<ScrollOffset>),
1380 pub folds: Vec<Range<Point>>,
1381 pub selections: Vec<Range<Point>>,
1382}
1383
1384impl ProjectItem for Editor {
1385 type Item = Buffer;
1386
1387 fn project_item_kind() -> Option<ProjectItemKind> {
1388 Some(ProjectItemKind("Editor"))
1389 }
1390
1391 fn for_project_item(
1392 project: Entity<Project>,
1393 pane: Option<&Pane>,
1394 buffer: Entity<Buffer>,
1395 window: &mut Window,
1396 cx: &mut Context<Self>,
1397 ) -> Self {
1398 let mut editor = Self::for_buffer(buffer.clone(), Some(project), window, cx);
1399 if let Some((excerpt_id, _, snapshot)) =
1400 editor.buffer().read(cx).snapshot(cx).as_singleton()
1401 && WorkspaceSettings::get(None, cx).restore_on_file_reopen
1402 && let Some(restoration_data) = Self::project_item_kind()
1403 .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind))
1404 .and_then(|data| data.downcast_ref::<EditorRestorationData>())
1405 .and_then(|data| {
1406 let file = project::File::from_dyn(buffer.read(cx).file())?;
1407 data.entries.get(&file.abs_path(cx))
1408 })
1409 {
1410 editor.fold_ranges(
1411 clip_ranges(&restoration_data.folds, snapshot),
1412 false,
1413 window,
1414 cx,
1415 );
1416 if !restoration_data.selections.is_empty() {
1417 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1418 s.select_ranges(clip_ranges(&restoration_data.selections, snapshot));
1419 });
1420 }
1421 let (top_row, offset) = restoration_data.scroll_position;
1422 let anchor =
1423 Anchor::in_buffer(*excerpt_id, snapshot.anchor_before(Point::new(top_row, 0)));
1424 editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx);
1425 }
1426
1427 editor
1428 }
1429
1430 fn for_broken_project_item(
1431 abs_path: &Path,
1432 is_local: bool,
1433 e: &anyhow::Error,
1434 window: &mut Window,
1435 cx: &mut App,
1436 ) -> Option<InvalidItemView> {
1437 Some(InvalidItemView::new(abs_path, is_local, e, window, cx))
1438 }
1439}
1440
1441fn clip_ranges<'a>(
1442 original: impl IntoIterator<Item = &'a Range<Point>> + 'a,
1443 snapshot: &'a BufferSnapshot,
1444) -> Vec<Range<Point>> {
1445 original
1446 .into_iter()
1447 .map(|range| {
1448 snapshot.clip_point(range.start, Bias::Left)
1449 ..snapshot.clip_point(range.end, Bias::Right)
1450 })
1451 .collect()
1452}
1453
1454impl EventEmitter<SearchEvent> for Editor {}
1455
1456impl Editor {
1457 pub fn update_restoration_data(
1458 &self,
1459 cx: &mut Context<Self>,
1460 write: impl for<'a> FnOnce(&'a mut RestorationData) + 'static,
1461 ) {
1462 if self.mode.is_minimap() || !WorkspaceSettings::get(None, cx).restore_on_file_reopen {
1463 return;
1464 }
1465
1466 let editor = cx.entity();
1467 cx.defer(move |cx| {
1468 editor.update(cx, |editor, cx| {
1469 let kind = Editor::project_item_kind()?;
1470 let pane = editor.workspace()?.read(cx).pane_for(&cx.entity())?;
1471 let buffer = editor.buffer().read(cx).as_singleton()?;
1472 let file_abs_path = project::File::from_dyn(buffer.read(cx).file())?.abs_path(cx);
1473 pane.update(cx, |pane, _| {
1474 let data = pane
1475 .project_item_restoration_data
1476 .entry(kind)
1477 .or_insert_with(|| Box::new(EditorRestorationData::default()) as Box<_>);
1478 let data = match data.downcast_mut::<EditorRestorationData>() {
1479 Some(data) => data,
1480 None => {
1481 *data = Box::new(EditorRestorationData::default());
1482 data.downcast_mut::<EditorRestorationData>()
1483 .expect("just written the type downcasted to")
1484 }
1485 };
1486
1487 let data = data.entries.entry(file_abs_path).or_default();
1488 write(data);
1489 Some(())
1490 })
1491 });
1492 });
1493 }
1494}
1495
1496impl SearchableItem for Editor {
1497 type Match = Range<Anchor>;
1498
1499 fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec<Range<Anchor>> {
1500 self.background_highlights
1501 .get(&HighlightKey::BufferSearchHighlights)
1502 .map_or(Vec::new(), |(_color, ranges)| {
1503 ranges.iter().cloned().collect()
1504 })
1505 }
1506
1507 fn clear_matches(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1508 if self
1509 .clear_background_highlights(HighlightKey::BufferSearchHighlights, cx)
1510 .is_some()
1511 {
1512 cx.emit(SearchEvent::MatchesInvalidated);
1513 }
1514 }
1515
1516 fn update_matches(
1517 &mut self,
1518 matches: &[Range<Anchor>],
1519 active_match_index: Option<usize>,
1520 _: &mut Window,
1521 cx: &mut Context<Self>,
1522 ) {
1523 let existing_range = self
1524 .background_highlights
1525 .get(&HighlightKey::BufferSearchHighlights)
1526 .map(|(_, range)| range.as_ref());
1527 let updated = existing_range != Some(matches);
1528 self.highlight_background(
1529 HighlightKey::BufferSearchHighlights,
1530 matches,
1531 move |index, theme| {
1532 if active_match_index == Some(*index) {
1533 theme.colors().search_active_match_background
1534 } else {
1535 theme.colors().search_match_background
1536 }
1537 },
1538 cx,
1539 );
1540 if updated {
1541 cx.emit(SearchEvent::MatchesInvalidated);
1542 }
1543 }
1544
1545 fn has_filtered_search_ranges(&mut self) -> bool {
1546 self.has_background_highlights(HighlightKey::SearchWithinRange)
1547 }
1548
1549 fn toggle_filtered_search_ranges(
1550 &mut self,
1551 enabled: Option<FilteredSearchRange>,
1552 _: &mut Window,
1553 cx: &mut Context<Self>,
1554 ) {
1555 if self.has_filtered_search_ranges() {
1556 self.previous_search_ranges = self
1557 .clear_background_highlights(HighlightKey::SearchWithinRange, cx)
1558 .map(|(_, ranges)| ranges)
1559 }
1560
1561 if let Some(range) = enabled {
1562 let ranges = self.selections.disjoint_anchor_ranges().collect::<Vec<_>>();
1563
1564 if ranges.iter().any(|s| s.start != s.end) {
1565 self.set_search_within_ranges(&ranges, cx);
1566 } else if let Some(previous_search_ranges) = self.previous_search_ranges.take()
1567 && range != FilteredSearchRange::Selection
1568 {
1569 self.set_search_within_ranges(&previous_search_ranges, cx);
1570 }
1571 }
1572 }
1573
1574 fn supported_options(&self) -> SearchOptions {
1575 if self.in_project_search {
1576 SearchOptions {
1577 case: true,
1578 word: true,
1579 regex: true,
1580 replacement: false,
1581 selection: false,
1582 find_in_results: true,
1583 }
1584 } else {
1585 SearchOptions {
1586 case: true,
1587 word: true,
1588 regex: true,
1589 replacement: true,
1590 selection: true,
1591 find_in_results: false,
1592 }
1593 }
1594 }
1595
1596 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1597 let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
1598 let snapshot = self.snapshot(window, cx);
1599 let selection = self.selections.newest_adjusted(&snapshot.display_snapshot);
1600 let buffer_snapshot = snapshot.buffer_snapshot();
1601
1602 match setting {
1603 SeedQuerySetting::Never => String::new(),
1604 SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
1605 let text: String = buffer_snapshot
1606 .text_for_range(selection.start..selection.end)
1607 .collect();
1608 if text.contains('\n') {
1609 String::new()
1610 } else {
1611 text
1612 }
1613 }
1614 SeedQuerySetting::Selection => String::new(),
1615 SeedQuerySetting::Always => {
1616 let (range, kind) = buffer_snapshot
1617 .surrounding_word(selection.start, Some(CharScopeContext::Completion));
1618 if kind == Some(CharKind::Word) {
1619 let text: String = buffer_snapshot.text_for_range(range).collect();
1620 if !text.trim().is_empty() {
1621 return text;
1622 }
1623 }
1624 String::new()
1625 }
1626 }
1627 }
1628
1629 fn activate_match(
1630 &mut self,
1631 index: usize,
1632 matches: &[Range<Anchor>],
1633 window: &mut Window,
1634 cx: &mut Context<Self>,
1635 ) {
1636 self.unfold_ranges(&[matches[index].clone()], false, true, cx);
1637 let range = self.range_for_match(&matches[index]);
1638 let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
1639 Autoscroll::center()
1640 } else {
1641 Autoscroll::fit()
1642 };
1643 self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
1644 s.select_ranges([range]);
1645 })
1646 }
1647
1648 fn select_matches(
1649 &mut self,
1650 matches: &[Self::Match],
1651 window: &mut Window,
1652 cx: &mut Context<Self>,
1653 ) {
1654 self.unfold_ranges(matches, false, false, cx);
1655 self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1656 s.select_ranges(matches.iter().cloned())
1657 });
1658 }
1659 fn replace(
1660 &mut self,
1661 identifier: &Self::Match,
1662 query: &SearchQuery,
1663 window: &mut Window,
1664 cx: &mut Context<Self>,
1665 ) {
1666 let text = self.buffer.read(cx);
1667 let text = text.snapshot(cx);
1668 let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
1669 let text: Cow<_> = if text.len() == 1 {
1670 text.first().cloned().unwrap().into()
1671 } else {
1672 let joined_chunks = text.join("");
1673 joined_chunks.into()
1674 };
1675
1676 if let Some(replacement) = query.replacement_for(&text) {
1677 self.transact(window, cx, |this, _, cx| {
1678 this.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
1679 });
1680 }
1681 }
1682 fn replace_all(
1683 &mut self,
1684 matches: &mut dyn Iterator<Item = &Self::Match>,
1685 query: &SearchQuery,
1686 window: &mut Window,
1687 cx: &mut Context<Self>,
1688 ) {
1689 let text = self.buffer.read(cx);
1690 let text = text.snapshot(cx);
1691 let mut edits = vec![];
1692
1693 // A regex might have replacement variables so we cannot apply
1694 // the same replacement to all matches
1695 if query.is_regex() {
1696 edits = matches
1697 .filter_map(|m| {
1698 let text = text.text_for_range(m.clone()).collect::<Vec<_>>();
1699
1700 let text: Cow<_> = if text.len() == 1 {
1701 text.first().cloned().unwrap().into()
1702 } else {
1703 let joined_chunks = text.join("");
1704 joined_chunks.into()
1705 };
1706
1707 query
1708 .replacement_for(&text)
1709 .map(|replacement| (m.clone(), Arc::from(&*replacement)))
1710 })
1711 .collect();
1712 } else if let Some(replacement) = query.replacement().map(Arc::<str>::from) {
1713 edits = matches.map(|m| (m.clone(), replacement.clone())).collect();
1714 }
1715
1716 if !edits.is_empty() {
1717 self.transact(window, cx, |this, _, cx| {
1718 this.edit(edits, cx);
1719 });
1720 }
1721 }
1722 fn match_index_for_direction(
1723 &mut self,
1724 matches: &[Range<Anchor>],
1725 current_index: usize,
1726 direction: Direction,
1727 count: usize,
1728 _: &mut Window,
1729 cx: &mut Context<Self>,
1730 ) -> usize {
1731 let buffer = self.buffer().read(cx).snapshot(cx);
1732 let current_index_position = if self.selections.disjoint_anchors_arc().len() == 1 {
1733 self.selections.newest_anchor().head()
1734 } else {
1735 matches[current_index].start
1736 };
1737
1738 let mut count = count % matches.len();
1739 if count == 0 {
1740 return current_index;
1741 }
1742 match direction {
1743 Direction::Next => {
1744 if matches[current_index]
1745 .start
1746 .cmp(¤t_index_position, &buffer)
1747 .is_gt()
1748 {
1749 count -= 1
1750 }
1751
1752 (current_index + count) % matches.len()
1753 }
1754 Direction::Prev => {
1755 if matches[current_index]
1756 .end
1757 .cmp(¤t_index_position, &buffer)
1758 .is_lt()
1759 {
1760 count -= 1;
1761 }
1762
1763 if current_index >= count {
1764 current_index - count
1765 } else {
1766 matches.len() - (count - current_index)
1767 }
1768 }
1769 }
1770 }
1771
1772 fn find_matches(
1773 &mut self,
1774 query: Arc<project::search::SearchQuery>,
1775 _: &mut Window,
1776 cx: &mut Context<Self>,
1777 ) -> Task<Vec<Range<Anchor>>> {
1778 let buffer = self.buffer().read(cx).snapshot(cx);
1779 let search_within_ranges = self
1780 .background_highlights
1781 .get(&HighlightKey::SearchWithinRange)
1782 .map_or(vec![], |(_color, ranges)| {
1783 ranges.iter().cloned().collect::<Vec<_>>()
1784 });
1785
1786 cx.background_spawn(async move {
1787 let mut ranges = Vec::new();
1788
1789 let search_within_ranges = if search_within_ranges.is_empty() {
1790 vec![buffer.anchor_before(MultiBufferOffset(0))..buffer.anchor_after(buffer.len())]
1791 } else {
1792 search_within_ranges
1793 };
1794
1795 for range in search_within_ranges {
1796 for (search_buffer, search_range, excerpt_id, deleted_hunk_anchor) in
1797 buffer.range_to_buffer_ranges_with_deleted_hunks(range)
1798 {
1799 ranges.extend(
1800 query
1801 .search(
1802 search_buffer,
1803 Some(search_range.start.0..search_range.end.0),
1804 )
1805 .await
1806 .into_iter()
1807 .map(|match_range| {
1808 if let Some(deleted_hunk_anchor) = deleted_hunk_anchor {
1809 let start = search_buffer
1810 .anchor_after(search_range.start + match_range.start);
1811 let end = search_buffer
1812 .anchor_before(search_range.start + match_range.end);
1813 deleted_hunk_anchor.with_diff_base_anchor(start)
1814 ..deleted_hunk_anchor.with_diff_base_anchor(end)
1815 } else {
1816 let start = search_buffer
1817 .anchor_after(search_range.start + match_range.start);
1818 let end = search_buffer
1819 .anchor_before(search_range.start + match_range.end);
1820 Anchor::range_in_buffer(excerpt_id, start..end)
1821 }
1822 }),
1823 );
1824 }
1825 }
1826
1827 ranges
1828 })
1829 }
1830
1831 fn active_match_index(
1832 &mut self,
1833 direction: Direction,
1834 matches: &[Range<Anchor>],
1835 _: &mut Window,
1836 cx: &mut Context<Self>,
1837 ) -> Option<usize> {
1838 active_match_index(
1839 direction,
1840 matches,
1841 &self.selections.newest_anchor().head(),
1842 &self.buffer().read(cx).snapshot(cx),
1843 )
1844 }
1845
1846 fn search_bar_visibility_changed(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {
1847 self.expect_bounds_change = self.last_bounds;
1848 }
1849
1850 fn set_search_is_case_sensitive(
1851 &mut self,
1852 case_sensitive: Option<bool>,
1853 _cx: &mut Context<Self>,
1854 ) {
1855 self.select_next_is_case_sensitive = case_sensitive;
1856 }
1857}
1858
1859pub fn active_match_index(
1860 direction: Direction,
1861 ranges: &[Range<Anchor>],
1862 cursor: &Anchor,
1863 buffer: &MultiBufferSnapshot,
1864) -> Option<usize> {
1865 if ranges.is_empty() {
1866 None
1867 } else {
1868 let r = ranges.binary_search_by(|probe| {
1869 if probe.end.cmp(cursor, buffer).is_lt() {
1870 Ordering::Less
1871 } else if probe.start.cmp(cursor, buffer).is_gt() {
1872 Ordering::Greater
1873 } else {
1874 Ordering::Equal
1875 }
1876 });
1877 match direction {
1878 Direction::Prev => match r {
1879 Ok(i) => Some(i),
1880 Err(i) => Some(i.saturating_sub(1)),
1881 },
1882 Direction::Next => match r {
1883 Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
1884 },
1885 }
1886 }
1887}
1888
1889pub fn entry_label_color(selected: bool) -> Color {
1890 if selected {
1891 Color::Default
1892 } else {
1893 Color::Muted
1894 }
1895}
1896
1897pub fn entry_diagnostic_aware_icon_name_and_color(
1898 diagnostic_severity: Option<DiagnosticSeverity>,
1899) -> Option<(IconName, Color)> {
1900 match diagnostic_severity {
1901 Some(DiagnosticSeverity::ERROR) => Some((IconName::Close, Color::Error)),
1902 Some(DiagnosticSeverity::WARNING) => Some((IconName::Triangle, Color::Warning)),
1903 _ => None,
1904 }
1905}
1906
1907pub fn entry_diagnostic_aware_icon_decoration_and_color(
1908 diagnostic_severity: Option<DiagnosticSeverity>,
1909) -> Option<(IconDecorationKind, Color)> {
1910 match diagnostic_severity {
1911 Some(DiagnosticSeverity::ERROR) => Some((IconDecorationKind::X, Color::Error)),
1912 Some(DiagnosticSeverity::WARNING) => Some((IconDecorationKind::Triangle, Color::Warning)),
1913 _ => None,
1914 }
1915}
1916
1917pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
1918 let tracked = git_status.index + git_status.worktree;
1919 if git_status.conflict > 0 {
1920 Color::Conflict
1921 } else if tracked.modified > 0 {
1922 Color::Modified
1923 } else if tracked.added > 0 || git_status.untracked > 0 {
1924 Color::Created
1925 } else if ignored {
1926 Color::Ignored
1927 } else {
1928 entry_label_color(selected)
1929 }
1930}
1931
1932fn path_for_buffer<'a>(
1933 buffer: &Entity<MultiBuffer>,
1934 height: usize,
1935 include_filename: bool,
1936 cx: &'a App,
1937) -> Option<Cow<'a, str>> {
1938 let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
1939 path_for_file(file, height, include_filename, cx)
1940}
1941
1942fn path_for_file<'a>(
1943 file: &'a Arc<dyn language::File>,
1944 mut height: usize,
1945 include_filename: bool,
1946 cx: &'a App,
1947) -> Option<Cow<'a, str>> {
1948 if project::File::from_dyn(Some(file)).is_none() {
1949 return None;
1950 }
1951
1952 let file = file.as_ref();
1953 // Ensure we always render at least the filename.
1954 height += 1;
1955
1956 let mut prefix = file.path().as_ref();
1957 while height > 0 {
1958 if let Some(parent) = prefix.parent() {
1959 prefix = parent;
1960 height -= 1;
1961 } else {
1962 break;
1963 }
1964 }
1965
1966 // The full_path method allocates, so avoid calling it if height is zero.
1967 if height > 0 {
1968 let mut full_path = file.full_path(cx);
1969 if !include_filename {
1970 if !full_path.pop() {
1971 return None;
1972 }
1973 }
1974 Some(full_path.to_string_lossy().into_owned().into())
1975 } else {
1976 let mut path = file.path().strip_prefix(prefix).ok()?;
1977 if !include_filename {
1978 path = path.parent()?;
1979 }
1980 Some(path.display(file.path_style(cx)))
1981 }
1982}
1983
1984/// Restores serialized buffer contents by overwriting the buffer with saved text.
1985/// This is somewhat wasteful since we load the whole buffer from disk then overwrite it,
1986/// but keeps implementation simple as we don't need to persist all metadata from loading
1987/// (git diff base, etc.).
1988fn restore_serialized_buffer_contents(
1989 buffer: &mut Buffer,
1990 contents: String,
1991 mtime: Option<MTime>,
1992 cx: &mut Context<Buffer>,
1993) {
1994 // If we did restore an mtime, store it on the buffer so that
1995 // the next edit will mark the buffer as dirty/conflicted.
1996 if mtime.is_some() {
1997 buffer.did_reload(buffer.version(), buffer.line_ending(), mtime, cx);
1998 }
1999 buffer.set_text(contents, cx);
2000 if let Some(entry) = buffer.peek_undo_stack() {
2001 buffer.forget_transaction(entry.transaction_id());
2002 }
2003}
2004
2005#[cfg(test)]
2006mod tests {
2007 use crate::editor_tests::init_test;
2008 use fs::Fs;
2009
2010 use super::*;
2011 use fs::MTime;
2012 use gpui::{App, VisualTestContext};
2013 use language::TestFile;
2014 use project::FakeFs;
2015 use std::path::{Path, PathBuf};
2016 use util::{path, rel_path::RelPath};
2017
2018 #[gpui::test]
2019 fn test_path_for_file(cx: &mut App) {
2020 let file: Arc<dyn language::File> = Arc::new(TestFile {
2021 path: RelPath::empty().into(),
2022 root_name: String::new(),
2023 local_root: None,
2024 });
2025 assert_eq!(path_for_file(&file, 0, false, cx), None);
2026 }
2027
2028 async fn deserialize_editor(
2029 item_id: ItemId,
2030 workspace_id: WorkspaceId,
2031 workspace: Entity<Workspace>,
2032 project: Entity<Project>,
2033 cx: &mut VisualTestContext,
2034 ) -> Entity<Editor> {
2035 workspace
2036 .update_in(cx, |workspace, window, cx| {
2037 let pane = workspace.active_pane();
2038 pane.update(cx, |_, cx| {
2039 Editor::deserialize(
2040 project.clone(),
2041 workspace.weak_handle(),
2042 workspace_id,
2043 item_id,
2044 window,
2045 cx,
2046 )
2047 })
2048 })
2049 .await
2050 .unwrap()
2051 }
2052
2053 #[gpui::test]
2054 async fn test_deserialize(cx: &mut gpui::TestAppContext) {
2055 init_test(cx, |_| {});
2056
2057 let fs = FakeFs::new(cx.executor());
2058 fs.insert_file(path!("/file.rs"), Default::default()).await;
2059
2060 // Test case 1: Deserialize with path and contents
2061 {
2062 let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
2063 let (workspace, cx) =
2064 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2065 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
2066 let item_id = 1234 as ItemId;
2067 let mtime = fs
2068 .metadata(Path::new(path!("/file.rs")))
2069 .await
2070 .unwrap()
2071 .unwrap()
2072 .mtime;
2073
2074 let serialized_editor = SerializedEditor {
2075 abs_path: Some(PathBuf::from(path!("/file.rs"))),
2076 contents: Some("fn main() {}".to_string()),
2077 language: Some("Rust".to_string()),
2078 mtime: Some(mtime),
2079 };
2080
2081 DB.save_serialized_editor(item_id, workspace_id, serialized_editor.clone())
2082 .await
2083 .unwrap();
2084
2085 let deserialized =
2086 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
2087
2088 deserialized.update(cx, |editor, cx| {
2089 assert_eq!(editor.text(cx), "fn main() {}");
2090 assert!(editor.is_dirty(cx));
2091 assert!(!editor.has_conflict(cx));
2092 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
2093 assert!(buffer.file().is_some());
2094 });
2095 }
2096
2097 // Test case 2: Deserialize with only path
2098 {
2099 let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
2100 let (workspace, cx) =
2101 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2102
2103 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
2104
2105 let item_id = 5678 as ItemId;
2106 let serialized_editor = SerializedEditor {
2107 abs_path: Some(PathBuf::from(path!("/file.rs"))),
2108 contents: None,
2109 language: None,
2110 mtime: None,
2111 };
2112
2113 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
2114 .await
2115 .unwrap();
2116
2117 let deserialized =
2118 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
2119
2120 deserialized.update(cx, |editor, cx| {
2121 assert_eq!(editor.text(cx), ""); // The file should be empty as per our initial setup
2122 assert!(!editor.is_dirty(cx));
2123 assert!(!editor.has_conflict(cx));
2124
2125 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
2126 assert!(buffer.file().is_some());
2127 });
2128 }
2129
2130 // Test case 3: Deserialize with no path (untitled buffer, with content and language)
2131 {
2132 let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
2133 // Add Rust to the language, so that we can restore the language of the buffer
2134 project.read_with(cx, |project, _| {
2135 project.languages().add(languages::rust_lang())
2136 });
2137
2138 let (workspace, cx) =
2139 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2140
2141 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
2142
2143 let item_id = 9012 as ItemId;
2144 let serialized_editor = SerializedEditor {
2145 abs_path: None,
2146 contents: Some("hello".to_string()),
2147 language: Some("Rust".to_string()),
2148 mtime: None,
2149 };
2150
2151 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
2152 .await
2153 .unwrap();
2154
2155 let deserialized =
2156 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
2157
2158 deserialized.update(cx, |editor, cx| {
2159 assert_eq!(editor.text(cx), "hello");
2160 assert!(editor.is_dirty(cx)); // The editor should be dirty for an untitled buffer
2161
2162 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
2163 assert_eq!(
2164 buffer.language().map(|lang| lang.name()),
2165 Some("Rust".into())
2166 ); // Language should be set to Rust
2167 assert!(buffer.file().is_none()); // The buffer should not have an associated file
2168 });
2169 }
2170
2171 // Test case 4: Deserialize with path, content, and old mtime
2172 {
2173 let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
2174 let (workspace, cx) =
2175 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2176
2177 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
2178
2179 let item_id = 9345 as ItemId;
2180 let old_mtime = MTime::from_seconds_and_nanos(0, 50);
2181 let serialized_editor = SerializedEditor {
2182 abs_path: Some(PathBuf::from(path!("/file.rs"))),
2183 contents: Some("fn main() {}".to_string()),
2184 language: Some("Rust".to_string()),
2185 mtime: Some(old_mtime),
2186 };
2187
2188 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
2189 .await
2190 .unwrap();
2191
2192 let deserialized =
2193 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
2194
2195 deserialized.update(cx, |editor, cx| {
2196 assert_eq!(editor.text(cx), "fn main() {}");
2197 assert!(editor.has_conflict(cx)); // The editor should have a conflict
2198 });
2199 }
2200
2201 // Test case 5: Deserialize with no path, no content, no language, and no old mtime (new, empty, unsaved buffer)
2202 {
2203 let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
2204 let (workspace, cx) =
2205 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2206
2207 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
2208
2209 let item_id = 10000 as ItemId;
2210 let serialized_editor = SerializedEditor {
2211 abs_path: None,
2212 contents: None,
2213 language: None,
2214 mtime: None,
2215 };
2216
2217 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
2218 .await
2219 .unwrap();
2220
2221 let deserialized =
2222 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
2223
2224 deserialized.update(cx, |editor, cx| {
2225 assert_eq!(editor.text(cx), "");
2226 assert!(!editor.is_dirty(cx));
2227 assert!(!editor.has_conflict(cx));
2228
2229 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
2230 assert!(buffer.file().is_none());
2231 });
2232 }
2233
2234 // Test case 6: Deserialize with path and contents in an empty workspace (no worktree)
2235 // This tests the hot-exit scenario where a file is opened in an empty workspace
2236 // and has unsaved changes that should be restored.
2237 {
2238 let fs = FakeFs::new(cx.executor());
2239 fs.insert_file(path!("/standalone.rs"), "original content".into())
2240 .await;
2241
2242 // Create an empty project with no worktrees
2243 let project = Project::test(fs.clone(), [], cx).await;
2244 let (workspace, cx) =
2245 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
2246
2247 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
2248 let item_id = 11000 as ItemId;
2249
2250 let mtime = fs
2251 .metadata(Path::new(path!("/standalone.rs")))
2252 .await
2253 .unwrap()
2254 .unwrap()
2255 .mtime;
2256
2257 // Simulate serialized state: file with unsaved changes
2258 let serialized_editor = SerializedEditor {
2259 abs_path: Some(PathBuf::from(path!("/standalone.rs"))),
2260 contents: Some("modified content".to_string()),
2261 language: Some("Rust".to_string()),
2262 mtime: Some(mtime),
2263 };
2264
2265 DB.save_serialized_editor(item_id, workspace_id, serialized_editor)
2266 .await
2267 .unwrap();
2268
2269 let deserialized =
2270 deserialize_editor(item_id, workspace_id, workspace, project, cx).await;
2271
2272 deserialized.update(cx, |editor, cx| {
2273 // The editor should have the serialized contents, not the disk contents
2274 assert_eq!(editor.text(cx), "modified content");
2275 assert!(editor.is_dirty(cx));
2276 assert!(!editor.has_conflict(cx));
2277
2278 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
2279 assert!(buffer.file().is_some());
2280 });
2281 }
2282 }
2283}