Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:07:21.798Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

lsp_button.rs

1525 lines · 63.1 KB · rust
1use std::{
2    cell::RefCell,
3    collections::{BTreeMap, HashMap},
4    path::{Path, PathBuf},
5    rc::Rc,
6    time::{Duration, Instant},
7};
8
9use sysinfo::{Pid, ProcessRefreshKind, RefreshKind, System};
10
11use language::language_settings::{EditPredictionProvider, all_language_settings};
12
13use client::proto;
14use collections::HashSet;
15use editor::{Editor, EditorEvent};
16use gpui::{Anchor, App, Entity, Subscription, Task, TaskExt, WeakEntity, actions};
17use language::{BinaryStatus, BufferId, ServerHealth};
18use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
19use project::{
20    LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore,
21    project_settings::ProjectSettings,
22};
23use settings::{Settings as _, SettingsStore};
24use ui::{
25    ContextMenu, ContextMenuEntry, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
26};
27
28use util::{ResultExt, paths::PathExt, rel_path::RelPath};
29use workspace::{StatusItemView, Workspace};
30
31use crate::lsp_log_view;
32
33actions!(
34    lsp_tool,
35    [
36        /// Toggles the language server tool menu.
37        ToggleMenu
38    ]
39);
40
41pub struct LspButton {
42    server_state: Entity<LanguageServerState>,
43    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
44    lsp_menu: Option<Entity<ContextMenu>>,
45    lsp_menu_refresh: Task<()>,
46    _subscriptions: Vec<Subscription>,
47}
48
49struct LanguageServerState {
50    items: Vec<LspMenuItem>,
51    workspace: WeakEntity<Workspace>,
52    lsp_store: WeakEntity<LspStore>,
53    active_editor: Option<ActiveEditor>,
54    language_servers: LanguageServers,
55    process_memory_cache: Rc<RefCell<ProcessMemoryCache>>,
56}
57
58impl std::fmt::Debug for LanguageServerState {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("LanguageServerState")
61            .field("items", &self.items)
62            .field("workspace", &self.workspace)
63            .field("lsp_store", &self.lsp_store)
64            .field("active_editor", &self.active_editor)
65            .field("language_servers", &self.language_servers)
66            .finish_non_exhaustive()
67    }
68}
69
70const PROCESS_MEMORY_CACHE_DURATION: Duration = Duration::from_secs(5);
71
72struct ProcessMemoryCache {
73    system: System,
74    memory_usage: HashMap<u32, u64>,
75    last_refresh: Option<Instant>,
76}
77
78impl ProcessMemoryCache {
79    fn new() -> Self {
80        Self {
81            system: System::new(),
82            memory_usage: HashMap::new(),
83            last_refresh: None,
84        }
85    }
86
87    fn get_memory_usage(&mut self, process_id: u32) -> u64 {
88        let cache_expired = self
89            .last_refresh
90            .map(|last| last.elapsed() >= PROCESS_MEMORY_CACHE_DURATION)
91            .unwrap_or(true);
92
93        if cache_expired {
94            let refresh_kind = RefreshKind::nothing()
95                .with_processes(ProcessRefreshKind::nothing().without_tasks().with_memory());
96            self.system.refresh_specifics(refresh_kind);
97            self.memory_usage.clear();
98            self.last_refresh = Some(Instant::now());
99        }
100
101        if let Some(&memory) = self.memory_usage.get(&process_id) {
102            return memory;
103        }
104
105        let root_pid = Pid::from_u32(process_id);
106
107        let parent_map: HashMap<Pid, Pid> = self
108            .system
109            .processes()
110            .iter()
111            .filter_map(|(&pid, process)| Some((pid, process.parent()?)))
112            .collect();
113
114        let total_memory = self
115            .system
116            .processes()
117            .iter()
118            .filter(|(pid, _)| self.is_descendant_of(**pid, root_pid, &parent_map))
119            .map(|(_, process)| process.memory())
120            .sum();
121
122        self.memory_usage.insert(process_id, total_memory);
123        total_memory
124    }
125
126    fn is_descendant_of(&self, pid: Pid, root_pid: Pid, parent_map: &HashMap<Pid, Pid>) -> bool {
127        let mut current = pid;
128        let mut visited = HashSet::default();
129        while current != root_pid {
130            if !visited.insert(current) {
131                return false;
132            }
133            match parent_map.get(&current) {
134                Some(&parent) => current = parent,
135                None => return false,
136            }
137        }
138        true
139    }
140}
141
142struct ActiveEditor {
143    editor: WeakEntity<Editor>,
144    _editor_subscription: Subscription,
145    editor_buffers: HashSet<BufferId>,
146}
147
148impl std::fmt::Debug for ActiveEditor {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        f.debug_struct("ActiveEditor")
151            .field("editor", &self.editor)
152            .field("editor_buffers", &self.editor_buffers)
153            .finish_non_exhaustive()
154    }
155}
156
157#[derive(Debug, Default, Clone)]
158struct LanguageServers {
159    health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
160    binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
161    servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,
162}
163
164#[derive(Debug, Clone)]
165struct ServersForPath {
166    servers: HashMap<LanguageServerId, Option<LanguageServerName>>,
167    worktree: Option<WeakEntity<Worktree>>,
168}
169
170#[derive(Debug, Clone)]
171struct LanguageServerHealthStatus {
172    name: LanguageServerName,
173    health: Option<(Option<SharedString>, ServerHealth)>,
174}
175
176#[derive(Debug, Clone)]
177struct LanguageServerBinaryStatus {
178    status: BinaryStatus,
179    message: Option<SharedString>,
180}
181
182#[derive(Debug, Clone)]
183struct ServerInfo {
184    name: LanguageServerName,
185    id: LanguageServerId,
186    health: Option<ServerHealth>,
187    binary_status: Option<LanguageServerBinaryStatus>,
188    message: Option<SharedString>,
189}
190
191impl ServerInfo {
192    fn server_selector(&self) -> LanguageServerSelector {
193        LanguageServerSelector::Id(self.id)
194    }
195
196    fn can_stop(&self) -> bool {
197        self.binary_status.as_ref().is_none_or(|status| {
198            matches!(status.status, BinaryStatus::None | BinaryStatus::Starting)
199        })
200    }
201}
202
203impl LanguageServerHealthStatus {
204    fn health(&self) -> Option<ServerHealth> {
205        self.health.as_ref().map(|(_, health)| *health)
206    }
207
208    fn message(&self) -> Option<SharedString> {
209        self.health
210            .as_ref()
211            .and_then(|(message, _)| message.clone())
212    }
213}
214
215impl LanguageServerState {
216    fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu {
217        let lsp_logs = cx
218            .try_global::<GlobalLogStore>()
219            .map(|lsp_logs| lsp_logs.0.clone());
220        let Some(lsp_logs) = lsp_logs else {
221            return menu;
222        };
223
224        let server_metadata = self
225            .lsp_store
226            .update(cx, |lsp_store, _| {
227                lsp_store
228                    .language_server_statuses()
229                    .map(|(server_id, status)| {
230                        (
231                            server_id,
232                            (
233                                status.server_readable_version.clone(),
234                                status.binary.as_ref().map(|b| b.path.clone()),
235                                status.process_id,
236                            ),
237                        )
238                    })
239                    .collect::<HashMap<_, _>>()
240            })
241            .unwrap_or_default();
242
243        let process_memory_cache = self.process_memory_cache.clone();
244
245        let mut first_button_encountered = false;
246        for item in &self.items {
247            if let LspMenuItem::ToggleServersButton { restart } = item {
248                let label = if *restart {
249                    "Restart All Servers"
250                } else {
251                    "Stop All Servers"
252                };
253
254                let restart = *restart;
255
256                let button = ContextMenuEntry::new(label).handler({
257                    let state = cx.entity();
258                    move |_, cx| {
259                        let lsp_store = state.read(cx).lsp_store.clone();
260                        lsp_store
261                            .update(cx, |lsp_store, cx| {
262                                if restart {
263                                    lsp_store.restart_all_language_servers(cx);
264                                } else {
265                                    lsp_store.stop_all_language_servers(cx);
266                                }
267                            })
268                            .ok();
269                    }
270                });
271
272                if !first_button_encountered {
273                    menu = menu.separator();
274                    first_button_encountered = true;
275                }
276
277                menu = menu.item(button);
278                continue;
279            } else if let LspMenuItem::Header { header, separator } = item {
280                menu = menu
281                    .when(*separator, |menu| menu.separator())
282                    .when_some(header.as_ref(), |menu, header| menu.header(header));
283                continue;
284            }
285
286            let Some(server_info) = item.server_info() else {
287                continue;
288            };
289            let server_selector = server_info.server_selector();
290            let is_remote = self
291                .lsp_store
292                .update(cx, |lsp_store, _| lsp_store.as_remote().is_some())
293                .unwrap_or(false);
294            let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector);
295
296            let (status_color, status_label) = server_info
297                .binary_status
298                .as_ref()
299                .and_then(|binary_status| match binary_status.status {
300                    BinaryStatus::None => None,
301                    BinaryStatus::CheckingForUpdate
302                    | BinaryStatus::Downloading
303                    | BinaryStatus::Starting => Some((Color::Modified, "Starting…")),
304                    BinaryStatus::Stopping | BinaryStatus::Stopped => {
305                        Some((Color::Disabled, "Stopped"))
306                    }
307                    BinaryStatus::Failed { .. } => Some((Color::Error, "Error")),
308                })
309                .or_else(|| {
310                    Some(match server_info.health? {
311                        ServerHealth::Ok => (Color::Success, "Running"),
312                        ServerHealth::Warning => (Color::Warning, "Warning"),
313                        ServerHealth::Error => (Color::Error, "Error"),
314                    })
315                })
316                .unwrap_or((Color::Success, "Running"));
317
318            let message = server_info
319                .message
320                .as_ref()
321                .or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
322                .cloned();
323
324            let (server_version, binary_path, process_id) = server_metadata
325                .get(&server_info.id)
326                .map(|(version, path, process_id)| {
327                    (
328                        version.clone(),
329                        path.as_ref()
330                            .map(|p| SharedString::from(p.compact().to_string_lossy().to_string())),
331                        *process_id,
332                    )
333                })
334                .unwrap_or((None, None, None));
335
336            let server_message = message.clone();
337
338            let submenu_server_name = server_info.name.clone();
339            let submenu_server_info = server_info.clone();
340
341            menu = menu.submenu_with_colored_icon(
342                server_info.name.0.clone(),
343                IconName::Circle,
344                status_color,
345                {
346                    let lsp_logs = lsp_logs.clone();
347                    let message = message.clone();
348                    let server_selector = server_selector.clone();
349                    let workspace = self.workspace.clone();
350                    let lsp_store = self.lsp_store.clone();
351                    let state = cx.entity().downgrade();
352                    let can_stop = submenu_server_info.can_stop();
353                    let process_memory_cache = process_memory_cache.clone();
354
355                    move |menu, _window, _cx| {
356                        let mut submenu = menu;
357
358                        if let Some(ref message) = message {
359                            let workspace_for_message = workspace.clone();
360                            let message_for_handler = message.clone();
361                            let server_name_for_message = submenu_server_name.clone();
362                            submenu = submenu.entry("View Message", None, move |window, cx| {
363                                let Some(create_buffer) = workspace_for_message
364                                    .update(cx, |workspace, cx| {
365                                        workspace.project().update(cx, |project, cx| {
366                                            project.create_buffer(None, false, cx)
367                                        })
368                                    })
369                                    .ok()
370                                else {
371                                    return;
372                                };
373
374                                let window_handle = window.window_handle();
375                                let workspace = workspace_for_message.clone();
376                                let message = message_for_handler.clone();
377                                let server_name = server_name_for_message.clone();
378                                cx.spawn(async move |cx| {
379                                    let buffer = create_buffer.await?;
380                                    buffer.update(cx, |buffer, cx| {
381                                        buffer.edit(
382                                            [(
383                                                0..0,
384                                                format!(
385                                                    "Language server {server_name}:\n\n{message}"
386                                                ),
387                                            )],
388                                            None,
389                                            cx,
390                                        );
391                                        buffer.set_capability(language::Capability::ReadOnly, cx);
392                                    });
393
394                                    workspace.update(cx, |workspace, cx| {
395                                        window_handle.update(cx, |_, window, cx| {
396                                            workspace.add_item_to_active_pane(
397                                                Box::new(cx.new(|cx| {
398                                                    let mut editor = Editor::for_buffer(
399                                                        buffer, None, window, cx,
400                                                    );
401                                                    editor.set_read_only(true);
402                                                    editor
403                                                })),
404                                                None,
405                                                true,
406                                                window,
407                                                cx,
408                                            );
409                                        })
410                                    })??;
411
412                                    anyhow::Ok(())
413                                })
414                                .detach();
415                            });
416                        }
417
418                        if has_logs {
419                            let lsp_logs_for_debug = lsp_logs.clone();
420                            let workspace_for_debug = workspace.clone();
421                            let server_selector_for_debug = server_selector.clone();
422                            submenu = submenu.entry("View Logs", None, move |window, cx| {
423                                lsp_log_view::open(
424                                    &lsp_logs_for_debug,
425                                    workspace_for_debug.clone(),
426                                    server_selector_for_debug.clone(),
427                                    window,
428                                    cx,
429                                );
430                            });
431                        }
432
433                        let state_for_restart = state.clone();
434                        let workspace_for_restart = workspace.clone();
435                        let lsp_store_for_restart = lsp_store.clone();
436                        let server_name_for_restart = submenu_server_name.clone();
437                        submenu = submenu.entry("Restart Server", None, move |_window, cx| {
438                            let Some(workspace) = workspace_for_restart.upgrade() else {
439                                return;
440                            };
441
442                            let project = workspace.read(cx).project().clone();
443                            let path_style = project.read(cx).path_style(cx);
444                            let buffer_store = project.read(cx).buffer_store().clone();
445
446                            let buffers = state_for_restart
447                                .update(cx, |state, cx| {
448                                    let server_buffers = state
449                                        .language_servers
450                                        .servers_per_buffer_abs_path
451                                        .iter()
452                                        .filter_map(|(abs_path, servers)| {
453                                            // Check if this server is associated with this path
454                                            let has_server = servers.servers.values().any(|name| {
455                                                name.as_ref() == Some(&server_name_for_restart)
456                                            });
457
458                                            if !has_server {
459                                                return None;
460                                            }
461
462                                            let worktree = servers.worktree.as_ref()?.upgrade()?;
463                                            let worktree_ref = worktree.read(cx);
464                                            let relative_path = abs_path
465                                                .strip_prefix(&worktree_ref.abs_path())
466                                                .ok()?;
467                                            let relative_path =
468                                                RelPath::new(relative_path, path_style)
469                                                    .log_err()?;
470                                            let entry =
471                                                worktree_ref.entry_for_path(&relative_path)?;
472                                            let project_path =
473                                                project.read(cx).path_for_entry(entry.id, cx)?;
474
475                                            buffer_store.read(cx).get_by_path(&project_path)
476                                        })
477                                        .collect::<Vec<_>>();
478
479                                    if server_buffers.is_empty() {
480                                        state
481                                            .language_servers
482                                            .servers_per_buffer_abs_path
483                                            .iter()
484                                            .filter_map(|(abs_path, servers)| {
485                                                let worktree =
486                                                    servers.worktree.as_ref()?.upgrade()?.read(cx);
487                                                let relative_path = abs_path
488                                                    .strip_prefix(&worktree.abs_path())
489                                                    .ok()?;
490                                                let relative_path =
491                                                    RelPath::new(relative_path, path_style)
492                                                        .log_err()?;
493                                                let entry =
494                                                    worktree.entry_for_path(&relative_path)?;
495                                                let project_path = project
496                                                    .read(cx)
497                                                    .path_for_entry(entry.id, cx)?;
498                                                buffer_store.read(cx).get_by_path(&project_path)
499                                            })
500                                            .collect()
501                                    } else {
502                                        server_buffers
503                                    }
504                                })
505                                .unwrap_or_default();
506
507                            if !buffers.is_empty() {
508                                lsp_store_for_restart
509                                    .update(cx, |lsp_store, cx| {
510                                        lsp_store.restart_language_servers_for_buffers(
511                                            buffers,
512                                            HashSet::from_iter([LanguageServerSelector::Name(
513                                                server_name_for_restart.clone(),
514                                            )]),
515                                            true,
516                                            cx,
517                                        );
518                                    })
519                                    .ok();
520                            }
521                        });
522
523                        if can_stop {
524                            let lsp_store_for_stop = lsp_store.clone();
525                            let server_selector_for_stop = server_selector.clone();
526
527                            submenu = submenu.entry("Stop Server", None, move |_window, cx| {
528                                lsp_store_for_stop
529                                    .update(cx, |lsp_store, cx| {
530                                        lsp_store
531                                            .stop_language_servers_for_buffers(
532                                                Vec::new(),
533                                                HashSet::from_iter([
534                                                    server_selector_for_stop.clone()
535                                                ]),
536                                                cx,
537                                            )
538                                            .detach_and_log_err(cx);
539                                    })
540                                    .ok();
541                            });
542                        }
543
544                        submenu = submenu.separator().custom_row({
545                            let binary_path = binary_path.clone();
546                            let server_version = server_version.clone();
547                            let server_message = server_message.clone();
548                            let process_memory_cache = process_memory_cache.clone();
549                            move |_, cx| {
550                                let memory_usage = process_id.map(|pid| {
551                                    process_memory_cache.borrow_mut().get_memory_usage(pid)
552                                });
553
554                                let memory_label = memory_usage.map(|bytes| {
555                                    if bytes >= 1024 * 1024 * 1024 {
556                                        format!(
557                                            "{:.1} GB",
558                                            bytes as f64 / (1024.0 * 1024.0 * 1024.0)
559                                        )
560                                    } else {
561                                        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
562                                    }
563                                });
564
565                                let version_label =
566                                    server_version.as_ref().map(|v| format!("v{}", v.as_ref()));
567
568                                let separator_color =
569                                    cx.theme().colors().icon_disabled.opacity(0.8);
570
571                                v_flex()
572                                    .id("metadata-container")
573                                    .gap_1()
574                                    .when_some(server_message.as_ref(), |this, _| {
575                                        this.w(rems_from_px(240.))
576                                    })
577                                    .child(
578                                        h_flex()
579                                            .ml_neg_1()
580                                            .gap_1()
581                                            .child(
582                                                Icon::new(IconName::Circle)
583                                                    .color(status_color)
584                                                    .size(IconSize::Small),
585                                            )
586                                            .child(
587                                                Label::new(status_label)
588                                                    .size(LabelSize::Small)
589                                                    .color(Color::Muted),
590                                            )
591                                            .when_some(version_label.as_ref(), |row, version| {
592                                                row.child(
593                                                    Icon::new(IconName::Dash)
594                                                        .color(Color::Custom(separator_color))
595                                                        .size(IconSize::XSmall),
596                                                )
597                                                .child(
598                                                    Label::new(version)
599                                                        .size(LabelSize::Small)
600                                                        .color(Color::Muted),
601                                                )
602                                            })
603                                            .when_some(memory_label.as_ref(), |row, memory| {
604                                                row.child(
605                                                    Icon::new(IconName::Dash)
606                                                        .color(Color::Custom(separator_color))
607                                                        .size(IconSize::XSmall),
608                                                )
609                                                .child(
610                                                    Label::new(memory)
611                                                        .size(LabelSize::Small)
612                                                        .color(Color::Muted),
613                                                )
614                                            }),
615                                    )
616                                    .when_some(server_message.clone(), |container, message| {
617                                        container.child(
618                                            Label::new(message)
619                                                .color(Color::Muted)
620                                                .size(LabelSize::Small),
621                                        )
622                                    })
623                                    .when_some(binary_path.clone(), |el, path| {
624                                        el.tooltip(Tooltip::text(path))
625                                    })
626                                    .into_any_element()
627                            }
628                        });
629
630                        submenu
631                    }
632                },
633            );
634        }
635        menu
636    }
637}
638
639impl LanguageServers {
640    fn update_binary_status(
641        &mut self,
642        binary_status: BinaryStatus,
643        message: Option<&str>,
644        name: LanguageServerName,
645    ) {
646        let binary_status_message = message.map(SharedString::new);
647        if matches!(
648            binary_status,
649            BinaryStatus::Stopped | BinaryStatus::Failed { .. }
650        ) {
651            self.health_statuses.retain(|_, server| server.name != name);
652        }
653        self.binary_statuses.insert(
654            name,
655            LanguageServerBinaryStatus {
656                status: binary_status,
657                message: binary_status_message,
658            },
659        );
660    }
661
662    fn update_server_health(
663        &mut self,
664        id: LanguageServerId,
665        health: ServerHealth,
666        message: Option<&str>,
667        name: Option<LanguageServerName>,
668    ) {
669        if let Some(state) = self.health_statuses.get_mut(&id) {
670            state.health = Some((message.map(SharedString::new), health));
671            if let Some(name) = name {
672                state.name = name;
673            }
674        } else if let Some(name) = name {
675            self.health_statuses.insert(
676                id,
677                LanguageServerHealthStatus {
678                    health: Some((message.map(SharedString::new), health)),
679                    name,
680                },
681            );
682        }
683    }
684
685    fn is_empty(&self) -> bool {
686        self.binary_statuses.is_empty() && self.health_statuses.is_empty()
687    }
688
689    /// Drop all id-keyed state for a server that has been removed (stopped or
690    /// reaching end-of-life via restart). `binary_statuses` is intentionally
691    /// preserved — it is keyed by name and shared across restart cycles to
692    /// drive the "Downloading… → Starting…" status UX.
693    fn remove_server(&mut self, server_id: LanguageServerId) {
694        self.health_statuses.remove(&server_id);
695        self.servers_per_buffer_abs_path
696            .retain(|_, servers_for_path| {
697                servers_for_path.servers.remove(&server_id);
698                !servers_for_path.servers.is_empty()
699            });
700    }
701}
702
703#[derive(Debug)]
704enum ServerData<'a> {
705    WithHealthCheck {
706        server_id: LanguageServerId,
707        health: &'a LanguageServerHealthStatus,
708        binary_status: Option<&'a LanguageServerBinaryStatus>,
709    },
710    WithBinaryStatus {
711        server_id: LanguageServerId,
712        server_name: &'a LanguageServerName,
713        binary_status: &'a LanguageServerBinaryStatus,
714    },
715}
716
717#[derive(Debug)]
718enum LspMenuItem {
719    WithHealthCheck {
720        server_id: LanguageServerId,
721        health: LanguageServerHealthStatus,
722        binary_status: Option<LanguageServerBinaryStatus>,
723    },
724    WithBinaryStatus {
725        server_id: LanguageServerId,
726        server_name: LanguageServerName,
727        binary_status: LanguageServerBinaryStatus,
728    },
729    ToggleServersButton {
730        restart: bool,
731    },
732    Header {
733        header: Option<SharedString>,
734        separator: bool,
735    },
736}
737
738impl LspMenuItem {
739    fn server_info(&self) -> Option<ServerInfo> {
740        match self {
741            Self::Header { .. } => None,
742            Self::ToggleServersButton { .. } => None,
743            Self::WithHealthCheck {
744                server_id,
745                health,
746                binary_status,
747                ..
748            } => Some(ServerInfo {
749                name: health.name.clone(),
750                id: *server_id,
751                health: health.health(),
752                binary_status: binary_status.clone(),
753                message: health.message(),
754            }),
755            Self::WithBinaryStatus {
756                server_id,
757                server_name,
758                binary_status,
759                ..
760            } => Some(ServerInfo {
761                name: server_name.clone(),
762                id: *server_id,
763                health: None,
764                binary_status: Some(binary_status.clone()),
765                message: binary_status.message.clone(),
766            }),
767        }
768    }
769}
770
771impl ServerData<'_> {
772    fn into_lsp_item(self) -> LspMenuItem {
773        match self {
774            Self::WithHealthCheck {
775                server_id,
776                health,
777                binary_status,
778                ..
779            } => LspMenuItem::WithHealthCheck {
780                server_id,
781                health: health.clone(),
782                binary_status: binary_status.cloned(),
783            },
784            Self::WithBinaryStatus {
785                server_id,
786                server_name,
787                binary_status,
788                ..
789            } => LspMenuItem::WithBinaryStatus {
790                server_id,
791                server_name: server_name.clone(),
792                binary_status: binary_status.clone(),
793            },
794        }
795    }
796}
797
798impl LspButton {
799    pub fn new(
800        workspace: &Workspace,
801        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
802        window: &mut Window,
803        cx: &mut Context<Self>,
804    ) -> Self {
805        let settings_subscription =
806            cx.observe_global_in::<SettingsStore>(window, move |lsp_button, window, cx| {
807                if ProjectSettings::get_global(cx).global_lsp_settings.button {
808                    if lsp_button.lsp_menu.is_none() {
809                        lsp_button.refresh_lsp_menu(true, window, cx);
810                    }
811                } else if lsp_button.lsp_menu.take().is_some() {
812                    cx.notify();
813                }
814            });
815
816        let lsp_store = workspace.project().read(cx).lsp_store();
817        let mut language_servers = LanguageServers::default();
818        for (_, status) in lsp_store.read(cx).language_server_statuses() {
819            language_servers.binary_statuses.insert(
820                status.name.clone(),
821                LanguageServerBinaryStatus {
822                    status: BinaryStatus::None,
823                    message: None,
824                },
825            );
826        }
827
828        let lsp_store_subscription =
829            cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| {
830                lsp_button.on_lsp_store_event(e, window, cx)
831            });
832
833        let server_state = cx.new(|_| LanguageServerState {
834            workspace: workspace.weak_handle(),
835            items: Vec::new(),
836            lsp_store: lsp_store.downgrade(),
837            active_editor: None,
838            language_servers,
839            process_memory_cache: Rc::new(RefCell::new(ProcessMemoryCache::new())),
840        });
841
842        let mut lsp_button = Self {
843            server_state,
844            popover_menu_handle,
845            lsp_menu: None,
846            lsp_menu_refresh: Task::ready(()),
847            _subscriptions: vec![settings_subscription, lsp_store_subscription],
848        };
849        if !lsp_button
850            .server_state
851            .read(cx)
852            .language_servers
853            .binary_statuses
854            .is_empty()
855        {
856            lsp_button.refresh_lsp_menu(true, window, cx);
857        }
858
859        lsp_button
860    }
861
862    fn on_lsp_store_event(
863        &mut self,
864        e: &LspStoreEvent,
865        window: &mut Window,
866        cx: &mut Context<Self>,
867    ) {
868        if self.lsp_menu.is_none() {
869            return;
870        };
871        let mut updated = false;
872
873        // TODO `LspStore` is global and reports status from all language servers, even from the other windows.
874        match e {
875            LspStoreEvent::LanguageServerUpdate {
876                language_server_id,
877                name,
878                message: proto::update_language_server::Variant::StatusUpdate(status_update),
879            } => match &status_update.status {
880                Some(proto::status_update::Status::Binary(binary_status)) => {
881                    let Some(name) = name.as_ref() else {
882                        return;
883                    };
884                    if let Some(binary_status) = proto::ServerBinaryStatus::from_i32(*binary_status)
885                    {
886                        let binary_status = match binary_status {
887                            proto::ServerBinaryStatus::None => BinaryStatus::None,
888                            proto::ServerBinaryStatus::CheckingForUpdate => {
889                                BinaryStatus::CheckingForUpdate
890                            }
891                            proto::ServerBinaryStatus::Downloading => BinaryStatus::Downloading,
892                            proto::ServerBinaryStatus::Starting => BinaryStatus::Starting,
893                            proto::ServerBinaryStatus::Stopping => BinaryStatus::Stopping,
894                            proto::ServerBinaryStatus::Stopped => BinaryStatus::Stopped,
895                            proto::ServerBinaryStatus::Failed => {
896                                let Some(error) = status_update.message.clone() else {
897                                    return;
898                                };
899                                BinaryStatus::Failed { error }
900                            }
901                        };
902                        self.server_state.update(cx, |state, _| {
903                            state.language_servers.update_binary_status(
904                                binary_status,
905                                status_update.message.as_deref(),
906                                name.clone(),
907                            );
908                        });
909                        updated = true;
910                    };
911                }
912                Some(proto::status_update::Status::Health(health_status)) => {
913                    if let Some(health) = proto::ServerHealth::from_i32(*health_status) {
914                        let health = match health {
915                            proto::ServerHealth::Ok => ServerHealth::Ok,
916                            proto::ServerHealth::Warning => ServerHealth::Warning,
917                            proto::ServerHealth::Error => ServerHealth::Error,
918                        };
919                        self.server_state.update(cx, |state, _| {
920                            state.language_servers.update_server_health(
921                                *language_server_id,
922                                health,
923                                status_update.message.as_deref(),
924                                name.clone(),
925                            );
926                        });
927                        updated = true;
928                    }
929                }
930                None => {}
931            },
932            LspStoreEvent::LanguageServerUpdate {
933                language_server_id,
934                name,
935                message: proto::update_language_server::Variant::RegisteredForBuffer(update),
936                ..
937            } => {
938                self.server_state.update(cx, |state, cx| {
939                    let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
940                        workspace
941                            .project()
942                            .read(cx)
943                            .find_worktree(Path::new(&update.buffer_abs_path), cx)
944                            .map(|(worktree, _)| worktree.downgrade())
945                    }) else {
946                        return;
947                    };
948                    let entry = state
949                        .language_servers
950                        .servers_per_buffer_abs_path
951                        .entry(PathBuf::from(&update.buffer_abs_path))
952                        .or_insert_with(|| ServersForPath {
953                            servers: HashMap::default(),
954                            worktree: worktree.clone(),
955                        });
956                    entry.servers.insert(*language_server_id, name.clone());
957                    if worktree.is_some() {
958                        entry.worktree = worktree;
959                    }
960                });
961                updated = true;
962            }
963            LspStoreEvent::LanguageServerRemoved(server_id) => {
964                self.server_state.update(cx, |state, _| {
965                    state.language_servers.remove_server(*server_id);
966                });
967                updated = true;
968            }
969            _ => {}
970        };
971
972        if updated {
973            self.refresh_lsp_menu(false, window, cx);
974        }
975    }
976
977    fn regenerate_items(&mut self, cx: &mut App) {
978        self.server_state.update(cx, |state, cx| {
979            let active_worktrees = state
980                .active_editor
981                .as_ref()
982                .into_iter()
983                .flat_map(|active_editor| {
984                    active_editor
985                        .editor
986                        .upgrade()
987                        .into_iter()
988                        .flat_map(|active_editor| {
989                            active_editor
990                                .read(cx)
991                                .buffer()
992                                .read(cx)
993                                .all_buffers()
994                                .into_iter()
995                                .filter_map(|buffer| {
996                                    project::File::from_dyn(buffer.read(cx).file())
997                                })
998                                .map(|buffer_file| buffer_file.worktree.clone())
999                        })
1000                })
1001                .collect::<HashSet<_>>();
1002
1003            let mut server_ids_to_worktrees =
1004                HashMap::<LanguageServerId, Entity<Worktree>>::default();
1005            let mut server_names_to_worktrees = HashMap::<
1006                LanguageServerName,
1007                HashSet<(Entity<Worktree>, LanguageServerId)>,
1008            >::default();
1009            for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
1010                if let Some(worktree) = servers_for_path
1011                    .worktree
1012                    .as_ref()
1013                    .and_then(|worktree| worktree.upgrade())
1014                {
1015                    for (server_id, server_name) in &servers_for_path.servers {
1016                        server_ids_to_worktrees.insert(*server_id, worktree.clone());
1017                        if let Some(server_name) = server_name {
1018                            server_names_to_worktrees
1019                                .entry(server_name.clone())
1020                                .or_default()
1021                                .insert((worktree.clone(), *server_id));
1022                        }
1023                    }
1024                }
1025            }
1026            state
1027                .lsp_store
1028                .update(cx, |lsp_store, cx| {
1029                    for (server_id, status) in lsp_store.language_server_statuses() {
1030                        if let Some(worktree) = status.worktree.and_then(|worktree_id| {
1031                            lsp_store
1032                                .worktree_store()
1033                                .read(cx)
1034                                .worktree_for_id(worktree_id, cx)
1035                        }) {
1036                            server_ids_to_worktrees.insert(server_id, worktree.clone());
1037                            server_names_to_worktrees
1038                                .entry(status.name.clone())
1039                                .or_default()
1040                                .insert((worktree, server_id));
1041                        }
1042                    }
1043                })
1044                .ok();
1045
1046            let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
1047            let mut servers_with_health_checks = HashSet::default();
1048
1049            for (server_id, health) in &state.language_servers.health_statuses {
1050                let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
1051                    let worktrees = server_names_to_worktrees.get(&health.name)?;
1052                    worktrees
1053                        .iter()
1054                        .find(|(worktree, _)| active_worktrees.contains(worktree))
1055                        .or_else(|| worktrees.iter().next())
1056                        .map(|(worktree, _)| worktree)
1057                });
1058                servers_with_health_checks.insert(&health.name);
1059                let worktree_name =
1060                    worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name_str()));
1061
1062                let binary_status = state.language_servers.binary_statuses.get(&health.name);
1063                let server_data = ServerData::WithHealthCheck {
1064                    server_id: *server_id,
1065                    health,
1066                    binary_status,
1067                };
1068                if let Some(worktree_name) = worktree_name {
1069                    servers_per_worktree
1070                        .entry(worktree_name.clone())
1071                        .or_default()
1072                        .push(server_data);
1073                }
1074            }
1075
1076            let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
1077            let mut can_restart_all = state.language_servers.health_statuses.is_empty();
1078            for (server_name, binary_status) in state
1079                .language_servers
1080                .binary_statuses
1081                .iter()
1082                .filter(|(name, _)| !servers_with_health_checks.contains(name))
1083            {
1084                match binary_status.status {
1085                    BinaryStatus::None => {
1086                        can_restart_all = false;
1087                        can_stop_all |= true;
1088                    }
1089                    BinaryStatus::CheckingForUpdate => {
1090                        can_restart_all = false;
1091                        can_stop_all = false;
1092                    }
1093                    BinaryStatus::Downloading => {
1094                        can_restart_all = false;
1095                        can_stop_all = false;
1096                    }
1097                    BinaryStatus::Starting => {
1098                        can_restart_all = false;
1099                        can_stop_all = false;
1100                    }
1101                    BinaryStatus::Stopping => {
1102                        can_restart_all = false;
1103                        can_stop_all = false;
1104                    }
1105                    BinaryStatus::Stopped => {}
1106                    BinaryStatus::Failed { .. } => {}
1107                }
1108
1109                if let Some(worktrees_for_name) = server_names_to_worktrees.get(server_name)
1110                    && let Some((worktree, server_id)) = worktrees_for_name
1111                        .iter()
1112                        .find(|(worktree, _)| active_worktrees.contains(worktree))
1113                        .or_else(|| worktrees_for_name.iter().next())
1114                {
1115                    let worktree_name = SharedString::new(worktree.read(cx).root_name_str());
1116                    servers_per_worktree
1117                        .entry(worktree_name.clone())
1118                        .or_default()
1119                        .push(ServerData::WithBinaryStatus {
1120                            server_name,
1121                            binary_status,
1122                            server_id: *server_id,
1123                        });
1124                }
1125            }
1126
1127            let mut new_lsp_items = Vec::with_capacity(servers_per_worktree.len() + 1);
1128            for (worktree_name, worktree_servers) in servers_per_worktree {
1129                if worktree_servers.is_empty() {
1130                    continue;
1131                }
1132                new_lsp_items.push(LspMenuItem::Header {
1133                    header: Some(worktree_name),
1134                    separator: false,
1135                });
1136                new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
1137            }
1138            if !new_lsp_items.is_empty() {
1139                if can_stop_all {
1140                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
1141                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false });
1142                } else if can_restart_all {
1143                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
1144                }
1145            }
1146
1147            state.items = new_lsp_items;
1148        });
1149    }
1150
1151    fn refresh_lsp_menu(
1152        &mut self,
1153        create_if_empty: bool,
1154        window: &mut Window,
1155        cx: &mut Context<Self>,
1156    ) {
1157        if create_if_empty || self.lsp_menu.is_some() {
1158            let state = self.server_state.clone();
1159            self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_button, cx| {
1160                cx.background_executor()
1161                    .timer(Duration::from_millis(30))
1162                    .await;
1163                lsp_button
1164                    .update_in(cx, |lsp_button, window, cx| {
1165                        lsp_button.regenerate_items(cx);
1166                        let menu = ContextMenu::build(window, cx, |menu, _, cx| {
1167                            state.update(cx, |state, cx| state.fill_menu(menu, cx))
1168                        });
1169                        lsp_button.lsp_menu = Some(menu.clone());
1170                        lsp_button.popover_menu_handle.refresh_menu(
1171                            window,
1172                            cx,
1173                            Rc::new(move |_, _| Some(menu.clone())),
1174                        );
1175                        cx.notify();
1176                    })
1177                    .ok();
1178            });
1179        }
1180    }
1181}
1182
1183impl StatusItemView for LspButton {
1184    fn set_active_pane_item(
1185        &mut self,
1186        active_pane_item: Option<&dyn workspace::ItemHandle>,
1187        window: &mut Window,
1188        cx: &mut Context<Self>,
1189    ) {
1190        if ProjectSettings::get_global(cx).global_lsp_settings.button {
1191            if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
1192                if Some(&editor)
1193                    != self
1194                        .server_state
1195                        .read(cx)
1196                        .active_editor
1197                        .as_ref()
1198                        .and_then(|active_editor| active_editor.editor.upgrade())
1199                        .as_ref()
1200                {
1201                    let editor_buffers = HashSet::from_iter(
1202                        editor
1203                            .read(cx)
1204                            .buffer()
1205                            .read(cx)
1206                            .snapshot(cx)
1207                            .excerpts()
1208                            .map(|excerpt| excerpt.context.start.buffer_id),
1209                    );
1210                    let _editor_subscription = cx.subscribe_in(
1211                        &editor,
1212                        window,
1213                        |lsp_button, _, e: &EditorEvent, window, cx| match e {
1214                            EditorEvent::BufferRangesUpdated { buffer, .. } => {
1215                                let updated = lsp_button.server_state.update(cx, |state, cx| {
1216                                    if let Some(active_editor) = state.active_editor.as_mut() {
1217                                        let buffer_id = buffer.read(cx).remote_id();
1218                                        active_editor.editor_buffers.insert(buffer_id)
1219                                    } else {
1220                                        false
1221                                    }
1222                                });
1223                                if updated {
1224                                    lsp_button.refresh_lsp_menu(false, window, cx);
1225                                }
1226                            }
1227                            EditorEvent::BuffersRemoved { removed_buffer_ids } => {
1228                                let removed = lsp_button.server_state.update(cx, |state, _| {
1229                                    let mut removed = false;
1230                                    if let Some(active_editor) = state.active_editor.as_mut() {
1231                                        for id in removed_buffer_ids {
1232                                            active_editor.editor_buffers.retain(|buffer_id| {
1233                                                let retain = buffer_id != id;
1234                                                removed |= !retain;
1235                                                retain
1236                                            });
1237                                        }
1238                                    }
1239                                    removed
1240                                });
1241                                if removed {
1242                                    lsp_button.refresh_lsp_menu(false, window, cx);
1243                                }
1244                            }
1245                            _ => {}
1246                        },
1247                    );
1248                    self.server_state.update(cx, |state, _| {
1249                        state.active_editor = Some(ActiveEditor {
1250                            editor: editor.downgrade(),
1251                            _editor_subscription,
1252                            editor_buffers,
1253                        });
1254                    });
1255                    self.refresh_lsp_menu(true, window, cx);
1256                }
1257            } else if self.server_state.read(cx).active_editor.is_some() {
1258                self.server_state.update(cx, |state, _| {
1259                    state.active_editor = None;
1260                });
1261                self.refresh_lsp_menu(false, window, cx);
1262            }
1263        } else if self.server_state.read(cx).active_editor.is_some() {
1264            self.server_state.update(cx, |state, _| {
1265                state.active_editor = None;
1266            });
1267            self.refresh_lsp_menu(false, window, cx);
1268        }
1269    }
1270
1271    fn hide_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
1272        Some(workspace::HideStatusItem::new(|settings| {
1273            settings.global_lsp_settings.get_or_insert_default().button = Some(false);
1274        }))
1275    }
1276}
1277
1278impl Render for LspButton {
1279    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
1280        if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
1281            return div().hidden();
1282        }
1283
1284        let state = self.server_state.read(cx);
1285        let is_via_ssh = state
1286            .workspace
1287            .upgrade()
1288            .map(|workspace| workspace.read(cx).project().read(cx).is_via_remote_server())
1289            .unwrap_or(false);
1290
1291        let mut has_errors = false;
1292        let mut has_warnings = false;
1293        let mut has_other_notifications = false;
1294        for binary_status in state.language_servers.binary_statuses.values() {
1295            has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
1296            has_other_notifications |= binary_status.message.is_some();
1297        }
1298
1299        for server in state.language_servers.health_statuses.values() {
1300            if let Some((message, health)) = &server.health {
1301                has_other_notifications |= message.is_some();
1302                match health {
1303                    ServerHealth::Ok => {}
1304                    ServerHealth::Warning => has_warnings = true,
1305                    ServerHealth::Error => has_errors = true,
1306                }
1307            }
1308        }
1309
1310        let (indicator, description) = if has_errors {
1311            (
1312                Some(Indicator::dot().color(Color::Error)),
1313                "Server with errors",
1314            )
1315        } else if has_warnings {
1316            (
1317                Some(Indicator::dot().color(Color::Warning)),
1318                "Server with warnings",
1319            )
1320        } else if has_other_notifications {
1321            (
1322                Some(Indicator::dot().color(Color::Modified)),
1323                "Server with notifications",
1324            )
1325        } else {
1326            (None, "All Servers Operational")
1327        };
1328
1329        let lsp_button = cx.weak_entity();
1330
1331        div().child(
1332            PopoverMenu::new("lsp-tool")
1333                .on_open(Rc::new(move |_window, cx| {
1334                    let copilot_enabled = all_language_settings(None, cx).edit_predictions.provider
1335                        == EditPredictionProvider::Copilot;
1336                    telemetry::event!(
1337                        "Toolbar Menu Opened",
1338                        name = "Language Servers",
1339                        copilot_enabled,
1340                        is_via_ssh,
1341                    );
1342                }))
1343                .menu(move |_, cx| {
1344                    lsp_button
1345                        .read_with(cx, |lsp_button, _| lsp_button.lsp_menu.clone())
1346                        .ok()
1347                        .flatten()
1348                })
1349                .anchor(Anchor::BottomLeft)
1350                .with_handle(self.popover_menu_handle.clone())
1351                .trigger_with_tooltip(
1352                    IconButton::new("zed-lsp-tool-button", IconName::BoltOutlined)
1353                        .when_some(indicator, IconButton::indicator)
1354                        .icon_size(IconSize::Small)
1355                        .tab_index(0isize)
1356                        .aria_label("Language Servers")
1357                        .indicator_border_color(Some(cx.theme().colors().status_bar_background)),
1358                    move |_window, cx| {
1359                        Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx)
1360                    },
1361                ),
1362        )
1363    }
1364}
1365
1366#[cfg(test)]
1367mod tests {
1368    use super::*;
1369
1370    fn server_id(n: usize) -> LanguageServerId {
1371        LanguageServerId(n)
1372    }
1373
1374    fn server_name(s: &str) -> LanguageServerName {
1375        LanguageServerName(s.into())
1376    }
1377
1378    fn health_status(name: &str) -> LanguageServerHealthStatus {
1379        LanguageServerHealthStatus {
1380            name: server_name(name),
1381            health: Some((None, ServerHealth::Ok)),
1382        }
1383    }
1384
1385    fn servers_for_path(servers: &[(LanguageServerId, &str)]) -> ServersForPath {
1386        ServersForPath {
1387            servers: servers
1388                .iter()
1389                .map(|(id, name)| (*id, Some(server_name(name))))
1390                .collect(),
1391            worktree: None,
1392        }
1393    }
1394
1395    /// `remove_server` evicts the id from `health_statuses` so a restarted
1396    /// server's new id renders without inheriting the old one's stale entry.
1397    /// This is the regression test for #53627.
1398    #[test]
1399    fn remove_server_drops_health_entry_for_id() {
1400        let mut state = LanguageServers::default();
1401        state
1402            .health_statuses
1403            .insert(server_id(1), health_status("rust-analyzer"));
1404        state
1405            .health_statuses
1406            .insert(server_id(2), health_status("typescript-language-server"));
1407
1408        state.remove_server(server_id(1));
1409
1410        assert!(!state.health_statuses.contains_key(&server_id(1)));
1411        assert!(state.health_statuses.contains_key(&server_id(2)));
1412    }
1413
1414    /// `remove_server` evicts the id from each per-buffer entry; entries that
1415    /// become empty are dropped so the map does not grow unbounded across
1416    /// many buffer opens/closes.
1417    #[test]
1418    fn remove_server_evicts_id_from_per_buffer_entries_and_drops_empty_entries() {
1419        let mut state = LanguageServers::default();
1420        let buffer_a = PathBuf::from("/project/a.rs");
1421        let buffer_b = PathBuf::from("/project/b.rs");
1422
1423        state.servers_per_buffer_abs_path.insert(
1424            buffer_a.clone(),
1425            servers_for_path(&[(server_id(1), "rust-analyzer")]),
1426        );
1427        state.servers_per_buffer_abs_path.insert(
1428            buffer_b.clone(),
1429            servers_for_path(&[(server_id(1), "rust-analyzer"), (server_id(2), "typos-lsp")]),
1430        );
1431
1432        state.remove_server(server_id(1));
1433
1434        assert!(
1435            !state.servers_per_buffer_abs_path.contains_key(&buffer_a),
1436            "buffer_a's entry held only the removed server, so the entry itself should be dropped",
1437        );
1438        let buffer_b_entry = state
1439            .servers_per_buffer_abs_path
1440            .get(&buffer_b)
1441            .expect("buffer_b's entry has another server, so it must be retained");
1442        assert!(!buffer_b_entry.servers.contains_key(&server_id(1)));
1443        assert!(buffer_b_entry.servers.contains_key(&server_id(2)));
1444    }
1445
1446    /// `binary_statuses` is keyed by name and intentionally shared across
1447    /// restart cycles to drive the "Downloading… → Starting…" UX. Removing a
1448    /// single server's id must not touch it.
1449    #[test]
1450    fn remove_server_does_not_touch_binary_statuses() {
1451        let mut state = LanguageServers::default();
1452        state.binary_statuses.insert(
1453            server_name("rust-analyzer"),
1454            LanguageServerBinaryStatus {
1455                status: BinaryStatus::Starting,
1456                message: None,
1457            },
1458        );
1459
1460        state.remove_server(server_id(1));
1461
1462        assert!(
1463            state
1464                .binary_statuses
1465                .contains_key(&server_name("rust-analyzer")),
1466            "binary_statuses is name-keyed and shared across restart cycles",
1467        );
1468    }
1469
1470    /// Simulates the full restart event sequence: remove old id, register
1471    /// new id with same name, write health for the new id. After restart
1472    /// only the new id should be visible — no leftover entry from the old
1473    /// incarnation.
1474    #[test]
1475    fn restart_sequence_leaves_only_new_server_id() {
1476        let mut state = LanguageServers::default();
1477        let buffer = PathBuf::from("/project/main.rs");
1478        let name = "rust-analyzer";
1479
1480        // Pre-restart: server v1 is registered for the buffer with health.
1481        state
1482            .servers_per_buffer_abs_path
1483            .insert(buffer.clone(), servers_for_path(&[(server_id(1), name)]));
1484        state
1485            .health_statuses
1486            .insert(server_id(1), health_status(name));
1487
1488        // Restart: old id is removed.
1489        state.remove_server(server_id(1));
1490
1491        // New id registers for the same buffer.
1492        let entry = state
1493            .servers_per_buffer_abs_path
1494            .entry(buffer.clone())
1495            .or_insert_with(|| ServersForPath {
1496                servers: HashMap::default(),
1497                worktree: None,
1498            });
1499        entry.servers.insert(server_id(2), Some(server_name(name)));
1500
1501        // Health update for the new id arrives.
1502        state
1503            .health_statuses
1504            .insert(server_id(2), health_status(name));
1505
1506        let entry = state
1507            .servers_per_buffer_abs_path
1508            .get(&buffer)
1509            .expect("buffer must still be tracked");
1510        assert_eq!(
1511            entry.servers.keys().copied().collect::<Vec<_>>(),
1512            vec![server_id(2)],
1513            "exactly one server for this buffer — the new incarnation",
1514        );
1515        assert!(
1516            !state.health_statuses.contains_key(&server_id(1)),
1517            "the dead server's health entry must not linger",
1518        );
1519        assert!(
1520            state.health_statuses.contains_key(&server_id(2)),
1521            "the new server's health entry is present",
1522        );
1523    }
1524}
1525
Served at tenant.openagents/omega Member data and write actions are omitted.