lsp_log.rs

  1use collections::{hash_map, HashMap};
  2use editor::Editor;
  3use futures::{channel::mpsc, StreamExt};
  4use gpui::{
  5    actions,
  6    elements::{
  7        AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
  8        ParentElement, Stack,
  9    },
 10    platform::{CursorStyle, MouseButton},
 11    AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
 12    ViewHandle, WeakModelHandle,
 13};
 14use language::{Buffer, LanguageServerId, LanguageServerName};
 15use project::{Project, WorktreeId};
 16use settings::Settings;
 17use std::{borrow::Cow, sync::Arc};
 18use theme::{ui, Theme};
 19use workspace::{
 20    item::{Item, ItemHandle},
 21    ToolbarItemLocation, ToolbarItemView, Workspace,
 22};
 23
 24const SEND_LINE: &str = "// Send:\n";
 25const RECEIVE_LINE: &str = "// Receive:\n";
 26
 27struct LogStore {
 28    projects: HashMap<WeakModelHandle<Project>, LogStoreProject>,
 29    io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
 30}
 31
 32struct LogStoreProject {
 33    servers: HashMap<LanguageServerId, LogStoreLanguageServer>,
 34    _subscription: gpui::Subscription,
 35}
 36
 37struct LogStoreLanguageServer {
 38    buffer: ModelHandle<Buffer>,
 39    last_message_kind: Option<MessageKind>,
 40    _subscription: lsp::Subscription,
 41}
 42
 43pub struct LspLogView {
 44    log_store: ModelHandle<LogStore>,
 45    current_server_id: Option<LanguageServerId>,
 46    editor: Option<ViewHandle<Editor>>,
 47    project: ModelHandle<Project>,
 48}
 49
 50pub struct LspLogToolbarItemView {
 51    log_view: Option<ViewHandle<LspLogView>>,
 52    menu_open: bool,
 53    project: ModelHandle<Project>,
 54}
 55
 56#[derive(Copy, Clone, PartialEq, Eq)]
 57enum MessageKind {
 58    Send,
 59    Receive,
 60}
 61
 62actions!(log, [OpenLanguageServerLogs]);
 63
 64pub fn init(cx: &mut AppContext) {
 65    let log_set = cx.add_model(|cx| LogStore::new(cx));
 66
 67    cx.add_action(
 68        move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
 69            let project = workspace.project().read(cx);
 70            if project.is_local() {
 71                workspace.add_item(
 72                    Box::new(cx.add_view(|cx| {
 73                        LspLogView::new(workspace.project().clone(), log_set.clone(), cx)
 74                    })),
 75                    cx,
 76                );
 77            }
 78        },
 79    );
 80}
 81
 82impl LogStore {
 83    fn new(cx: &mut ModelContext<Self>) -> Self {
 84        let (io_tx, mut io_rx) = mpsc::unbounded();
 85        let this = Self {
 86            projects: HashMap::default(),
 87            io_tx,
 88        };
 89        cx.spawn_weak(|this, mut cx| async move {
 90            while let Some((project, server_id, is_output, mut message)) = io_rx.next().await {
 91                if let Some(this) = this.upgrade(&cx) {
 92                    this.update(&mut cx, |this, cx| {
 93                        message.push('\n');
 94                        this.on_io(project, server_id, is_output, &message, cx);
 95                    });
 96                }
 97            }
 98            anyhow::Ok(())
 99        })
100        .detach();
101        this
102    }
103
104    pub fn has_enabled_logs_for_language_server(
105        &self,
106        project: &ModelHandle<Project>,
107        server_id: LanguageServerId,
108    ) -> bool {
109        self.projects
110            .get(&project.downgrade())
111            .map_or(false, |store| store.servers.contains_key(&server_id))
112    }
113
114    pub fn enable_logs_for_language_server(
115        &mut self,
116        project: &ModelHandle<Project>,
117        server_id: LanguageServerId,
118        cx: &mut ModelContext<Self>,
119    ) -> Option<ModelHandle<Buffer>> {
120        let server = project.read(cx).language_server_for_id(server_id)?;
121        let weak_project = project.downgrade();
122        let project_logs = match self.projects.entry(weak_project) {
123            hash_map::Entry::Occupied(entry) => entry.into_mut(),
124            hash_map::Entry::Vacant(entry) => entry.insert(LogStoreProject {
125                servers: HashMap::default(),
126                _subscription: cx.observe_release(&project, move |this, _, _| {
127                    this.projects.remove(&weak_project);
128                }),
129            }),
130        };
131        let server_log_state = project_logs.servers.entry(server_id).or_insert_with(|| {
132            let io_tx = self.io_tx.clone();
133            let language = project.read(cx).languages().language_for_name("JSON");
134            let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
135            cx.spawn_weak({
136                let buffer = buffer.clone();
137                |_, mut cx| async move {
138                    let language = language.await.ok();
139                    buffer.update(&mut cx, |buffer, cx| {
140                        buffer.set_language(language, cx);
141                    });
142                }
143            })
144            .detach();
145
146            let project = project.downgrade();
147            LogStoreLanguageServer {
148                buffer,
149                last_message_kind: None,
150                _subscription: server.on_io(move |is_received, json| {
151                    io_tx
152                        .unbounded_send((project, server_id, is_received, json.to_string()))
153                        .ok();
154                }),
155            }
156        });
157        Some(server_log_state.buffer.clone())
158    }
159
160    pub fn disable_logs_for_language_server(
161        &mut self,
162        project: &ModelHandle<Project>,
163        server_id: LanguageServerId,
164        _: &mut ModelContext<Self>,
165    ) {
166        let project = project.downgrade();
167        if let Some(store) = self.projects.get_mut(&project) {
168            store.servers.remove(&server_id);
169            if store.servers.is_empty() {
170                self.projects.remove(&project);
171            }
172        }
173    }
174
175    fn on_io(
176        &mut self,
177        project: WeakModelHandle<Project>,
178        language_server_id: LanguageServerId,
179        is_received: bool,
180        message: &str,
181        cx: &mut AppContext,
182    ) -> Option<()> {
183        let state = self
184            .projects
185            .get_mut(&project)?
186            .servers
187            .get_mut(&language_server_id)?;
188        state.buffer.update(cx, |buffer, cx| {
189            let kind = if is_received {
190                MessageKind::Receive
191            } else {
192                MessageKind::Send
193            };
194            if state.last_message_kind != Some(kind) {
195                let len = buffer.len();
196                let line = match kind {
197                    MessageKind::Send => SEND_LINE,
198                    MessageKind::Receive => RECEIVE_LINE,
199                };
200                buffer.edit([(len..len, line)], None, cx);
201                state.last_message_kind = Some(kind);
202            }
203            let len = buffer.len();
204            buffer.edit([(len..len, message)], None, cx);
205        });
206        Some(())
207    }
208}
209
210impl LspLogView {
211    fn new(
212        project: ModelHandle<Project>,
213        log_set: ModelHandle<LogStore>,
214        _: &mut ViewContext<Self>,
215    ) -> Self {
216        Self {
217            project,
218            log_store: log_set,
219            editor: None,
220            current_server_id: None,
221        }
222    }
223
224    fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
225        let buffer = self.log_store.update(cx, |log_set, cx| {
226            log_set.enable_logs_for_language_server(&self.project, server_id, cx)
227        });
228        if let Some(buffer) = buffer {
229            self.current_server_id = Some(server_id);
230            self.editor = Some(cx.add_view(|cx| {
231                let mut editor = Editor::for_buffer(buffer, Some(self.project.clone()), cx);
232                editor.set_read_only(true);
233                editor.move_to_end(&Default::default(), cx);
234                editor
235            }));
236            cx.notify();
237        }
238    }
239
240    fn toggle_logging_for_server(
241        &mut self,
242        server_id: LanguageServerId,
243        enabled: bool,
244        cx: &mut ViewContext<Self>,
245    ) {
246        self.log_store.update(cx, |log_store, cx| {
247            if enabled {
248                log_store.enable_logs_for_language_server(&self.project, server_id, cx);
249            } else {
250                log_store.disable_logs_for_language_server(&self.project, server_id, cx);
251            }
252        });
253    }
254}
255
256impl View for LspLogView {
257    fn ui_name() -> &'static str {
258        "LspLogView"
259    }
260
261    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
262        if let Some(editor) = &self.editor {
263            ChildView::new(&editor, cx).into_any()
264        } else {
265            Empty::new().into_any()
266        }
267    }
268}
269
270impl Item for LspLogView {
271    fn tab_content<V: View>(
272        &self,
273        _: Option<usize>,
274        style: &theme::Tab,
275        _: &AppContext,
276    ) -> AnyElement<V> {
277        Label::new("LSP Logs", style.label.clone()).into_any()
278    }
279}
280
281impl ToolbarItemView for LspLogToolbarItemView {
282    fn set_active_pane_item(
283        &mut self,
284        active_pane_item: Option<&dyn ItemHandle>,
285        _: &mut ViewContext<Self>,
286    ) -> workspace::ToolbarItemLocation {
287        self.menu_open = false;
288        if let Some(item) = active_pane_item {
289            if let Some(log_view) = item.downcast::<LspLogView>() {
290                self.log_view = Some(log_view.clone());
291                return ToolbarItemLocation::PrimaryLeft {
292                    flex: Some((1., false)),
293                };
294            }
295        }
296        self.log_view = None;
297        ToolbarItemLocation::Hidden
298    }
299}
300
301impl View for LspLogToolbarItemView {
302    fn ui_name() -> &'static str {
303        "LspLogView"
304    }
305
306    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
307        let theme = cx.global::<Settings>().theme.clone();
308        let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
309        let project = self.project.read(cx);
310        let log_view = log_view.read(cx);
311        let log_store = log_view.log_store.read(cx);
312
313        let mut language_servers = project
314            .language_servers()
315            .map(|(id, name, worktree)| {
316                (
317                    id,
318                    name,
319                    worktree,
320                    log_store.has_enabled_logs_for_language_server(&self.project, id),
321                )
322            })
323            .collect::<Vec<_>>();
324        language_servers.sort_by_key(|a| (a.0, a.2));
325        language_servers.dedup_by_key(|a| a.0);
326
327        let current_server_id = log_view.current_server_id;
328        let current_server = current_server_id.and_then(|current_server_id| {
329            if let Ok(ix) = language_servers.binary_search_by_key(&current_server_id, |e| e.0) {
330                Some(language_servers[ix].clone())
331            } else {
332                None
333            }
334        });
335
336        enum Menu {}
337
338        Stack::new()
339            .with_child(Self::render_language_server_menu_header(
340                current_server,
341                &self.project,
342                &theme,
343                cx,
344            ))
345            .with_children(if self.menu_open {
346                Some(
347                    Overlay::new(
348                        MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
349                            Flex::column()
350                                .with_children(language_servers.into_iter().filter_map(
351                                    |(id, name, worktree_id, logging_enabled)| {
352                                        Self::render_language_server_menu_item(
353                                            id,
354                                            name,
355                                            worktree_id,
356                                            logging_enabled,
357                                            Some(id) == current_server_id,
358                                            &self.project,
359                                            &theme,
360                                            cx,
361                                        )
362                                    },
363                                ))
364                                .contained()
365                                .with_style(theme.context_menu.container)
366                                .constrained()
367                                .with_width(400.)
368                                .with_height(400.)
369                        })
370                        .on_down_out(MouseButton::Left, |_, this, cx| {
371                            this.menu_open = false;
372                            cx.notify()
373                        }),
374                    )
375                    .with_fit_mode(OverlayFitMode::SwitchAnchor)
376                    .with_anchor_corner(AnchorCorner::TopLeft)
377                    .with_z_index(999)
378                    .aligned()
379                    .bottom()
380                    .left(),
381                )
382            } else {
383                None
384            })
385            .aligned()
386            .left()
387            .clipped()
388            .into_any()
389    }
390}
391
392impl LspLogToolbarItemView {
393    pub fn new(project: ModelHandle<Project>) -> Self {
394        Self {
395            menu_open: false,
396            log_view: None,
397            project,
398        }
399    }
400
401    fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
402        self.menu_open = !self.menu_open;
403        cx.notify();
404    }
405
406    fn toggle_logging_for_server(
407        &mut self,
408        id: LanguageServerId,
409        enabled: bool,
410        cx: &mut ViewContext<Self>,
411    ) {
412        if let Some(log_view) = &self.log_view {
413            log_view.update(cx, |log_view, cx| {
414                log_view.toggle_logging_for_server(id, enabled, cx);
415                if !enabled && Some(id) == log_view.current_server_id {
416                    log_view.current_server_id = None;
417                    log_view.editor = None;
418                    cx.notify();
419                }
420            });
421        }
422        cx.notify();
423    }
424
425    fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
426        if let Some(log_view) = &self.log_view {
427            log_view.update(cx, |log_view, cx| {
428                log_view.show_logs_for_server(id, cx);
429            });
430            self.menu_open = false;
431        }
432        cx.notify();
433    }
434
435    fn render_language_server_menu_header(
436        current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId, bool)>,
437        project: &ModelHandle<Project>,
438        theme: &Arc<Theme>,
439        cx: &mut ViewContext<Self>,
440    ) -> impl Element<Self> {
441        enum ToggleMenu {}
442        MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
443            let project = project.read(cx);
444            let label: Cow<str> = current_server
445                .and_then(|(_, server_name, worktree_id, _)| {
446                    let worktree = project.worktree_for_id(worktree_id, cx)?;
447                    let worktree = &worktree.read(cx);
448                    Some(format!("{} - ({})", server_name.0, worktree.root_name()).into())
449                })
450                .unwrap_or_else(|| "No server selected".into());
451            Label::new(
452                label,
453                theme
454                    .context_menu
455                    .item
456                    .style_for(state, false)
457                    .label
458                    .clone(),
459            )
460        })
461        .with_cursor_style(CursorStyle::PointingHand)
462        .on_click(MouseButton::Left, move |_, view, cx| {
463            view.toggle_menu(cx);
464        })
465    }
466
467    fn render_language_server_menu_item(
468        id: LanguageServerId,
469        name: LanguageServerName,
470        worktree_id: WorktreeId,
471        logging_enabled: bool,
472        is_selected: bool,
473        project: &ModelHandle<Project>,
474        theme: &Arc<Theme>,
475        cx: &mut ViewContext<Self>,
476    ) -> Option<impl Element<Self>> {
477        enum ActivateLog {}
478        let project = project.read(cx);
479        let worktree = project.worktree_for_id(worktree_id, cx)?;
480        let worktree = &worktree.read(cx);
481        if !worktree.is_visible() {
482            return None;
483        }
484        let label = format!("{} - ({})", name.0, worktree.root_name());
485
486        Some(
487            MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, cx| {
488                let item_style = theme.context_menu.item.style_for(state, is_selected);
489                Flex::row()
490                    .with_child(ui::checkbox_with_label::<Self, _, Self, _>(
491                        Empty::new(),
492                        &theme.welcome.checkbox,
493                        logging_enabled,
494                        id.0,
495                        cx,
496                        move |this, enabled, cx| {
497                            this.toggle_logging_for_server(id, enabled, cx);
498                        },
499                    ))
500                    .with_child(Label::new(label, item_style.label.clone()).aligned().left())
501                    .align_children_center()
502                    .contained()
503                    .with_style(item_style.container)
504            })
505            .with_cursor_style(CursorStyle::PointingHand)
506            .on_click(MouseButton::Left, move |_, view, cx| {
507                view.show_logs_for_server(id, cx);
508            }),
509        )
510    }
511}
512
513impl Entity for LogStore {
514    type Event = ();
515}
516
517impl Entity for LspLogView {
518    type Event = ();
519}
520
521impl Entity for LspLogToolbarItemView {
522    type Event = ();
523}