lsp_log.rs

  1use collections::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::MouseButton,
 11    AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle,
 12};
 13use language::{Buffer, LanguageServerId, LanguageServerName};
 14use project::{Project, WorktreeId};
 15use settings::Settings;
 16use std::{borrow::Cow, sync::Arc};
 17use theme::Theme;
 18use util::ResultExt;
 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
 27pub struct LspLogView {
 28    enabled_logs: HashMap<LanguageServerId, LogState>,
 29    current_server_id: Option<LanguageServerId>,
 30    project: ModelHandle<Project>,
 31    io_tx: mpsc::UnboundedSender<(LanguageServerId, bool, String)>,
 32}
 33
 34pub struct LspLogToolbarItemView {
 35    log_view: Option<ViewHandle<LspLogView>>,
 36    menu_open: bool,
 37    project: ModelHandle<Project>,
 38}
 39
 40struct LogState {
 41    buffer: ModelHandle<Buffer>,
 42    editor: ViewHandle<Editor>,
 43    last_message_kind: Option<MessageKind>,
 44    _subscription: lsp::Subscription,
 45}
 46
 47#[derive(Copy, Clone, PartialEq, Eq)]
 48enum MessageKind {
 49    Send,
 50    Receive,
 51}
 52
 53actions!(log, [OpenLanguageServerLogs]);
 54
 55pub fn init(cx: &mut AppContext) {
 56    cx.add_action(LspLogView::open);
 57}
 58
 59impl LspLogView {
 60    pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
 61        let (io_tx, mut io_rx) = mpsc::unbounded();
 62        let this = Self {
 63            enabled_logs: HashMap::default(),
 64            current_server_id: None,
 65            io_tx,
 66            project,
 67        };
 68        cx.spawn_weak(|this, mut cx| async move {
 69            while let Some((language_server_id, is_output, mut message)) = io_rx.next().await {
 70                if let Some(this) = this.upgrade(&cx) {
 71                    this.update(&mut cx, |this, cx| {
 72                        message.push('\n');
 73                        this.on_io(language_server_id, is_output, &message, cx);
 74                    })
 75                    .log_err();
 76                }
 77            }
 78            anyhow::Ok(())
 79        })
 80        .detach();
 81        this
 82    }
 83
 84    fn open(
 85        workspace: &mut Workspace,
 86        _: &OpenLanguageServerLogs,
 87        cx: &mut ViewContext<Workspace>,
 88    ) {
 89        let project = workspace.project().read(cx);
 90        if project.is_remote() {
 91            return;
 92        }
 93
 94        let log_view = cx.add_view(|cx| Self::new(workspace.project().clone(), cx));
 95        workspace.add_item(Box::new(log_view), cx);
 96    }
 97
 98    fn activate_log(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
 99        self.enable_logs_for_language_server(server_id, cx);
100        self.current_server_id = Some(server_id);
101        cx.notify();
102    }
103
104    fn on_io(
105        &mut self,
106        language_server_id: LanguageServerId,
107        is_received: bool,
108        message: &str,
109        cx: &mut ViewContext<Self>,
110    ) {
111        if let Some(state) = self.enabled_logs.get_mut(&language_server_id) {
112            state.buffer.update(cx, |buffer, cx| {
113                let kind = if is_received {
114                    MessageKind::Receive
115                } else {
116                    MessageKind::Send
117                };
118                if state.last_message_kind != Some(kind) {
119                    let len = buffer.len();
120                    let line = match kind {
121                        MessageKind::Send => SEND_LINE,
122                        MessageKind::Receive => RECEIVE_LINE,
123                    };
124                    buffer.edit([(len..len, line)], None, cx);
125                    state.last_message_kind = Some(kind);
126                }
127                let len = buffer.len();
128                buffer.edit([(len..len, message)], None, cx);
129            });
130        }
131    }
132
133    pub fn enable_logs_for_language_server(
134        &mut self,
135        server_id: LanguageServerId,
136        cx: &mut ViewContext<Self>,
137    ) {
138        if let Some(server) = self.project.read(cx).language_server_for_id(server_id) {
139            self.enabled_logs.entry(server_id).or_insert_with(|| {
140                let project = self.project.read(cx);
141                let io_tx = self.io_tx.clone();
142                let language = project.languages().language_for_name("JSON");
143                let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
144                cx.spawn({
145                    let buffer = buffer.clone();
146                    |_, mut cx| async move {
147                        let language = language.await.ok();
148                        buffer.update(&mut cx, |buffer, cx| {
149                            buffer.set_language(language, cx);
150                        });
151                    }
152                })
153                .detach();
154                let editor = cx.add_view(|cx| {
155                    let mut editor =
156                        Editor::for_buffer(buffer.clone(), Some(self.project.clone()), cx);
157                    editor.set_read_only(true);
158                    editor
159                });
160
161                LogState {
162                    buffer,
163                    editor,
164                    last_message_kind: None,
165                    _subscription: server.on_io(move |is_received, json| {
166                        io_tx
167                            .unbounded_send((server_id, is_received, json.to_string()))
168                            .ok();
169                    }),
170                }
171            });
172        }
173    }
174}
175
176impl View for LspLogView {
177    fn ui_name() -> &'static str {
178        "LspLogView"
179    }
180
181    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
182        if let Some(id) = self.current_server_id {
183            if let Some(log) = self.enabled_logs.get_mut(&id) {
184                return ChildView::new(&log.editor, cx).into_any();
185            }
186        }
187        Empty::new().into_any()
188    }
189}
190
191impl Item for LspLogView {
192    fn tab_content<V: View>(
193        &self,
194        _: Option<usize>,
195        style: &theme::Tab,
196        _: &AppContext,
197    ) -> AnyElement<V> {
198        Label::new("Logs", style.label.clone()).into_any()
199    }
200}
201
202impl ToolbarItemView for LspLogToolbarItemView {
203    fn set_active_pane_item(
204        &mut self,
205        active_pane_item: Option<&dyn ItemHandle>,
206        _: &mut ViewContext<Self>,
207    ) -> workspace::ToolbarItemLocation {
208        self.menu_open = false;
209        if let Some(item) = active_pane_item {
210            if let Some(log_view) = item.downcast::<LspLogView>() {
211                self.log_view = Some(log_view.clone());
212                return ToolbarItemLocation::PrimaryLeft {
213                    flex: Some((1., false)),
214                };
215            }
216        }
217        self.log_view = None;
218        ToolbarItemLocation::Hidden
219    }
220}
221
222impl View for LspLogToolbarItemView {
223    fn ui_name() -> &'static str {
224        "LspLogView"
225    }
226
227    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
228        let theme = cx.global::<Settings>().theme.clone();
229        let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
230        let project = self.project.read(cx);
231        let mut language_servers = project.language_servers().collect::<Vec<_>>();
232        language_servers.sort_by_key(|a| a.0);
233
234        let current_server_id = log_view.read(cx).current_server_id;
235        let current_server = current_server_id.and_then(|current_server_id| {
236            if let Ok(ix) = language_servers.binary_search_by_key(&current_server_id, |e| e.0) {
237                Some(language_servers[ix].clone())
238            } else {
239                None
240            }
241        });
242
243        Stack::new()
244            .with_child(Self::render_language_server_menu_header(
245                current_server,
246                &self.project,
247                &theme,
248                cx,
249            ))
250            .with_children(if self.menu_open {
251                Some(
252                    Overlay::new(
253                        Flex::column()
254                            .with_children(language_servers.into_iter().filter_map(
255                                |(id, name, worktree_id)| {
256                                    Self::render_language_server_menu_item(
257                                        id,
258                                        name,
259                                        worktree_id,
260                                        &self.project,
261                                        &theme,
262                                        cx,
263                                    )
264                                },
265                            ))
266                            .contained()
267                            .with_style(theme.contacts_popover.container)
268                            .constrained()
269                            .with_width(200.)
270                            .with_height(400.),
271                    )
272                    .with_fit_mode(OverlayFitMode::SwitchAnchor)
273                    .with_anchor_corner(AnchorCorner::TopRight)
274                    .with_z_index(999)
275                    .aligned()
276                    .bottom()
277                    .right(),
278                )
279            } else {
280                None
281            })
282            .into_any()
283    }
284}
285
286impl LspLogToolbarItemView {
287    pub fn new(project: ModelHandle<Project>) -> Self {
288        Self {
289            menu_open: false,
290            log_view: None,
291            project,
292        }
293    }
294
295    fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
296        self.menu_open = !self.menu_open;
297        cx.notify();
298    }
299
300    fn activate_log_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
301        if let Some(log_view) = &self.log_view {
302            log_view.update(cx, |log_view, cx| {
303                log_view.activate_log(id, cx);
304            });
305            self.menu_open = false;
306        }
307        cx.notify();
308    }
309
310    fn render_language_server_menu_header(
311        current_server: Option<(LanguageServerId, LanguageServerName, WorktreeId)>,
312        project: &ModelHandle<Project>,
313        theme: &Arc<Theme>,
314        cx: &mut ViewContext<Self>,
315    ) -> impl Element<Self> {
316        enum ToggleMenu {}
317        MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
318            let project = project.read(cx);
319            let label: Cow<str> = current_server
320                .and_then(|(_, server_name, worktree_id)| {
321                    let worktree = project.worktree_for_id(worktree_id, cx)?;
322                    let worktree = &worktree.read(cx);
323                    Some(format!("{} - ({})", server_name.0, worktree.root_name()).into())
324                })
325                .unwrap_or_else(|| "No server selected".into());
326            Label::new(label, theme.context_menu.item.default.label.clone())
327        })
328        .on_click(MouseButton::Left, move |_, view, cx| {
329            view.toggle_menu(cx);
330        })
331    }
332
333    fn render_language_server_menu_item(
334        id: LanguageServerId,
335        name: LanguageServerName,
336        worktree_id: WorktreeId,
337        project: &ModelHandle<Project>,
338        theme: &Arc<Theme>,
339        cx: &mut ViewContext<Self>,
340    ) -> Option<impl Element<Self>> {
341        enum ActivateLog {}
342        let project = project.read(cx);
343        let worktree = project.worktree_for_id(worktree_id, cx)?;
344        let worktree = &worktree.read(cx);
345        if !worktree.is_visible() {
346            return None;
347        }
348        let label = format!("{} - ({})", name.0, worktree.root_name());
349
350        Some(
351            MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, cx| {
352                Label::new(label, theme.context_menu.item.default.label.clone())
353            })
354            .on_click(MouseButton::Left, move |_, view, cx| {
355                view.activate_log_for_server(id, cx);
356            }),
357        )
358    }
359}
360
361impl Entity for LspLogView {
362    type Event = ();
363}
364
365impl Entity for LspLogToolbarItemView {
366    type Event = ();
367}