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