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::{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        self.0.read(cx).file(cx).map(|f| ProjectPath {
 70            worktree_id: f.worktree_id(),
 71            path: f.path().clone(),
 72        })
 73    }
 74}
 75
 76impl WeakItemHandle for WeakBufferItemHandle {
 77    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
 78        self.0
 79            .upgrade(cx)
 80            .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
 81    }
 82}
 83
 84impl ItemView for Editor {
 85    fn should_activate_item_on_event(event: &Event) -> bool {
 86        matches!(event, Event::Activate)
 87    }
 88
 89    fn should_close_item_on_event(event: &Event) -> bool {
 90        matches!(event, Event::Closed)
 91    }
 92
 93    fn should_update_tab_on_event(event: &Event) -> bool {
 94        matches!(
 95            event,
 96            Event::Saved | Event::Dirtied | Event::FileHandleChanged
 97        )
 98    }
 99
100    fn title(&self, cx: &AppContext) -> String {
101        let filename = self
102            .buffer()
103            .read(cx)
104            .file(cx)
105            .and_then(|file| file.file_name());
106        if let Some(name) = filename {
107            name.to_string_lossy().into()
108        } else {
109            "untitled".into()
110        }
111    }
112
113    fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
114        self.buffer().read(cx).file(cx).map(|file| ProjectPath {
115            worktree_id: file.worktree_id(),
116            path: file.path().clone(),
117        })
118    }
119
120    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
121    where
122        Self: Sized,
123    {
124        Some(self.clone(cx))
125    }
126
127    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
128        let save = self.buffer().update(cx, |b, cx| b.save(cx))?;
129        Ok(cx.spawn(|_, _| async move {
130            save.await?;
131            Ok(())
132        }))
133    }
134
135    fn save_as(
136        &mut self,
137        worktree: ModelHandle<Worktree>,
138        path: &Path,
139        cx: &mut ViewContext<Self>,
140    ) -> Task<Result<()>> {
141        let buffer = self
142            .buffer()
143            .read(cx)
144            .as_singleton()
145            .expect("cannot call save_as on an excerpt list")
146            .clone();
147
148        buffer.update(cx, |buffer, cx| {
149            let handle = cx.handle();
150            let text = buffer.as_rope().clone();
151            let version = buffer.version();
152
153            let save_as = worktree.update(cx, |worktree, cx| {
154                worktree
155                    .as_local_mut()
156                    .unwrap()
157                    .save_buffer_as(handle, path, text, cx)
158            });
159
160            cx.spawn(|buffer, mut cx| async move {
161                save_as.await.map(|new_file| {
162                    let (language, language_server) = worktree.update(&mut cx, |worktree, cx| {
163                        let worktree = worktree.as_local_mut().unwrap();
164                        let language = worktree
165                            .languages()
166                            .select_language(new_file.full_path())
167                            .cloned();
168                        let language_server = language
169                            .as_ref()
170                            .and_then(|language| worktree.ensure_language_server(language, cx));
171                        (language, language_server.clone())
172                    });
173
174                    buffer.update(&mut cx, |buffer, cx| {
175                        buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
176                        buffer.set_language(language, language_server, cx);
177                    });
178                })
179            })
180        })
181    }
182
183    fn is_dirty(&self, cx: &AppContext) -> bool {
184        self.buffer().read(cx).is_dirty(cx)
185    }
186
187    fn has_conflict(&self, cx: &AppContext) -> bool {
188        self.buffer().read(cx).has_conflict(cx)
189    }
190}
191
192pub struct CursorPosition {
193    position: Option<Point>,
194    selected_count: usize,
195    settings: watch::Receiver<Settings>,
196    _observe_active_editor: Option<Subscription>,
197}
198
199impl CursorPosition {
200    pub fn new(settings: watch::Receiver<Settings>) -> Self {
201        Self {
202            position: None,
203            selected_count: 0,
204            settings,
205            _observe_active_editor: None,
206        }
207    }
208
209    fn update_position(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
210        let editor = editor.read(cx);
211        let buffer = editor.buffer().read(cx).snapshot(cx);
212
213        self.selected_count = 0;
214        let mut last_selection: Option<Selection<usize>> = None;
215        for selection in editor.local_selections::<usize>(cx) {
216            self.selected_count += selection.end - selection.start;
217            if last_selection
218                .as_ref()
219                .map_or(true, |last_selection| selection.id > last_selection.id)
220            {
221                last_selection = Some(selection);
222            }
223        }
224        self.position = last_selection.map(|s| s.head().to_point(&buffer));
225
226        cx.notify();
227    }
228}
229
230impl Entity for CursorPosition {
231    type Event = ();
232}
233
234impl View for CursorPosition {
235    fn ui_name() -> &'static str {
236        "CursorPosition"
237    }
238
239    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
240        if let Some(position) = self.position {
241            let theme = &self.settings.borrow().theme.workspace.status_bar;
242            let mut text = format!("{},{}", position.row + 1, position.column + 1);
243            if self.selected_count > 0 {
244                write!(text, " ({} selected)", self.selected_count).unwrap();
245            }
246            Label::new(text, theme.cursor_position.clone()).boxed()
247        } else {
248            Empty::new().boxed()
249        }
250    }
251}
252
253impl StatusItemView for CursorPosition {
254    fn set_active_pane_item(
255        &mut self,
256        active_pane_item: Option<&dyn ItemViewHandle>,
257        cx: &mut ViewContext<Self>,
258    ) {
259        if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
260            self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
261            self.update_position(editor, cx);
262        } else {
263            self.position = None;
264            self._observe_active_editor = None;
265        }
266
267        cx.notify();
268    }
269}
270
271pub struct DiagnosticMessage {
272    settings: watch::Receiver<Settings>,
273    diagnostic: Option<Diagnostic>,
274    _observe_active_editor: Option<Subscription>,
275}
276
277impl DiagnosticMessage {
278    pub fn new(settings: watch::Receiver<Settings>) -> Self {
279        Self {
280            diagnostic: None,
281            settings,
282            _observe_active_editor: None,
283        }
284    }
285
286    fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
287        let editor = editor.read(cx);
288        let buffer = editor.buffer().read(cx);
289        let cursor_position = editor.newest_selection::<usize>(&buffer.read(cx)).head();
290        let new_diagnostic = buffer
291            .read(cx)
292            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position)
293            .filter(|entry| !entry.range.is_empty())
294            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
295            .map(|entry| entry.diagnostic);
296        if new_diagnostic != self.diagnostic {
297            self.diagnostic = new_diagnostic;
298            cx.notify();
299        }
300    }
301}
302
303impl Entity for DiagnosticMessage {
304    type Event = ();
305}
306
307impl View for DiagnosticMessage {
308    fn ui_name() -> &'static str {
309        "DiagnosticMessage"
310    }
311
312    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
313        if let Some(diagnostic) = &self.diagnostic {
314            let theme = &self.settings.borrow().theme.workspace.status_bar;
315            Flex::row()
316                .with_child(
317                    Svg::new("icons/warning.svg")
318                        .with_color(theme.diagnostic_icon_color)
319                        .constrained()
320                        .with_height(theme.diagnostic_icon_size)
321                        .contained()
322                        .with_margin_right(theme.diagnostic_icon_spacing)
323                        .boxed(),
324                )
325                .with_child(
326                    Label::new(
327                        diagnostic.message.lines().next().unwrap().to_string(),
328                        theme.diagnostic_message.clone(),
329                    )
330                    .boxed(),
331                )
332                .boxed()
333        } else {
334            Empty::new().boxed()
335        }
336    }
337}
338
339impl StatusItemView for DiagnosticMessage {
340    fn set_active_pane_item(
341        &mut self,
342        active_pane_item: Option<&dyn ItemViewHandle>,
343        cx: &mut ViewContext<Self>,
344    ) {
345        if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
346            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
347            self.update(editor, cx);
348        } else {
349            self.diagnostic = Default::default();
350            self._observe_active_editor = None;
351        }
352        cx.notify();
353    }
354}