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