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(¤t_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}