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