items.rs

  1use crate::{Editor, Event};
  2use crate::{MultiBuffer, ToPoint as _};
  3use anyhow::Result;
  4use gpui::{
  5    elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
  6    Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle,
  7};
  8use language::{Diagnostic, File as _};
  9use postage::watch;
 10use project::{File, ProjectPath, Worktree};
 11use std::fmt::Write;
 12use std::path::Path;
 13use text::{Point, Selection};
 14use workspace::{
 15    EntryOpener, ItemHandle, ItemView, ItemViewHandle, Settings, StatusItemView, WeakItemHandle,
 16};
 17
 18pub struct BufferOpener;
 19
 20#[derive(Clone)]
 21pub struct BufferItemHandle(pub ModelHandle<MultiBuffer>);
 22
 23#[derive(Clone)]
 24struct WeakBufferItemHandle(WeakModelHandle<MultiBuffer>);
 25
 26impl EntryOpener for BufferOpener {
 27    fn open(
 28        &self,
 29        worktree: &mut Worktree,
 30        project_path: ProjectPath,
 31        cx: &mut ModelContext<Worktree>,
 32    ) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
 33        let buffer = worktree.open_buffer(project_path.path, cx);
 34        let task = cx.spawn(|_, mut cx| async move {
 35            let buffer = buffer.await?;
 36            let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 37            Ok(Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
 38        });
 39        Some(task)
 40    }
 41}
 42
 43impl ItemHandle for BufferItemHandle {
 44    fn add_view(
 45        &self,
 46        window_id: usize,
 47        settings: watch::Receiver<Settings>,
 48        cx: &mut MutableAppContext,
 49    ) -> Box<dyn ItemViewHandle> {
 50        let buffer = self.0.downgrade();
 51        Box::new(cx.add_view(window_id, |cx| {
 52            Editor::for_buffer(
 53                self.0.clone(),
 54                crate::settings_builder(buffer, settings),
 55                cx,
 56            )
 57        }))
 58    }
 59
 60    fn boxed_clone(&self) -> Box<dyn ItemHandle> {
 61        Box::new(self.clone())
 62    }
 63
 64    fn downgrade(&self) -> Box<dyn workspace::WeakItemHandle> {
 65        Box::new(WeakBufferItemHandle(self.0.downgrade()))
 66    }
 67
 68    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
 69        File::from_dyn(self.0.read(cx).file(cx)).map(|f| ProjectPath {
 70            worktree_id: f.worktree_id(cx),
 71            path: f.path().clone(),
 72        })
 73    }
 74
 75    fn id(&self) -> usize {
 76        self.0.id()
 77    }
 78}
 79
 80impl WeakItemHandle for WeakBufferItemHandle {
 81    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
 82        self.0
 83            .upgrade(cx)
 84            .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
 85    }
 86
 87    fn id(&self) -> usize {
 88        self.0.id()
 89    }
 90}
 91
 92impl ItemView for Editor {
 93    type ItemHandle = BufferItemHandle;
 94
 95    fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle {
 96        BufferItemHandle(self.buffer.clone())
 97    }
 98
 99    fn title(&self, cx: &AppContext) -> String {
100        let filename = self
101            .buffer()
102            .read(cx)
103            .file(cx)
104            .and_then(|file| file.file_name());
105        if let Some(name) = filename {
106            name.to_string_lossy().into()
107        } else {
108            "untitled".into()
109        }
110    }
111
112    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
113        File::from_dyn(self.buffer().read(cx).file(cx)).map(|file| ProjectPath {
114            worktree_id: file.worktree_id(cx),
115            path: file.path().clone(),
116        })
117    }
118
119    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
120    where
121        Self: Sized,
122    {
123        Some(self.clone(cx))
124    }
125
126    fn is_dirty(&self, cx: &AppContext) -> bool {
127        self.buffer().read(cx).read(cx).is_dirty()
128    }
129
130    fn has_conflict(&self, cx: &AppContext) -> bool {
131        self.buffer().read(cx).read(cx).has_conflict()
132    }
133
134    fn can_save(&self, cx: &AppContext) -> bool {
135        self.project_path(cx).is_some()
136    }
137
138    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
139        let save = self.buffer().update(cx, |b, cx| b.save(cx))?;
140        Ok(cx.spawn(|_, _| async move {
141            save.await?;
142            Ok(())
143        }))
144    }
145
146    fn can_save_as(&self, _: &AppContext) -> bool {
147        true
148    }
149
150    fn save_as(
151        &mut self,
152        worktree: ModelHandle<Worktree>,
153        path: &Path,
154        cx: &mut ViewContext<Self>,
155    ) -> Task<Result<()>> {
156        let buffer = self
157            .buffer()
158            .read(cx)
159            .as_singleton()
160            .expect("cannot call save_as on an excerpt list")
161            .clone();
162
163        buffer.update(cx, |buffer, cx| {
164            let handle = cx.handle();
165            let text = buffer.as_rope().clone();
166            let version = buffer.version();
167
168            let save_as = worktree.update(cx, |worktree, cx| {
169                worktree
170                    .as_local_mut()
171                    .unwrap()
172                    .save_buffer_as(handle, path, text, cx)
173            });
174
175            cx.spawn(|buffer, mut cx| async move {
176                save_as.await.map(|new_file| {
177                    let (language, language_server) = worktree.update(&mut cx, |worktree, cx| {
178                        let worktree = worktree.as_local_mut().unwrap();
179                        let language = worktree
180                            .language_registry()
181                            .select_language(new_file.full_path())
182                            .cloned();
183                        let language_server = language
184                            .as_ref()
185                            .and_then(|language| worktree.register_language(language, cx));
186                        (language, language_server.clone())
187                    });
188
189                    buffer.update(&mut cx, |buffer, cx| {
190                        buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
191                        buffer.set_language(language, language_server, cx);
192                    });
193                })
194            })
195        })
196    }
197
198    fn should_activate_item_on_event(event: &Event) -> bool {
199        matches!(event, Event::Activate)
200    }
201
202    fn should_close_item_on_event(event: &Event) -> bool {
203        matches!(event, Event::Closed)
204    }
205
206    fn should_update_tab_on_event(event: &Event) -> bool {
207        matches!(
208            event,
209            Event::Saved | Event::Dirtied | Event::FileHandleChanged
210        )
211    }
212}
213
214pub struct CursorPosition {
215    position: Option<Point>,
216    selected_count: usize,
217    settings: watch::Receiver<Settings>,
218    _observe_active_editor: Option<Subscription>,
219}
220
221impl CursorPosition {
222    pub fn new(settings: watch::Receiver<Settings>) -> Self {
223        Self {
224            position: None,
225            selected_count: 0,
226            settings,
227            _observe_active_editor: None,
228        }
229    }
230
231    fn update_position(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
232        let editor = editor.read(cx);
233        let buffer = editor.buffer().read(cx).snapshot(cx);
234
235        self.selected_count = 0;
236        let mut last_selection: Option<Selection<usize>> = None;
237        for selection in editor.local_selections::<usize>(cx) {
238            self.selected_count += selection.end - selection.start;
239            if last_selection
240                .as_ref()
241                .map_or(true, |last_selection| selection.id > last_selection.id)
242            {
243                last_selection = Some(selection);
244            }
245        }
246        self.position = last_selection.map(|s| s.head().to_point(&buffer));
247
248        cx.notify();
249    }
250}
251
252impl Entity for CursorPosition {
253    type Event = ();
254}
255
256impl View for CursorPosition {
257    fn ui_name() -> &'static str {
258        "CursorPosition"
259    }
260
261    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
262        if let Some(position) = self.position {
263            let theme = &self.settings.borrow().theme.workspace.status_bar;
264            let mut text = format!("{},{}", position.row + 1, position.column + 1);
265            if self.selected_count > 0 {
266                write!(text, " ({} selected)", self.selected_count).unwrap();
267            }
268            Label::new(text, theme.cursor_position.clone()).boxed()
269        } else {
270            Empty::new().boxed()
271        }
272    }
273}
274
275impl StatusItemView for CursorPosition {
276    fn set_active_pane_item(
277        &mut self,
278        active_pane_item: Option<&dyn ItemViewHandle>,
279        cx: &mut ViewContext<Self>,
280    ) {
281        if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
282            self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
283            self.update_position(editor, cx);
284        } else {
285            self.position = None;
286            self._observe_active_editor = None;
287        }
288
289        cx.notify();
290    }
291}
292
293pub struct DiagnosticMessage {
294    settings: watch::Receiver<Settings>,
295    diagnostic: Option<Diagnostic>,
296    _observe_active_editor: Option<Subscription>,
297}
298
299impl DiagnosticMessage {
300    pub fn new(settings: watch::Receiver<Settings>) -> Self {
301        Self {
302            diagnostic: None,
303            settings,
304            _observe_active_editor: None,
305        }
306    }
307
308    fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
309        let editor = editor.read(cx);
310        let buffer = editor.buffer().read(cx);
311        let cursor_position = editor.newest_selection::<usize>(&buffer.read(cx)).head();
312        let new_diagnostic = buffer
313            .read(cx)
314            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position)
315            .filter(|entry| !entry.range.is_empty())
316            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
317            .map(|entry| entry.diagnostic);
318        if new_diagnostic != self.diagnostic {
319            self.diagnostic = new_diagnostic;
320            cx.notify();
321        }
322    }
323}
324
325impl Entity for DiagnosticMessage {
326    type Event = ();
327}
328
329impl View for DiagnosticMessage {
330    fn ui_name() -> &'static str {
331        "DiagnosticMessage"
332    }
333
334    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
335        if let Some(diagnostic) = &self.diagnostic {
336            let theme = &self.settings.borrow().theme.workspace.status_bar;
337            Flex::row()
338                .with_child(
339                    Svg::new("icons/warning.svg")
340                        .with_color(theme.diagnostic_icon_color)
341                        .constrained()
342                        .with_height(theme.diagnostic_icon_size)
343                        .contained()
344                        .with_margin_right(theme.diagnostic_icon_spacing)
345                        .boxed(),
346                )
347                .with_child(
348                    Label::new(
349                        diagnostic.message.lines().next().unwrap().to_string(),
350                        theme.diagnostic_message.clone(),
351                    )
352                    .boxed(),
353                )
354                .boxed()
355        } else {
356            Empty::new().boxed()
357        }
358    }
359}
360
361impl StatusItemView for DiagnosticMessage {
362    fn set_active_pane_item(
363        &mut self,
364        active_pane_item: Option<&dyn ItemViewHandle>,
365        cx: &mut ViewContext<Self>,
366    ) {
367        if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
368            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
369            self.update(editor, cx);
370        } else {
371            self.diagnostic = Default::default();
372            self._observe_active_editor = None;
373        }
374        cx.notify();
375    }
376}