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}