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