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