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