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