items.rs

  1use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _};
  2use anyhow::Result;
  3use gpui::{
  4    elements::*, AppContext, Entity, ModelContext, ModelHandle, RenderContext, Subscription, Task,
  5    View, ViewContext, ViewHandle, WeakModelHandle,
  6};
  7use language::{Bias, Buffer, Diagnostic, File as _};
  8use project::{File, Project, ProjectEntry, ProjectPath};
  9use std::fmt::Write;
 10use std::path::PathBuf;
 11use text::{Point, Selection};
 12use util::ResultExt;
 13use workspace::{ItemNavHistory, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView};
 14
 15pub struct BufferOpener;
 16
 17#[derive(Clone)]
 18pub struct BufferItemHandle(pub ModelHandle<Buffer>);
 19
 20#[derive(Clone)]
 21struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
 22
 23#[derive(Clone)]
 24pub struct MultiBufferItemHandle(pub ModelHandle<MultiBuffer>);
 25
 26#[derive(Clone)]
 27struct WeakMultiBufferItemHandle(WeakModelHandle<MultiBuffer>);
 28
 29impl PathOpener for BufferOpener {
 30    fn open(
 31        &self,
 32        project: &mut Project,
 33        project_path: ProjectPath,
 34        window_id: usize,
 35        cx: &mut ModelContext<Project>,
 36    ) -> Option<Task<Result<Box<dyn ItemViewHandle>>>> {
 37        let buffer = project.open_buffer(project_path, cx);
 38        Some(cx.spawn(|project, mut cx| async move {
 39            let buffer = buffer.await?;
 40            let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 41            let editor = cx.add_view(window_id, |cx| {
 42                Editor::for_buffer(multibuffer, Some(project), cx)
 43            });
 44            Ok(Box::new(editor) as Box<dyn ItemViewHandle>)
 45        }))
 46    }
 47}
 48
 49impl ItemView for Editor {
 50    fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
 51        if let Some(data) = data.downcast_ref::<NavigationData>() {
 52            let buffer = self.buffer.read(cx).read(cx);
 53            let offset = if buffer.can_resolve(&data.anchor) {
 54                data.anchor.to_offset(&buffer)
 55            } else {
 56                buffer.clip_offset(data.offset, Bias::Left)
 57            };
 58
 59            drop(buffer);
 60            let nav_history = self.nav_history.take();
 61            self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
 62            self.nav_history = nav_history;
 63        }
 64    }
 65
 66    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
 67        let title = self.title(cx);
 68        Label::new(title, style.label.clone()).boxed()
 69    }
 70
 71    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
 72        File::from_dyn(self.buffer().read(cx).file(cx)).map(|file| ProjectPath {
 73            worktree_id: file.worktree_id(cx),
 74            path: file.path().clone(),
 75        })
 76    }
 77
 78    fn project_entry(&self, cx: &AppContext) -> Option<ProjectEntry> {
 79        File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry(cx))
 80    }
 81
 82    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 83    where
 84        Self: Sized,
 85    {
 86        Some(self.clone(cx))
 87    }
 88
 89    fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
 90        self.nav_history = Some(history);
 91    }
 92
 93    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 94        let selection = self.newest_anchor_selection();
 95        self.push_to_nav_history(selection.head(), None, cx);
 96    }
 97
 98    fn is_dirty(&self, cx: &AppContext) -> bool {
 99        self.buffer().read(cx).read(cx).is_dirty()
100    }
101
102    fn has_conflict(&self, cx: &AppContext) -> bool {
103        self.buffer().read(cx).read(cx).has_conflict()
104    }
105
106    fn can_save(&self, cx: &AppContext) -> bool {
107        !self.buffer().read(cx).is_singleton() || self.project_path(cx).is_some()
108    }
109
110    fn save(
111        &mut self,
112        project: ModelHandle<Project>,
113        cx: &mut ViewContext<Self>,
114    ) -> Task<Result<()>> {
115        let buffer = self.buffer().clone();
116        let buffers = buffer.read(cx).all_buffers();
117        let transaction = project.update(cx, |project, cx| project.format(buffers, true, cx));
118        cx.spawn(|this, mut cx| async move {
119            let transaction = transaction.await.log_err();
120            this.update(&mut cx, |editor, cx| {
121                editor.request_autoscroll(Autoscroll::Fit, cx)
122            });
123            buffer
124                .update(&mut cx, |buffer, cx| {
125                    if let Some(transaction) = transaction {
126                        if !buffer.is_singleton() {
127                            buffer.push_transaction(&transaction.0);
128                        }
129                    }
130
131                    buffer.save(cx)
132                })
133                .await?;
134            Ok(())
135        })
136    }
137
138    fn can_save_as(&self, cx: &AppContext) -> bool {
139        self.buffer().read(cx).is_singleton()
140    }
141
142    fn save_as(
143        &mut self,
144        project: ModelHandle<Project>,
145        abs_path: PathBuf,
146        cx: &mut ViewContext<Self>,
147    ) -> Task<Result<()>> {
148        let buffer = self
149            .buffer()
150            .read(cx)
151            .as_singleton()
152            .expect("cannot call save_as on an excerpt list")
153            .clone();
154
155        project.update(cx, |project, cx| {
156            project.save_buffer_as(buffer, abs_path, cx)
157        })
158    }
159
160    fn should_activate_item_on_event(event: &Event) -> bool {
161        matches!(event, Event::Activate)
162    }
163
164    fn should_close_item_on_event(event: &Event) -> bool {
165        matches!(event, Event::Closed)
166    }
167
168    fn should_update_tab_on_event(event: &Event) -> bool {
169        matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
170    }
171}
172
173pub struct CursorPosition {
174    position: Option<Point>,
175    selected_count: usize,
176    _observe_active_editor: Option<Subscription>,
177}
178
179impl CursorPosition {
180    pub fn new() -> Self {
181        Self {
182            position: None,
183            selected_count: 0,
184            _observe_active_editor: None,
185        }
186    }
187
188    fn update_position(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
189        let editor = editor.read(cx);
190        let buffer = editor.buffer().read(cx).snapshot(cx);
191
192        self.selected_count = 0;
193        let mut last_selection: Option<Selection<usize>> = None;
194        for selection in editor.local_selections::<usize>(cx) {
195            self.selected_count += selection.end - selection.start;
196            if last_selection
197                .as_ref()
198                .map_or(true, |last_selection| selection.id > last_selection.id)
199            {
200                last_selection = Some(selection);
201            }
202        }
203        self.position = last_selection.map(|s| s.head().to_point(&buffer));
204
205        cx.notify();
206    }
207}
208
209impl Entity for CursorPosition {
210    type Event = ();
211}
212
213impl View for CursorPosition {
214    fn ui_name() -> &'static str {
215        "CursorPosition"
216    }
217
218    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
219        if let Some(position) = self.position {
220            let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
221            let mut text = format!("{},{}", position.row + 1, position.column + 1);
222            if self.selected_count > 0 {
223                write!(text, " ({} selected)", self.selected_count).unwrap();
224            }
225            Label::new(text, theme.cursor_position.clone()).boxed()
226        } else {
227            Empty::new().boxed()
228        }
229    }
230}
231
232impl StatusItemView for CursorPosition {
233    fn set_active_pane_item(
234        &mut self,
235        active_pane_item: Option<&dyn ItemViewHandle>,
236        cx: &mut ViewContext<Self>,
237    ) {
238        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
239            self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
240            self.update_position(editor, cx);
241        } else {
242            self.position = None;
243            self._observe_active_editor = None;
244        }
245
246        cx.notify();
247    }
248}
249
250pub struct DiagnosticMessage {
251    diagnostic: Option<Diagnostic>,
252    _observe_active_editor: Option<Subscription>,
253}
254
255impl DiagnosticMessage {
256    pub fn new() -> Self {
257        Self {
258            diagnostic: None,
259            _observe_active_editor: None,
260        }
261    }
262
263    fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
264        let editor = editor.read(cx);
265        let buffer = editor.buffer().read(cx);
266        let cursor_position = editor
267            .newest_selection_with_snapshot::<usize>(&buffer.read(cx))
268            .head();
269        let new_diagnostic = buffer
270            .read(cx)
271            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
272            .filter(|entry| !entry.range.is_empty())
273            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
274            .map(|entry| entry.diagnostic);
275        if new_diagnostic != self.diagnostic {
276            self.diagnostic = new_diagnostic;
277            cx.notify();
278        }
279    }
280}
281
282impl Entity for DiagnosticMessage {
283    type Event = ();
284}
285
286impl View for DiagnosticMessage {
287    fn ui_name() -> &'static str {
288        "DiagnosticMessage"
289    }
290
291    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
292        if let Some(diagnostic) = &self.diagnostic {
293            let theme = &cx.app_state::<Settings>().theme.workspace.status_bar;
294            Label::new(
295                diagnostic.message.split('\n').next().unwrap().to_string(),
296                theme.diagnostic_message.clone(),
297            )
298            .boxed()
299        } else {
300            Empty::new().boxed()
301        }
302    }
303}
304
305impl StatusItemView for DiagnosticMessage {
306    fn set_active_pane_item(
307        &mut self,
308        active_pane_item: Option<&dyn ItemViewHandle>,
309        cx: &mut ViewContext<Self>,
310    ) {
311        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
312            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
313            self.update(editor, cx);
314        } else {
315            self.diagnostic = Default::default();
316            self._observe_active_editor = None;
317        }
318        cx.notify();
319    }
320}