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