1use crate::{
2 editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
3 Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
4 NavigationData, SearchWithinRange, ToPoint as _,
5};
6use anyhow::{anyhow, Context as _, Result};
7use collections::HashSet;
8use futures::future::try_join_all;
9use git::repository::GitFileStatus;
10use gpui::{
11 point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
12 IntoElement, Model, ParentElement, Pixels, SharedString, Styled, Task, View, ViewContext,
13 VisualContext, WeakView, WindowContext,
14};
15use language::{
16 proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
17 Point, SelectionGoal,
18};
19use multi_buffer::AnchorRangeExt;
20use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
21use rpc::proto::{self, update_view, PeerId};
22use settings::Settings;
23use workspace::item::{ItemSettings, TabContentParams};
24
25use std::{
26 any::TypeId,
27 borrow::Cow,
28 cmp::{self, Ordering},
29 iter,
30 ops::Range,
31 path::Path,
32 sync::Arc,
33};
34use text::{BufferId, Selection};
35use theme::{Theme, ThemeSettings};
36use ui::{h_flex, prelude::*, Label};
37use util::{paths::PathExt, ResultExt, TryFutureExt};
38use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
39use workspace::{
40 item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
41 searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
42 ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
43};
44
45pub const MAX_TAB_TITLE_LEN: usize = 24;
46
47impl FollowableItem for Editor {
48 fn remote_id(&self) -> Option<ViewId> {
49 self.remote_id
50 }
51
52 fn from_state_proto(
53 pane: View<workspace::Pane>,
54 workspace: View<Workspace>,
55 remote_id: ViewId,
56 state: &mut Option<proto::view::Variant>,
57 cx: &mut WindowContext,
58 ) -> Option<Task<Result<View<Self>>>> {
59 let project = workspace.read(cx).project().to_owned();
60 let Some(proto::view::Variant::Editor(_)) = state else {
61 return None;
62 };
63 let Some(proto::view::Variant::Editor(state)) = state.take() else {
64 unreachable!()
65 };
66
67 let client = project.read(cx).client();
68 let replica_id = project.read(cx).replica_id();
69 let buffer_ids = state
70 .excerpts
71 .iter()
72 .map(|excerpt| excerpt.buffer_id)
73 .collect::<HashSet<_>>();
74 let buffers = project.update(cx, |project, cx| {
75 buffer_ids
76 .iter()
77 .map(|id| BufferId::new(*id).map(|id| project.open_buffer_by_id(id, cx)))
78 .collect::<Result<Vec<_>>>()
79 });
80
81 let pane = pane.downgrade();
82 Some(cx.spawn(|mut cx| async move {
83 let mut buffers = futures::future::try_join_all(buffers?)
84 .await
85 .debug_assert_ok("leaders don't share views for unshared buffers")?;
86
87 let editor = pane.update(&mut cx, |pane, cx| {
88 let mut editors = pane.items_of_type::<Self>();
89 editors.find(|editor| {
90 let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
91 let singleton_buffer_matches = state.singleton
92 && buffers.first()
93 == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
94 ids_match || singleton_buffer_matches
95 })
96 })?;
97
98 let editor = if let Some(editor) = editor {
99 editor
100 } else {
101 pane.update(&mut cx, |_, cx| {
102 let multibuffer = cx.new_model(|cx| {
103 let mut multibuffer;
104 if state.singleton && buffers.len() == 1 {
105 multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
106 } else {
107 multibuffer =
108 MultiBuffer::new(replica_id, project.read(cx).capability());
109 let mut excerpts = state.excerpts.into_iter().peekable();
110 while let Some(excerpt) = excerpts.peek() {
111 let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
112 continue;
113 };
114 let buffer_excerpts = iter::from_fn(|| {
115 let excerpt = excerpts.peek()?;
116 (excerpt.buffer_id == u64::from(buffer_id))
117 .then(|| excerpts.next().unwrap())
118 });
119 let buffer =
120 buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
121 if let Some(buffer) = buffer {
122 multibuffer.push_excerpts(
123 buffer.clone(),
124 buffer_excerpts.filter_map(deserialize_excerpt_range),
125 cx,
126 );
127 }
128 }
129 };
130
131 if let Some(title) = &state.title {
132 multibuffer = multibuffer.with_title(title.clone())
133 }
134
135 multibuffer
136 });
137
138 cx.new_view(|cx| {
139 let mut editor =
140 Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
141 editor.remote_id = Some(remote_id);
142 editor
143 })
144 })?
145 };
146
147 update_editor_from_message(
148 editor.downgrade(),
149 project,
150 proto::update_view::Editor {
151 selections: state.selections,
152 pending_selection: state.pending_selection,
153 scroll_top_anchor: state.scroll_top_anchor,
154 scroll_x: state.scroll_x,
155 scroll_y: state.scroll_y,
156 ..Default::default()
157 },
158 &mut cx,
159 )
160 .await?;
161
162 Ok(editor)
163 }))
164 }
165
166 fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
167 self.leader_peer_id = leader_peer_id;
168 if self.leader_peer_id.is_some() {
169 self.buffer.update(cx, |buffer, cx| {
170 buffer.remove_active_selections(cx);
171 });
172 } else if self.focus_handle.is_focused(cx) {
173 self.buffer.update(cx, |buffer, cx| {
174 buffer.set_active_selections(
175 &self.selections.disjoint_anchors(),
176 self.selections.line_mode,
177 self.cursor_shape,
178 cx,
179 );
180 });
181 }
182 cx.notify();
183 }
184
185 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
186 let buffer = self.buffer.read(cx);
187 if buffer
188 .as_singleton()
189 .and_then(|buffer| buffer.read(cx).file())
190 .map_or(false, |file| file.is_private())
191 {
192 return None;
193 }
194
195 let scroll_anchor = self.scroll_manager.anchor();
196 let excerpts = buffer
197 .read(cx)
198 .excerpts()
199 .map(|(id, buffer, range)| proto::Excerpt {
200 id: id.to_proto(),
201 buffer_id: buffer.remote_id().into(),
202 context_start: Some(serialize_text_anchor(&range.context.start)),
203 context_end: Some(serialize_text_anchor(&range.context.end)),
204 primary_start: range
205 .primary
206 .as_ref()
207 .map(|range| serialize_text_anchor(&range.start)),
208 primary_end: range
209 .primary
210 .as_ref()
211 .map(|range| serialize_text_anchor(&range.end)),
212 })
213 .collect();
214
215 Some(proto::view::Variant::Editor(proto::view::Editor {
216 singleton: buffer.is_singleton(),
217 title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
218 excerpts,
219 scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
220 scroll_x: scroll_anchor.offset.x,
221 scroll_y: scroll_anchor.offset.y,
222 selections: self
223 .selections
224 .disjoint_anchors()
225 .iter()
226 .map(serialize_selection)
227 .collect(),
228 pending_selection: self
229 .selections
230 .pending_anchor()
231 .as_ref()
232 .map(serialize_selection),
233 }))
234 }
235
236 fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
237 match event {
238 EditorEvent::Edited => Some(FollowEvent::Unfollow),
239 EditorEvent::SelectionsChanged { local }
240 | EditorEvent::ScrollPositionChanged { local, .. } => {
241 if *local {
242 Some(FollowEvent::Unfollow)
243 } else {
244 None
245 }
246 }
247 _ => None,
248 }
249 }
250
251 fn add_event_to_update_proto(
252 &self,
253 event: &EditorEvent,
254 update: &mut Option<proto::update_view::Variant>,
255 cx: &WindowContext,
256 ) -> bool {
257 let update =
258 update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
259
260 match update {
261 proto::update_view::Variant::Editor(update) => match event {
262 EditorEvent::ExcerptsAdded {
263 buffer,
264 predecessor,
265 excerpts,
266 } => {
267 let buffer_id = buffer.read(cx).remote_id();
268 let mut excerpts = excerpts.iter();
269 if let Some((id, range)) = excerpts.next() {
270 update.inserted_excerpts.push(proto::ExcerptInsertion {
271 previous_excerpt_id: Some(predecessor.to_proto()),
272 excerpt: serialize_excerpt(buffer_id, id, range),
273 });
274 update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
275 proto::ExcerptInsertion {
276 previous_excerpt_id: None,
277 excerpt: serialize_excerpt(buffer_id, id, range),
278 }
279 }))
280 }
281 true
282 }
283 EditorEvent::ExcerptsRemoved { ids } => {
284 update
285 .deleted_excerpts
286 .extend(ids.iter().map(ExcerptId::to_proto));
287 true
288 }
289 EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => {
290 let scroll_anchor = self.scroll_manager.anchor();
291 update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
292 update.scroll_x = scroll_anchor.offset.x;
293 update.scroll_y = scroll_anchor.offset.y;
294 true
295 }
296 EditorEvent::SelectionsChanged { .. } => {
297 update.selections = self
298 .selections
299 .disjoint_anchors()
300 .iter()
301 .map(serialize_selection)
302 .collect();
303 update.pending_selection = self
304 .selections
305 .pending_anchor()
306 .as_ref()
307 .map(serialize_selection);
308 true
309 }
310 _ => false,
311 },
312 }
313 }
314
315 fn apply_update_proto(
316 &mut self,
317 project: &Model<Project>,
318 message: update_view::Variant,
319 cx: &mut ViewContext<Self>,
320 ) -> Task<Result<()>> {
321 let update_view::Variant::Editor(message) = message;
322 let project = project.clone();
323 cx.spawn(|this, mut cx| async move {
324 update_editor_from_message(this, project, message, &mut cx).await
325 })
326 }
327
328 fn is_project_item(&self, _cx: &WindowContext) -> bool {
329 true
330 }
331}
332
333async fn update_editor_from_message(
334 this: WeakView<Editor>,
335 project: Model<Project>,
336 message: proto::update_view::Editor,
337 cx: &mut AsyncWindowContext,
338) -> Result<()> {
339 // Open all of the buffers of which excerpts were added to the editor.
340 let inserted_excerpt_buffer_ids = message
341 .inserted_excerpts
342 .iter()
343 .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
344 .collect::<HashSet<_>>();
345 let inserted_excerpt_buffers = project.update(cx, |project, cx| {
346 inserted_excerpt_buffer_ids
347 .into_iter()
348 .map(|id| BufferId::new(id).map(|id| project.open_buffer_by_id(id, cx)))
349 .collect::<Result<Vec<_>>>()
350 })??;
351 let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
352
353 // Update the editor's excerpts.
354 this.update(cx, |editor, cx| {
355 editor.buffer.update(cx, |multibuffer, cx| {
356 let mut removed_excerpt_ids = message
357 .deleted_excerpts
358 .into_iter()
359 .map(ExcerptId::from_proto)
360 .collect::<Vec<_>>();
361 removed_excerpt_ids.sort_by({
362 let multibuffer = multibuffer.read(cx);
363 move |a, b| a.cmp(&b, &multibuffer)
364 });
365
366 let mut insertions = message.inserted_excerpts.into_iter().peekable();
367 while let Some(insertion) = insertions.next() {
368 let Some(excerpt) = insertion.excerpt else {
369 continue;
370 };
371 let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
372 continue;
373 };
374 let buffer_id = BufferId::new(excerpt.buffer_id)?;
375 let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
376 continue;
377 };
378
379 let adjacent_excerpts = iter::from_fn(|| {
380 let insertion = insertions.peek()?;
381 if insertion.previous_excerpt_id.is_none()
382 && insertion.excerpt.as_ref()?.buffer_id == u64::from(buffer_id)
383 {
384 insertions.next()?.excerpt
385 } else {
386 None
387 }
388 });
389
390 multibuffer.insert_excerpts_with_ids_after(
391 ExcerptId::from_proto(previous_excerpt_id),
392 buffer,
393 [excerpt]
394 .into_iter()
395 .chain(adjacent_excerpts)
396 .filter_map(|excerpt| {
397 Some((
398 ExcerptId::from_proto(excerpt.id),
399 deserialize_excerpt_range(excerpt)?,
400 ))
401 }),
402 cx,
403 );
404 }
405
406 multibuffer.remove_excerpts(removed_excerpt_ids, cx);
407 Result::<(), anyhow::Error>::Ok(())
408 })
409 })??;
410
411 // Deserialize the editor state.
412 let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
413 let buffer = editor.buffer.read(cx).read(cx);
414 let selections = message
415 .selections
416 .into_iter()
417 .filter_map(|selection| deserialize_selection(&buffer, selection))
418 .collect::<Vec<_>>();
419 let pending_selection = message
420 .pending_selection
421 .and_then(|selection| deserialize_selection(&buffer, selection));
422 let scroll_top_anchor = message
423 .scroll_top_anchor
424 .and_then(|anchor| deserialize_anchor(&buffer, anchor));
425 anyhow::Ok((selections, pending_selection, scroll_top_anchor))
426 })??;
427
428 // Wait until the buffer has received all of the operations referenced by
429 // the editor's new state.
430 this.update(cx, |editor, cx| {
431 editor.buffer.update(cx, |buffer, cx| {
432 buffer.wait_for_anchors(
433 selections
434 .iter()
435 .chain(pending_selection.as_ref())
436 .flat_map(|selection| [selection.start, selection.end])
437 .chain(scroll_top_anchor),
438 cx,
439 )
440 })
441 })?
442 .await?;
443
444 // Update the editor's state.
445 this.update(cx, |editor, cx| {
446 if !selections.is_empty() || pending_selection.is_some() {
447 editor.set_selections_from_remote(selections, pending_selection, cx);
448 editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
449 } else if let Some(scroll_top_anchor) = scroll_top_anchor {
450 editor.set_scroll_anchor_remote(
451 ScrollAnchor {
452 anchor: scroll_top_anchor,
453 offset: point(message.scroll_x, message.scroll_y),
454 },
455 cx,
456 );
457 }
458 })?;
459 Ok(())
460}
461
462fn serialize_excerpt(
463 buffer_id: BufferId,
464 id: &ExcerptId,
465 range: &ExcerptRange<language::Anchor>,
466) -> Option<proto::Excerpt> {
467 Some(proto::Excerpt {
468 id: id.to_proto(),
469 buffer_id: buffer_id.into(),
470 context_start: Some(serialize_text_anchor(&range.context.start)),
471 context_end: Some(serialize_text_anchor(&range.context.end)),
472 primary_start: range
473 .primary
474 .as_ref()
475 .map(|r| serialize_text_anchor(&r.start)),
476 primary_end: range
477 .primary
478 .as_ref()
479 .map(|r| serialize_text_anchor(&r.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 Some(ExcerptRange { context, primary })
514}
515
516fn deserialize_selection(
517 buffer: &MultiBufferSnapshot,
518 selection: proto::Selection,
519) -> Option<Selection<Anchor>> {
520 Some(Selection {
521 id: selection.id as usize,
522 start: deserialize_anchor(buffer, selection.start?)?,
523 end: deserialize_anchor(buffer, selection.end?)?,
524 reversed: selection.reversed,
525 goal: SelectionGoal::None,
526 })
527}
528
529fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option<Anchor> {
530 let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id);
531 Some(Anchor {
532 excerpt_id,
533 text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
534 buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
535 })
536}
537
538impl Item for Editor {
539 type Event = EditorEvent;
540
541 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
542 if let Ok(data) = data.downcast::<NavigationData>() {
543 let newest_selection = self.selections.newest::<Point>(cx);
544 let buffer = self.buffer.read(cx).read(cx);
545 let offset = if buffer.can_resolve(&data.cursor_anchor) {
546 data.cursor_anchor.to_point(&buffer)
547 } else {
548 buffer.clip_point(data.cursor_position, Bias::Left)
549 };
550
551 let mut scroll_anchor = data.scroll_anchor;
552 if !buffer.can_resolve(&scroll_anchor.anchor) {
553 scroll_anchor.anchor = buffer.anchor_before(
554 buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
555 );
556 }
557
558 drop(buffer);
559
560 if newest_selection.head() == offset {
561 false
562 } else {
563 let nav_history = self.nav_history.take();
564 self.set_scroll_anchor(scroll_anchor, cx);
565 self.change_selections(Some(Autoscroll::fit()), cx, |s| {
566 s.select_ranges([offset..offset])
567 });
568 self.nav_history = nav_history;
569 true
570 }
571 } else {
572 false
573 }
574 }
575
576 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
577 let file_path = self
578 .buffer()
579 .read(cx)
580 .as_singleton()?
581 .read(cx)
582 .file()
583 .and_then(|f| f.as_local())?
584 .abs_path(cx);
585
586 let file_path = file_path.compact().to_string_lossy().to_string();
587
588 Some(file_path.into())
589 }
590
591 fn telemetry_event_text(&self) -> Option<&'static str> {
592 None
593 }
594
595 fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString> {
596 let path = path_for_buffer(&self.buffer, detail, true, cx)?;
597 Some(path.to_string_lossy().to_string().into())
598 }
599
600 fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
601 let label_color = if ItemSettings::get_global(cx).git_status {
602 self.buffer()
603 .read(cx)
604 .as_singleton()
605 .and_then(|buffer| buffer.read(cx).project_path(cx))
606 .and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx))
607 .map(|entry| {
608 entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
609 })
610 .unwrap_or_else(|| entry_label_color(params.selected))
611 } else {
612 entry_label_color(params.selected)
613 };
614
615 let description = params.detail.and_then(|detail| {
616 let path = path_for_buffer(&self.buffer, detail, false, cx)?;
617 let description = path.to_string_lossy();
618 let description = description.trim();
619
620 if description.is_empty() {
621 return None;
622 }
623
624 Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN))
625 });
626
627 h_flex()
628 .gap_2()
629 .child(
630 Label::new(self.title(cx).to_string())
631 .color(label_color)
632 .italic(params.preview),
633 )
634 .when_some(description, |this, description| {
635 this.child(
636 Label::new(description)
637 .size(LabelSize::XSmall)
638 .color(Color::Muted),
639 )
640 })
641 .into_any_element()
642 }
643
644 fn for_each_project_item(
645 &self,
646 cx: &AppContext,
647 f: &mut dyn FnMut(EntityId, &dyn project::Item),
648 ) {
649 self.buffer
650 .read(cx)
651 .for_each_buffer(|buffer| f(buffer.entity_id(), buffer.read(cx)));
652 }
653
654 fn is_singleton(&self, cx: &AppContext) -> bool {
655 self.buffer.read(cx).is_singleton()
656 }
657
658 fn clone_on_split(
659 &self,
660 _workspace_id: Option<WorkspaceId>,
661 cx: &mut ViewContext<Self>,
662 ) -> Option<View<Editor>>
663 where
664 Self: Sized,
665 {
666 Some(cx.new_view(|cx| self.clone(cx)))
667 }
668
669 fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
670 self.nav_history = Some(history);
671 }
672
673 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
674 let selection = self.selections.newest_anchor();
675 self.push_to_nav_history(selection.head(), None, cx);
676 }
677
678 fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
679 self.hide_hovered_link(cx);
680 }
681
682 fn is_dirty(&self, cx: &AppContext) -> bool {
683 self.buffer().read(cx).read(cx).is_dirty()
684 }
685
686 fn has_conflict(&self, cx: &AppContext) -> bool {
687 self.buffer().read(cx).read(cx).has_conflict()
688 }
689
690 fn can_save(&self, cx: &AppContext) -> bool {
691 let buffer = &self.buffer().read(cx);
692 if let Some(buffer) = buffer.as_singleton() {
693 buffer.read(cx).project_path(cx).is_some()
694 } else {
695 true
696 }
697 }
698
699 fn save(
700 &mut self,
701 format: bool,
702 project: Model<Project>,
703 cx: &mut ViewContext<Self>,
704 ) -> Task<Result<()>> {
705 self.report_editor_event("save", None, cx);
706 let buffers = self.buffer().clone().read(cx).all_buffers();
707 cx.spawn(|this, mut cx| async move {
708 if format {
709 this.update(&mut cx, |editor, cx| {
710 editor.perform_format(project.clone(), FormatTrigger::Save, cx)
711 })?
712 .await?;
713 }
714
715 if buffers.len() == 1 {
716 // Apply full save routine for singleton buffers, to allow to `touch` the file via the editor.
717 project
718 .update(&mut cx, |project, cx| project.save_buffers(buffers, cx))?
719 .await?;
720 } else {
721 // For multi-buffers, only format and save the buffers with changes.
722 // For clean buffers, we simulate saving by calling `Buffer::did_save`,
723 // so that language servers or other downstream listeners of save events get notified.
724 let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
725 buffer
726 .update(&mut cx, |buffer, _| {
727 buffer.is_dirty() || buffer.has_conflict()
728 })
729 .unwrap_or(false)
730 });
731
732 project
733 .update(&mut cx, |project, cx| {
734 project.save_buffers(dirty_buffers, cx)
735 })?
736 .await?;
737 for buffer in clean_buffers {
738 buffer
739 .update(&mut cx, |buffer, cx| {
740 let version = buffer.saved_version().clone();
741 let mtime = buffer.saved_mtime();
742 buffer.did_save(version, mtime, cx);
743 })
744 .ok();
745 }
746 }
747
748 Ok(())
749 })
750 }
751
752 fn save_as(
753 &mut self,
754 project: Model<Project>,
755 path: ProjectPath,
756 cx: &mut ViewContext<Self>,
757 ) -> Task<Result<()>> {
758 let buffer = self
759 .buffer()
760 .read(cx)
761 .as_singleton()
762 .expect("cannot call save_as on an excerpt list");
763
764 let file_extension = path
765 .path
766 .extension()
767 .map(|a| a.to_string_lossy().to_string());
768 self.report_editor_event("save", file_extension, cx);
769
770 project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
771 }
772
773 fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
774 let buffer = self.buffer().clone();
775 let buffers = self.buffer.read(cx).all_buffers();
776 let reload_buffers =
777 project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx));
778 cx.spawn(|this, mut cx| async move {
779 let transaction = reload_buffers.log_err().await;
780 this.update(&mut cx, |editor, cx| {
781 editor.request_autoscroll(Autoscroll::fit(), cx)
782 })?;
783 buffer
784 .update(&mut cx, |buffer, cx| {
785 if let Some(transaction) = transaction {
786 if !buffer.is_singleton() {
787 buffer.push_transaction(&transaction.0, cx);
788 }
789 }
790 })
791 .ok();
792 Ok(())
793 })
794 }
795
796 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
797 Some(Box::new(handle.clone()))
798 }
799
800 fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<gpui::Point<Pixels>> {
801 self.pixel_position_of_newest_cursor
802 }
803
804 fn breadcrumb_location(&self) -> ToolbarItemLocation {
805 if self.show_breadcrumbs {
806 ToolbarItemLocation::PrimaryLeft
807 } else {
808 ToolbarItemLocation::Hidden
809 }
810 }
811
812 fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
813 let cursor = self.selections.newest_anchor().head();
814 let multibuffer = &self.buffer().read(cx);
815 let (buffer_id, symbols) =
816 multibuffer.symbols_containing(cursor, Some(&variant.syntax()), cx)?;
817 let buffer = multibuffer.buffer(buffer_id)?;
818
819 let buffer = buffer.read(cx);
820 let filename = buffer
821 .snapshot()
822 .resolve_file_path(
823 cx,
824 self.project
825 .as_ref()
826 .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
827 .unwrap_or_default(),
828 )
829 .map(|path| path.to_string_lossy().to_string())
830 .unwrap_or_else(|| "untitled".to_string());
831
832 let settings = ThemeSettings::get_global(cx);
833
834 let mut breadcrumbs = vec![BreadcrumbText {
835 text: filename,
836 highlights: None,
837 font: Some(settings.buffer_font.clone()),
838 }];
839
840 breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
841 text: symbol.text,
842 highlights: Some(symbol.highlight_ranges),
843 font: Some(settings.buffer_font.clone()),
844 }));
845 Some(breadcrumbs)
846 }
847
848 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
849 self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
850 let Some(workspace_id) = workspace.database_id() else {
851 return;
852 };
853
854 let item_id = cx.view().item_id().as_u64() as ItemId;
855
856 fn serialize(
857 buffer: Model<Buffer>,
858 workspace_id: WorkspaceId,
859 item_id: ItemId,
860 cx: &mut AppContext,
861 ) {
862 if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
863 let path = file.abs_path(cx);
864
865 cx.background_executor()
866 .spawn(async move {
867 DB.save_path(item_id, workspace_id, path.clone())
868 .await
869 .log_err()
870 })
871 .detach();
872 }
873 }
874
875 if let Some(buffer) = self.buffer().read(cx).as_singleton() {
876 serialize(buffer.clone(), workspace_id, item_id, cx);
877
878 cx.subscribe(&buffer, |this, buffer, event, cx| {
879 if let Some((_, Some(workspace_id))) = this.workspace.as_ref() {
880 if let language::Event::FileHandleChanged = event {
881 serialize(
882 buffer,
883 *workspace_id,
884 cx.view().item_id().as_u64() as ItemId,
885 cx,
886 );
887 }
888 }
889 })
890 .detach();
891 }
892 }
893
894 fn serialized_item_kind() -> Option<&'static str> {
895 Some("Editor")
896 }
897
898 fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
899 match event {
900 EditorEvent::Closed => f(ItemEvent::CloseItem),
901
902 EditorEvent::Saved | EditorEvent::TitleChanged => {
903 f(ItemEvent::UpdateTab);
904 f(ItemEvent::UpdateBreadcrumbs);
905 }
906
907 EditorEvent::Reparsed => {
908 f(ItemEvent::UpdateBreadcrumbs);
909 }
910
911 EditorEvent::SelectionsChanged { local } if *local => {
912 f(ItemEvent::UpdateBreadcrumbs);
913 }
914
915 EditorEvent::DirtyChanged => {
916 f(ItemEvent::UpdateTab);
917 }
918
919 EditorEvent::BufferEdited => {
920 f(ItemEvent::Edit);
921 f(ItemEvent::UpdateBreadcrumbs);
922 }
923
924 EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
925 f(ItemEvent::Edit);
926 }
927
928 _ => {}
929 }
930 }
931
932 fn deserialize(
933 project: Model<Project>,
934 _workspace: WeakView<Workspace>,
935 workspace_id: workspace::WorkspaceId,
936 item_id: ItemId,
937 cx: &mut ViewContext<Pane>,
938 ) -> Task<Result<View<Self>>> {
939 let project_item: Result<_> = project.update(cx, |project, cx| {
940 // Look up the path with this key associated, create a self with that path
941 let path = DB
942 .get_path(item_id, workspace_id)?
943 .context("No path stored for this editor")?;
944
945 let (worktree, path) = project
946 .find_local_worktree(&path, cx)
947 .with_context(|| format!("No worktree for path: {path:?}"))?;
948 let project_path = ProjectPath {
949 worktree_id: worktree.read(cx).id(),
950 path: path.into(),
951 };
952
953 Ok(project.open_path(project_path, cx))
954 });
955
956 project_item
957 .map(|project_item| {
958 cx.spawn(|pane, mut cx| async move {
959 let (_, project_item) = project_item.await?;
960 let buffer = project_item
961 .downcast::<Buffer>()
962 .map_err(|_| anyhow!("Project item at stored path was not a buffer"))?;
963 pane.update(&mut cx, |_, cx| {
964 cx.new_view(|cx| {
965 let mut editor = Editor::for_buffer(buffer, Some(project), cx);
966
967 editor.read_scroll_position_from_db(item_id, workspace_id, cx);
968 editor
969 })
970 })
971 })
972 })
973 .unwrap_or_else(|error| Task::ready(Err(error)))
974 }
975}
976
977impl ProjectItem for Editor {
978 type Item = Buffer;
979
980 fn for_project_item(
981 project: Model<Project>,
982 buffer: Model<Buffer>,
983 cx: &mut ViewContext<Self>,
984 ) -> Self {
985 Self::for_buffer(buffer, Some(project), cx)
986 }
987}
988
989impl EventEmitter<SearchEvent> for Editor {}
990
991pub(crate) enum BufferSearchHighlights {}
992impl SearchableItem for Editor {
993 type Match = Range<Anchor>;
994
995 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
996 self.clear_background_highlights::<BufferSearchHighlights>(cx);
997 }
998
999 fn update_matches(&mut self, matches: &[Range<Anchor>], cx: &mut ViewContext<Self>) {
1000 self.highlight_background::<BufferSearchHighlights>(
1001 matches,
1002 |theme| theme.search_match_background,
1003 cx,
1004 );
1005 }
1006
1007 fn has_filtered_search_ranges(&mut self) -> bool {
1008 self.has_background_highlights::<SearchWithinRange>()
1009 }
1010
1011 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
1012 let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
1013 let snapshot = &self.snapshot(cx).buffer_snapshot;
1014 let selection = self.selections.newest::<usize>(cx);
1015
1016 match setting {
1017 SeedQuerySetting::Never => String::new(),
1018 SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
1019 snapshot
1020 .text_for_range(selection.start..selection.end)
1021 .collect()
1022 }
1023 SeedQuerySetting::Selection => String::new(),
1024 SeedQuerySetting::Always => {
1025 let (range, kind) = snapshot.surrounding_word(selection.start);
1026 if kind == Some(CharKind::Word) {
1027 let text: String = snapshot.text_for_range(range).collect();
1028 if !text.trim().is_empty() {
1029 return text;
1030 }
1031 }
1032 String::new()
1033 }
1034 }
1035 }
1036
1037 fn activate_match(
1038 &mut self,
1039 index: usize,
1040 matches: &[Range<Anchor>],
1041 cx: &mut ViewContext<Self>,
1042 ) {
1043 self.unfold_ranges([matches[index].clone()], false, true, cx);
1044 let range = self.range_for_match(&matches[index]);
1045 self.change_selections(Some(Autoscroll::fit()), cx, |s| {
1046 s.select_ranges([range]);
1047 })
1048 }
1049
1050 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
1051 self.unfold_ranges(matches.to_vec(), false, false, cx);
1052 let mut ranges = Vec::new();
1053 for m in matches {
1054 ranges.push(self.range_for_match(&m))
1055 }
1056 self.change_selections(None, cx, |s| s.select_ranges(ranges));
1057 }
1058 fn replace(
1059 &mut self,
1060 identifier: &Self::Match,
1061 query: &SearchQuery,
1062 cx: &mut ViewContext<Self>,
1063 ) {
1064 let text = self.buffer.read(cx);
1065 let text = text.snapshot(cx);
1066 let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
1067 let text: Cow<_> = if text.len() == 1 {
1068 text.first().cloned().unwrap().into()
1069 } else {
1070 let joined_chunks = text.join("");
1071 joined_chunks.into()
1072 };
1073
1074 if let Some(replacement) = query.replacement_for(&text) {
1075 self.transact(cx, |this, cx| {
1076 this.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
1077 });
1078 }
1079 }
1080 fn match_index_for_direction(
1081 &mut self,
1082 matches: &[Range<Anchor>],
1083 current_index: usize,
1084 direction: Direction,
1085 count: usize,
1086 cx: &mut ViewContext<Self>,
1087 ) -> usize {
1088 let buffer = self.buffer().read(cx).snapshot(cx);
1089 let current_index_position = if self.selections.disjoint_anchors().len() == 1 {
1090 self.selections.newest_anchor().head()
1091 } else {
1092 matches[current_index].start
1093 };
1094
1095 let mut count = count % matches.len();
1096 if count == 0 {
1097 return current_index;
1098 }
1099 match direction {
1100 Direction::Next => {
1101 if matches[current_index]
1102 .start
1103 .cmp(¤t_index_position, &buffer)
1104 .is_gt()
1105 {
1106 count = count - 1
1107 }
1108
1109 (current_index + count) % matches.len()
1110 }
1111 Direction::Prev => {
1112 if matches[current_index]
1113 .end
1114 .cmp(¤t_index_position, &buffer)
1115 .is_lt()
1116 {
1117 count = count - 1;
1118 }
1119
1120 if current_index >= count {
1121 current_index - count
1122 } else {
1123 matches.len() - (count - current_index)
1124 }
1125 }
1126 }
1127 }
1128
1129 fn find_matches(
1130 &mut self,
1131 query: Arc<project::search::SearchQuery>,
1132 cx: &mut ViewContext<Self>,
1133 ) -> Task<Vec<Range<Anchor>>> {
1134 let buffer = self.buffer().read(cx).snapshot(cx);
1135 let search_within_ranges = self
1136 .background_highlights
1137 .get(&TypeId::of::<SearchWithinRange>())
1138 .map(|(_color, ranges)| {
1139 ranges
1140 .iter()
1141 .map(|range| range.to_offset(&buffer))
1142 .collect::<Vec<_>>()
1143 });
1144 cx.background_executor().spawn(async move {
1145 let mut ranges = Vec::new();
1146 if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
1147 if let Some(search_within_ranges) = search_within_ranges {
1148 for range in search_within_ranges {
1149 let offset = range.start;
1150 ranges.extend(
1151 query
1152 .search(excerpt_buffer, Some(range))
1153 .await
1154 .into_iter()
1155 .map(|range| {
1156 buffer.anchor_after(range.start + offset)
1157 ..buffer.anchor_before(range.end + offset)
1158 }),
1159 );
1160 }
1161 } else {
1162 ranges.extend(query.search(excerpt_buffer, None).await.into_iter().map(
1163 |range| buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
1164 ));
1165 }
1166 } else {
1167 for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
1168 if let Some(next_excerpt) = excerpt.next {
1169 let excerpt_range =
1170 next_excerpt.range.context.to_offset(&next_excerpt.buffer);
1171 ranges.extend(
1172 query
1173 .search(&next_excerpt.buffer, Some(excerpt_range.clone()))
1174 .await
1175 .into_iter()
1176 .map(|range| {
1177 let start = next_excerpt
1178 .buffer
1179 .anchor_after(excerpt_range.start + range.start);
1180 let end = next_excerpt
1181 .buffer
1182 .anchor_before(excerpt_range.start + range.end);
1183 buffer.anchor_in_excerpt(next_excerpt.id, start).unwrap()
1184 ..buffer.anchor_in_excerpt(next_excerpt.id, end).unwrap()
1185 }),
1186 );
1187 }
1188 }
1189 }
1190 ranges
1191 })
1192 }
1193
1194 fn active_match_index(
1195 &mut self,
1196 matches: &[Range<Anchor>],
1197 cx: &mut ViewContext<Self>,
1198 ) -> Option<usize> {
1199 active_match_index(
1200 matches,
1201 &self.selections.newest_anchor().head(),
1202 &self.buffer().read(cx).snapshot(cx),
1203 )
1204 }
1205
1206 fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {
1207 self.expect_bounds_change = self.last_bounds;
1208 }
1209}
1210
1211pub fn active_match_index(
1212 ranges: &[Range<Anchor>],
1213 cursor: &Anchor,
1214 buffer: &MultiBufferSnapshot,
1215) -> Option<usize> {
1216 if ranges.is_empty() {
1217 None
1218 } else {
1219 match ranges.binary_search_by(|probe| {
1220 if probe.end.cmp(cursor, buffer).is_lt() {
1221 Ordering::Less
1222 } else if probe.start.cmp(cursor, buffer).is_gt() {
1223 Ordering::Greater
1224 } else {
1225 Ordering::Equal
1226 }
1227 }) {
1228 Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
1229 }
1230 }
1231}
1232
1233pub fn entry_label_color(selected: bool) -> Color {
1234 if selected {
1235 Color::Default
1236 } else {
1237 Color::Muted
1238 }
1239}
1240
1241pub fn entry_git_aware_label_color(
1242 git_status: Option<GitFileStatus>,
1243 ignored: bool,
1244 selected: bool,
1245) -> Color {
1246 if ignored {
1247 Color::Ignored
1248 } else {
1249 match git_status {
1250 Some(GitFileStatus::Added) => Color::Created,
1251 Some(GitFileStatus::Modified) => Color::Modified,
1252 Some(GitFileStatus::Conflict) => Color::Conflict,
1253 None => entry_label_color(selected),
1254 }
1255 }
1256}
1257
1258fn path_for_buffer<'a>(
1259 buffer: &Model<MultiBuffer>,
1260 height: usize,
1261 include_filename: bool,
1262 cx: &'a AppContext,
1263) -> Option<Cow<'a, Path>> {
1264 let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
1265 path_for_file(file.as_ref(), height, include_filename, cx)
1266}
1267
1268fn path_for_file<'a>(
1269 file: &'a dyn language::File,
1270 mut height: usize,
1271 include_filename: bool,
1272 cx: &'a AppContext,
1273) -> Option<Cow<'a, Path>> {
1274 // Ensure we always render at least the filename.
1275 height += 1;
1276
1277 let mut prefix = file.path().as_ref();
1278 while height > 0 {
1279 if let Some(parent) = prefix.parent() {
1280 prefix = parent;
1281 height -= 1;
1282 } else {
1283 break;
1284 }
1285 }
1286
1287 // Here we could have just always used `full_path`, but that is very
1288 // allocation-heavy and so we try to use a `Cow<Path>` if we haven't
1289 // traversed all the way up to the worktree's root.
1290 if height > 0 {
1291 let full_path = file.full_path(cx);
1292 if include_filename {
1293 Some(full_path.into())
1294 } else {
1295 Some(full_path.parent()?.to_path_buf().into())
1296 }
1297 } else {
1298 let mut path = file.path().strip_prefix(prefix).ok()?;
1299 if !include_filename {
1300 path = path.parent()?;
1301 }
1302 Some(path.into())
1303 }
1304}
1305
1306#[cfg(test)]
1307mod tests {
1308 use super::*;
1309 use gpui::AppContext;
1310 use language::TestFile;
1311 use std::path::Path;
1312
1313 #[gpui::test]
1314 fn test_path_for_file(cx: &mut AppContext) {
1315 let file = TestFile {
1316 path: Path::new("").into(),
1317 root_name: String::new(),
1318 };
1319 assert_eq!(path_for_file(&file, 0, false, cx), None);
1320 }
1321}