ts_bridge/
server.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2use std::io::{self, BufReader};
3use std::net::{TcpListener, TcpStream};
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
7use std::sync::{Arc, Mutex};
8use std::thread;
9use std::time::{Duration, SystemTime, UNIX_EPOCH};
10
11use anyhow::{Context, anyhow};
12use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError, bounded, unbounded};
13use lsp_server::{
14    Connection, ErrorCode, Message, Notification as ServerNotification, Request, RequestId,
15    Response,
16};
17use lsp_types::{
18    CodeActionKind, CodeActionOptions, CodeActionProviderCapability, CompletionOptions,
19    ExecuteCommandOptions, HoverProviderCapability, InitializeParams, InitializeResult,
20    InlayHintOptions, InlayHintServerCapabilities, OneOf, PositionEncodingKind, ProgressParams,
21    ProgressParamsValue, ProgressToken, PublishDiagnosticsParams, RenameOptions,
22    ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
23    TextDocumentSyncOptions, TextDocumentSyncSaveOptions, TypeDefinitionProviderCapability,
24    WorkDoneProgress as LspWorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressCreateParams,
25    WorkDoneProgressEnd, WorkDoneProgressReport,
26    notification::{
27        DidChangeConfiguration, DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument,
28        Notification as LspNotification, Progress, PublishDiagnostics,
29    },
30    request::{
31        InlayHintRefreshRequest, InlayHintRequest, Request as LspRequest, WorkDoneProgressCreate,
32    },
33};
34use serde_json::{self, Map, Value, json};
35
36use crate::config::{Config, PluginSettings};
37use crate::documents::{DocumentStore, OpenDocumentSnapshot, TextSpan};
38use crate::process::ServerKind;
39use crate::protocol::diagnostics::{DiagnosticsEvent, DiagnosticsKind};
40use crate::protocol::text_document::completion::TRIGGER_CHARACTERS;
41use crate::protocol::text_document::signature_help::TRIGGER_CHARACTERS as SIG_HELP_TRIGGER_CHARACTERS;
42use crate::protocol::{self, AdapterResult, ResponseAdapter};
43use crate::provider::Provider;
44use crate::rpc::{DispatchReceipt, Priority, Route, ServerEvent, Service, ServiceError};
45use crate::utils::uri_to_file_path;
46
47const DEFAULT_INLAY_HINT_SPAN: u32 = 5_000_000;
48const DEFAULT_DAEMON_IDLE_TTL: Duration = Duration::from_secs(30 * 60);
49
50fn current_epoch_seconds() -> u64 {
51    match SystemTime::now().duration_since(UNIX_EPOCH) {
52        Ok(duration) => duration.as_secs(),
53        Err(_) => 0,
54    }
55}
56
57fn idle_sweep_interval(idle_ttl: Duration) -> Duration {
58    let min_interval = Duration::from_secs(5);
59    let max_interval = Duration::from_secs(60);
60    let mut interval = Duration::from_secs(idle_ttl.as_secs().saturating_div(2));
61    if interval < min_interval {
62        interval = min_interval;
63    }
64    if interval > max_interval {
65        interval = max_interval;
66    }
67    interval
68}
69
70/// Runs the LSP server over stdio. This is the entry-point Neovim (or any LSP
71/// client) will execute.
72pub fn run_stdio_server() -> anyhow::Result<()> {
73    let _ = env_logger::try_init();
74    let registry = ProjectRegistry::new(None);
75    let (connection, io_threads) = Connection::stdio();
76    run_session(connection, &registry)?;
77    io_threads.join()?;
78
79    Ok(())
80}
81
82#[derive(Debug)]
83pub struct DaemonConfig {
84    pub listen: Option<std::net::SocketAddr>,
85    pub socket: Option<PathBuf>,
86    pub idle_ttl: Option<Duration>,
87}
88
89impl Default for DaemonConfig {
90    fn default() -> Self {
91        Self {
92            listen: None,
93            socket: None,
94            idle_ttl: Some(DEFAULT_DAEMON_IDLE_TTL),
95        }
96    }
97}
98
99pub fn run_daemon_server(config: DaemonConfig) -> anyhow::Result<()> {
100    let _ = env_logger::try_init();
101    if config.listen.is_some() && config.socket.is_some() {
102        return Err(anyhow!("daemon listen and socket cannot be used together"));
103    }
104
105    let registry = ProjectRegistry::new(config.idle_ttl);
106
107    if let Some(socket_path) = config.socket {
108        return run_daemon_unix(socket_path, registry);
109    }
110
111    let addr = config
112        .listen
113        .unwrap_or_else(|| "127.0.0.1:0".parse().expect("valid default addr"));
114    run_daemon_tcp(addr, registry)
115}
116
117fn run_daemon_tcp(addr: std::net::SocketAddr, registry: ProjectRegistry) -> anyhow::Result<()> {
118    let listener = TcpListener::bind(addr).context("bind daemon listener")?;
119    let bound = listener
120        .local_addr()
121        .context("resolve daemon listen addr")?;
122    log::info!("daemon listening on {bound}");
123
124    loop {
125        let (stream, peer) = match listener.accept() {
126            Ok(accepted) => accepted,
127            Err(err) => {
128                log::warn!("daemon accept failed: {err}");
129                continue;
130            }
131        };
132        log::info!("daemon accepted connection from {peer}");
133        if let Err(err) = stream.set_nodelay(true) {
134            log::debug!("failed to set TCP_NODELAY for {peer}: {err}");
135        }
136        let registry = registry.clone();
137        thread::spawn(move || {
138            if let Err(err) = run_stream_session(stream, registry) {
139                log::warn!("session from {peer} exited with error: {err:?}");
140            }
141        });
142    }
143}
144
145#[cfg(unix)]
146fn run_daemon_unix(socket_path: PathBuf, registry: ProjectRegistry) -> anyhow::Result<()> {
147    use std::fs;
148    use std::os::unix::net::UnixListener;
149
150    if socket_path.exists() {
151        fs::remove_file(&socket_path)
152            .with_context(|| format!("remove existing socket {}", socket_path.display()))?;
153    }
154    let listener = UnixListener::bind(&socket_path)
155        .with_context(|| format!("bind unix socket {}", socket_path.display()))?;
156    log::info!("daemon listening on {}", socket_path.display());
157
158    loop {
159        let (stream, _) = match listener.accept() {
160            Ok(accepted) => accepted,
161            Err(err) => {
162                log::warn!("daemon accept failed: {err}");
163                continue;
164            }
165        };
166        log::info!("daemon accepted unix connection");
167        let registry = registry.clone();
168        thread::spawn(move || {
169            if let Err(err) = run_unix_stream_session(stream, registry) {
170                log::warn!("unix session exited with error: {err:?}");
171            }
172        });
173    }
174}
175
176#[cfg(not(unix))]
177fn run_daemon_unix(_socket_path: PathBuf, _registry: ProjectRegistry) -> anyhow::Result<()> {
178    Err(anyhow!(
179        "unix domain sockets are not supported on this platform"
180    ))
181}
182
183fn run_stream_session(stream: TcpStream, registry: ProjectRegistry) -> anyhow::Result<()> {
184    let (connection, io_threads) = connection_from_stream(stream)?;
185    run_session(connection, &registry)?;
186    io_threads
187        .join()
188        .context("daemon session IO threads failed")?;
189    Ok(())
190}
191
192#[cfg(unix)]
193fn run_unix_stream_session(
194    stream: std::os::unix::net::UnixStream,
195    registry: ProjectRegistry,
196) -> anyhow::Result<()> {
197    let (connection, io_threads) = connection_from_stream(stream)?;
198    run_session(connection, &registry)?;
199    io_threads
200        .join()
201        .context("daemon session IO threads failed")?;
202    Ok(())
203}
204
205trait CloneableStream: io::Read + io::Write + Send + 'static + Sized {
206    fn try_clone(&self) -> io::Result<Self>;
207}
208
209impl CloneableStream for TcpStream {
210    fn try_clone(&self) -> io::Result<Self> {
211        TcpStream::try_clone(self)
212    }
213}
214
215#[cfg(unix)]
216impl CloneableStream for std::os::unix::net::UnixStream {
217    fn try_clone(&self) -> io::Result<Self> {
218        std::os::unix::net::UnixStream::try_clone(self)
219    }
220}
221
222fn connection_from_stream<S: CloneableStream>(stream: S) -> anyhow::Result<(Connection, DaemonIo)> {
223    let reader_stream = stream.try_clone().context("clone daemon stream")?;
224    let (reader_sender, reader_receiver) = bounded::<Message>(0);
225    let reader = thread::spawn(move || {
226        let mut buf_read = BufReader::new(reader_stream);
227        while let Some(msg) = Message::read(&mut buf_read)? {
228            let is_exit = matches!(&msg, Message::Notification(n) if n.method == "exit");
229            if reader_sender.send(msg).is_err() {
230                break;
231            }
232            if is_exit {
233                break;
234            }
235        }
236        Ok(())
237    });
238
239    let (writer_sender, writer_receiver) = bounded::<Message>(0);
240    let (drop_sender, drop_receiver) = bounded::<Message>(0);
241    let writer = thread::spawn(move || {
242        let mut stream = stream;
243        writer_receiver.into_iter().try_for_each(|msg| {
244            let result = msg.write(&mut stream);
245            let _ = drop_sender.send(msg);
246            result
247        })
248    });
249    let dropper = thread::spawn(move || drop_receiver.into_iter().for_each(drop));
250
251    Ok((
252        Connection {
253            sender: writer_sender,
254            receiver: reader_receiver,
255        },
256        DaemonIo {
257            reader,
258            writer,
259            dropper,
260        },
261    ))
262}
263
264struct DaemonIo {
265    reader: thread::JoinHandle<io::Result<()>>,
266    writer: thread::JoinHandle<io::Result<()>>,
267    dropper: thread::JoinHandle<()>,
268}
269
270impl DaemonIo {
271    fn join(self) -> io::Result<()> {
272        match self.reader.join() {
273            Ok(r) => r?,
274            Err(err) => std::panic::panic_any(err),
275        }
276        match self.dropper.join() {
277            Ok(_) => (),
278            Err(err) => std::panic::panic_any(err),
279        }
280        match self.writer.join() {
281            Ok(r) => r,
282            Err(err) => std::panic::panic_any(err),
283        }
284    }
285}
286
287// ==============================================================================
288// Project Registry And Shared Tsserver Service
289// ==============================================================================
290
291#[derive(Clone)]
292struct ProjectRegistry {
293    inner: Arc<Mutex<ProjectRegistryState>>,
294}
295
296struct ProjectRegistryState {
297    entries: HashMap<PathBuf, ProjectEntry>,
298    max_entries: Option<usize>,
299    #[allow(dead_code)]
300    idle_ttl: Option<Duration>,
301}
302
303impl ProjectRegistry {
304    fn new(idle_ttl: Option<Duration>) -> Self {
305        let registry = Self {
306            inner: Arc::new(Mutex::new(ProjectRegistryState {
307                entries: HashMap::new(),
308                max_entries: None,
309                idle_ttl,
310            })),
311        };
312        registry.spawn_eviction_loop();
313        registry
314    }
315
316    fn status_snapshot(&self) -> Vec<Value> {
317        let seeds = {
318            let guard = self.inner.lock().expect("project registry mutex poisoned");
319            guard
320                .entries
321                .iter()
322                .map(|(root, entry)| {
323                    (
324                        root.clone(),
325                        entry.handle.label().to_string(),
326                        entry.handle.clone(),
327                        entry.last_used.load(Ordering::Relaxed),
328                    )
329                })
330                .collect::<Vec<_>>()
331        };
332
333        let mut entries = Vec::with_capacity(seeds.len());
334        for (root, label, handle, last_used) in seeds {
335            let status = handle.status().unwrap_or_else(|err| {
336                log::warn!(
337                    "failed to fetch status for project {}: {err}",
338                    root.display()
339                );
340                ProjectThreadStatus::default()
341            });
342            entries.push(json!({
343                "root": root.to_string_lossy(),
344                "label": label,
345                "session_count": status.session_count,
346                "session_ids": status.session_ids,
347                "last_used_epoch_seconds": last_used,
348                "tsserver": {
349                    "syntax_pid": status.tsserver_syntax_pid,
350                    "semantic_pid": status.tsserver_semantic_pid,
351                },
352            }));
353        }
354        entries.sort_by_key(|entry| {
355            entry
356                .get("root")
357                .and_then(|value| value.as_str())
358                .unwrap_or_default()
359                .to_string()
360        });
361        entries
362    }
363
364    fn register_session(&self, params: &InitializeParams) -> anyhow::Result<SessionInit> {
365        let workspace_root =
366            workspace_root_from_params(params).unwrap_or_else(|| std::env::current_dir().unwrap());
367        let mut config = Config::new(PluginSettings::default());
368
369        if let Some(options) = params.initialization_options.as_ref()
370            && config.apply_workspace_settings(options)
371        {
372            log::info!("applied initializationOptions to ts-bridge settings");
373        }
374
375        let handle = self.get_or_create(workspace_root.clone(), config.clone())?;
376        let registration = handle.register_session(config)?;
377        Ok(SessionInit {
378            project: handle,
379            events: registration.events,
380            config: registration.config,
381            workspace_root,
382            session_id: registration.session_id,
383        })
384    }
385
386    fn get_or_create(
387        &self,
388        workspace_root: PathBuf,
389        config: Config,
390    ) -> anyhow::Result<ProjectHandle> {
391        let normalized = normalize_root(workspace_root);
392        let mut guard = self.inner.lock().expect("project registry mutex poisoned");
393        guard.maybe_evict();
394        if let Some(entry) = guard.entries.get_mut(&normalized) {
395            entry.touch();
396            return Ok(entry.handle.clone());
397        }
398
399        let provider = Provider::new(normalized.clone());
400        let last_used = Arc::new(AtomicU64::new(current_epoch_seconds()));
401        let session_count = Arc::new(AtomicUsize::new(0));
402        let handle = ProjectHandle::spawn(
403            normalized.clone(),
404            config,
405            provider,
406            last_used.clone(),
407            session_count.clone(),
408        );
409        let entry = ProjectEntry {
410            handle: handle.clone(),
411            last_used,
412            session_count,
413        };
414        guard.entries.insert(normalized, entry);
415        Ok(handle)
416    }
417
418    fn spawn_eviction_loop(&self) {
419        let idle_ttl = {
420            let guard = self.inner.lock().expect("project registry mutex poisoned");
421            guard.idle_ttl
422        };
423        let Some(idle_ttl) = idle_ttl else {
424            return;
425        };
426        if idle_ttl.is_zero() {
427            return;
428        }
429        let registry = self.clone();
430        thread::spawn(move || registry.evict_idle_loop(idle_ttl));
431    }
432
433    fn evict_idle_loop(self, idle_ttl: Duration) {
434        let sweep_interval = idle_sweep_interval(idle_ttl);
435        loop {
436            thread::sleep(sweep_interval);
437            let mut guard = self.inner.lock().expect("project registry mutex poisoned");
438            guard.maybe_evict();
439        }
440    }
441}
442
443struct ProjectEntry {
444    handle: ProjectHandle,
445    last_used: Arc<AtomicU64>,
446    session_count: Arc<AtomicUsize>,
447}
448
449impl ProjectEntry {
450    fn touch(&self) {
451        self.last_used
452            .store(current_epoch_seconds(), Ordering::Relaxed);
453    }
454}
455
456impl ProjectRegistryState {
457    fn maybe_evict(&mut self) {
458        self.evict_idle_entries();
459        self.evict_overflow_entries();
460    }
461
462    fn evict_idle_entries(&mut self) {
463        let Some(idle_ttl) = self.idle_ttl else {
464            return;
465        };
466        if idle_ttl.is_zero() {
467            return;
468        }
469        let ttl_secs = idle_ttl.as_secs();
470        let now = current_epoch_seconds();
471        let mut expired = Vec::new();
472        for (root, entry) in self.entries.iter() {
473            if entry.session_count.load(Ordering::Relaxed) > 0 {
474                continue;
475            }
476            let last_used = entry.last_used.load(Ordering::Relaxed);
477            if now.saturating_sub(last_used) >= ttl_secs {
478                expired.push(root.clone());
479            }
480        }
481        for root in expired {
482            if let Some(entry) = self.entries.remove(&root) {
483                entry.handle.shutdown();
484            }
485        }
486    }
487
488    fn evict_overflow_entries(&mut self) {
489        let Some(max_entries) = self.max_entries else {
490            return;
491        };
492        if self.entries.len() <= max_entries {
493            return;
494        }
495
496        let mut candidates = self
497            .entries
498            .iter()
499            .map(|(root, entry)| (entry.last_used.load(Ordering::Relaxed), root.clone()))
500            .collect::<Vec<_>>();
501        candidates.sort_by_key(|(last_used, _)| *last_used);
502        for (_, root) in candidates
503            .into_iter()
504            .take(self.entries.len() - max_entries)
505        {
506            if let Some(entry) = self.entries.remove(&root) {
507                entry.handle.shutdown();
508            }
509        }
510    }
511}
512
513fn normalize_root(root: PathBuf) -> PathBuf {
514    root.canonicalize().unwrap_or(root)
515}
516
517#[derive(Clone)]
518struct ProjectHandle {
519    root: PathBuf,
520    label: String,
521    commands: Sender<ProjectCommand>,
522    last_used: Arc<AtomicU64>,
523    session_count: Arc<AtomicUsize>,
524}
525
526impl ProjectHandle {
527    fn spawn(
528        root: PathBuf,
529        config: Config,
530        provider: Provider,
531        last_used: Arc<AtomicU64>,
532        session_count: Arc<AtomicUsize>,
533    ) -> Self {
534        let label = friendly_project_name(&root);
535        let (tx, rx) = unbounded();
536        let label_clone = label.clone();
537        thread::spawn(move || project_thread(config, provider, label_clone, rx));
538        Self {
539            root,
540            label,
541            commands: tx,
542            last_used,
543            session_count,
544        }
545    }
546
547    fn register_session(&self, config: Config) -> anyhow::Result<SessionRegistration> {
548        let session_id = next_session_id();
549        let (event_tx, event_rx) = unbounded();
550        let (reply_tx, reply_rx) = bounded(0);
551        self.commands
552            .send(ProjectCommand::RegisterSession {
553                session_id,
554                sender: event_tx,
555                config,
556                reply: reply_tx,
557            })
558            .context("register session with project service")?;
559        let confirmed = reply_rx
560            .recv()
561            .context("receive project session configuration")?;
562        self.session_started();
563        Ok(SessionRegistration {
564            session_id,
565            events: event_rx,
566            config: confirmed,
567        })
568    }
569
570    fn unregister_session(&self, session_id: SessionId) {
571        let _ = self
572            .commands
573            .send(ProjectCommand::UnregisterSession { session_id });
574        self.session_ended();
575    }
576
577    fn dispatch_request(
578        &self,
579        route: Route,
580        payload: Value,
581        priority: Priority,
582    ) -> anyhow::Result<Vec<DispatchReceipt>> {
583        self.touch();
584        let (reply_tx, reply_rx) = bounded(0);
585        self.commands
586            .send(ProjectCommand::Dispatch {
587                route,
588                payload,
589                priority,
590                reply: reply_tx,
591            })
592            .context("dispatch request to project service")?;
593        reply_rx
594            .recv()
595            .context("receive project dispatch receipt")?
596            .map_err(|err| anyhow!(err))
597    }
598
599    fn update_config(&self, settings: Value) -> anyhow::Result<ConfigUpdate> {
600        self.touch();
601        let (reply_tx, reply_rx) = bounded(0);
602        self.commands
603            .send(ProjectCommand::UpdateConfig {
604                settings,
605                reply: reply_tx,
606            })
607            .context("update project configuration")?;
608        reply_rx.recv().context("receive configuration update")
609    }
610
611    fn restart(&self, kind: RestartKind) -> anyhow::Result<()> {
612        self.touch();
613        let (reply_tx, reply_rx) = bounded(0);
614        self.commands
615            .send(ProjectCommand::Restart {
616                kind,
617                reply: reply_tx,
618            })
619            .context("dispatch project restart")?;
620        reply_rx
621            .recv()
622            .context("receive project restart result")?
623            .map_err(|err| anyhow!(err))
624    }
625
626    fn shutdown(&self) {
627        let _ = self.commands.send(ProjectCommand::Shutdown);
628    }
629
630    fn status(&self) -> anyhow::Result<ProjectThreadStatus> {
631        let (reply_tx, reply_rx) = bounded(0);
632        self.commands
633            .send(ProjectCommand::Status { reply: reply_tx })
634            .context("dispatch project status request")?;
635        reply_rx.recv().context("receive project status")
636    }
637
638    fn root(&self) -> &Path {
639        &self.root
640    }
641
642    fn label(&self) -> &str {
643        &self.label
644    }
645
646    fn touch(&self) {
647        self.last_used
648            .store(current_epoch_seconds(), Ordering::Relaxed);
649    }
650
651    fn session_started(&self) {
652        self.session_count.fetch_add(1, Ordering::Relaxed);
653        self.touch();
654    }
655
656    fn session_ended(&self) {
657        let previous = self
658            .session_count
659            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
660                if value == 0 { None } else { Some(value - 1) }
661            })
662            .unwrap_or(0);
663        if previous == 1 {
664            self.touch();
665        }
666    }
667}
668
669struct SessionInit {
670    project: ProjectHandle,
671    events: Receiver<ProjectEvent>,
672    config: Config,
673    workspace_root: PathBuf,
674    session_id: SessionId,
675}
676
677struct SessionRegistration {
678    session_id: SessionId,
679    events: Receiver<ProjectEvent>,
680    config: Config,
681}
682
683struct ConfigUpdate {
684    changed: bool,
685    config: Config,
686}
687
688#[derive(Debug, Clone, Default)]
689struct ProjectThreadStatus {
690    session_count: usize,
691    session_ids: Vec<SessionId>,
692    tsserver_syntax_pid: Option<u32>,
693    tsserver_semantic_pid: Option<u32>,
694}
695
696#[derive(Debug, Clone)]
697enum ProjectEvent {
698    Server(ServerEvent),
699    Restarting { kind: RestartKind },
700    Restarted { kind: RestartKind },
701    RestartFailed { kind: RestartKind, message: String },
702    ConfigUpdated(Config),
703}
704
705#[derive(Debug, Clone, Copy)]
706enum RestartKind {
707    Syntax,
708    Semantic,
709    Both,
710}
711
712impl RestartKind {
713    fn from_str(value: &str) -> Option<Self> {
714        match value {
715            "syntax" => Some(Self::Syntax),
716            "semantic" => Some(Self::Semantic),
717            "both" => Some(Self::Both),
718            _ => None,
719        }
720    }
721
722    fn as_flags(self) -> (bool, bool) {
723        match self {
724            Self::Syntax => (true, false),
725            Self::Semantic => (false, true),
726            Self::Both => (true, true),
727        }
728    }
729
730    fn label(self) -> &'static str {
731        match self {
732            Self::Syntax => "syntax",
733            Self::Semantic => "semantic",
734            Self::Both => "both",
735        }
736    }
737}
738
739enum ProjectCommand {
740    RegisterSession {
741        session_id: SessionId,
742        sender: Sender<ProjectEvent>,
743        config: Config,
744        reply: Sender<Config>,
745    },
746    UnregisterSession {
747        session_id: SessionId,
748    },
749    Dispatch {
750        route: Route,
751        payload: Value,
752        priority: Priority,
753        reply: Sender<Result<Vec<DispatchReceipt>, ServiceError>>,
754    },
755    UpdateConfig {
756        settings: Value,
757        reply: Sender<ConfigUpdate>,
758    },
759    Restart {
760        kind: RestartKind,
761        reply: Sender<Result<(), ServiceError>>,
762    },
763    Status {
764        reply: Sender<ProjectThreadStatus>,
765    },
766    Shutdown,
767}
768
769type SessionId = u64;
770
771static SESSION_IDS: AtomicU64 = AtomicU64::new(1);
772
773fn next_session_id() -> SessionId {
774    SESSION_IDS.fetch_add(1, Ordering::Relaxed)
775}
776
777fn project_thread(config: Config, provider: Provider, label: String, rx: Receiver<ProjectCommand>) {
778    let mut service = Service::new(config.clone(), provider);
779    let mut config = config;
780    let mut sessions: HashMap<SessionId, Sender<ProjectEvent>> = HashMap::new();
781    let poll_interval = Duration::from_millis(10);
782    loop {
783        for event in service.poll_responses() {
784            broadcast_event(&mut sessions, ProjectEvent::Server(event));
785        }
786
787        let command = match rx.recv_timeout(poll_interval) {
788            Ok(command) => command,
789            Err(RecvTimeoutError::Timeout) => continue,
790            Err(RecvTimeoutError::Disconnected) => break,
791        };
792
793        if !handle_project_command(command, &mut service, &mut config, &mut sessions, &label) {
794            break;
795        }
796        while let Ok(command) = rx.try_recv() {
797            if !handle_project_command(command, &mut service, &mut config, &mut sessions, &label) {
798                return;
799            }
800        }
801    }
802}
803
804fn handle_project_command(
805    command: ProjectCommand,
806    service: &mut Service,
807    config: &mut Config,
808    sessions: &mut HashMap<SessionId, Sender<ProjectEvent>>,
809    label: &str,
810) -> bool {
811    match command {
812        ProjectCommand::RegisterSession {
813            session_id,
814            sender,
815            config: session_config,
816            reply,
817        } => {
818            if session_config != *config {
819                log::warn!(
820                    "session config mismatch for project {label}; using first session settings"
821                );
822            }
823            sessions.insert(session_id, sender);
824            let _ = reply.send(config.clone());
825            true
826        }
827        ProjectCommand::UnregisterSession { session_id } => {
828            sessions.remove(&session_id);
829            true
830        }
831        ProjectCommand::Dispatch {
832            route,
833            payload,
834            priority,
835            reply,
836        } => {
837            let result = service.dispatch_request(route, payload, priority);
838            let _ = reply.send(result);
839            true
840        }
841        ProjectCommand::UpdateConfig { settings, reply } => {
842            let changed = config.apply_workspace_settings(&settings);
843            if changed {
844                log::info!("project {label} settings updated");
845                service.update_config(config.clone());
846                broadcast_event(sessions, ProjectEvent::ConfigUpdated(config.clone()));
847            }
848            let _ = reply.send(ConfigUpdate {
849                changed,
850                config: config.clone(),
851            });
852            true
853        }
854        ProjectCommand::Restart { kind, reply } => {
855            broadcast_event(sessions, ProjectEvent::Restarting { kind });
856            let (restart_syntax, restart_semantic) = kind.as_flags();
857            let result = service.restart(restart_syntax, restart_semantic);
858            match &result {
859                Ok(_) => broadcast_event(sessions, ProjectEvent::Restarted { kind }),
860                Err(err) => broadcast_event(
861                    sessions,
862                    ProjectEvent::RestartFailed {
863                        kind,
864                        message: err.to_string(),
865                    },
866                ),
867            }
868            let _ = reply.send(result);
869            true
870        }
871        ProjectCommand::Status { reply } => {
872            let status = service.tsserver_status();
873            let mut session_ids = sessions.keys().copied().collect::<Vec<_>>();
874            session_ids.sort_unstable();
875            let _ = reply.send(ProjectThreadStatus {
876                session_count: session_ids.len(),
877                session_ids,
878                tsserver_syntax_pid: status.syntax_pid,
879                tsserver_semantic_pid: status.semantic_pid,
880            });
881            true
882        }
883        ProjectCommand::Shutdown => {
884            sessions.clear();
885            false
886        }
887    }
888}
889
890fn broadcast_event(sessions: &mut HashMap<SessionId, Sender<ProjectEvent>>, event: ProjectEvent) {
891    let mut stale = Vec::new();
892    for (session_id, sender) in sessions.iter() {
893        if sender.send(event.clone()).is_err() {
894            stale.push(*session_id);
895        }
896    }
897    for session_id in stale {
898        sessions.remove(&session_id);
899    }
900}
901
902fn advertised_capabilities(settings: &PluginSettings) -> ServerCapabilities {
903    let text_sync = TextDocumentSyncOptions {
904        open_close: Some(true),
905        change: Some(TextDocumentSyncKind::INCREMENTAL),
906        will_save: Some(false),
907        will_save_wait_until: Some(false),
908        save: Some(TextDocumentSyncSaveOptions::SaveOptions(
909            lsp_types::SaveOptions::default(),
910        )),
911    };
912    let completion_provider = CompletionOptions {
913        resolve_provider: Some(true),
914        trigger_characters: Some(TRIGGER_CHARACTERS.iter().map(|ch| ch.to_string()).collect()),
915        ..CompletionOptions::default()
916    };
917    let signature_help_provider = SignatureHelpOptions {
918        trigger_characters: Some(
919            SIG_HELP_TRIGGER_CHARACTERS
920                .iter()
921                .map(|ch| ch.to_string())
922                .collect(),
923        ),
924        retrigger_characters: Some(vec![",".into(), ")".into()]),
925        ..SignatureHelpOptions::default()
926    };
927    let code_action_provider = CodeActionProviderCapability::Options(CodeActionOptions {
928        code_action_kinds: Some(vec![
929            CodeActionKind::QUICKFIX,
930            CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
931        ]),
932        resolve_provider: Some(true),
933        work_done_progress_options: Default::default(),
934    });
935    let rename_provider = OneOf::Right(RenameOptions {
936        prepare_provider: Some(true),
937        work_done_progress_options: Default::default(),
938    });
939    let semantic_tokens_provider =
940        lsp_types::SemanticTokensServerCapabilities::SemanticTokensOptions(
941            lsp_types::SemanticTokensOptions {
942                legend: crate::protocol::text_document::semantic_tokens::legend(),
943                range: Some(true),
944                full: Some(lsp_types::SemanticTokensFullOptions::Bool(true)),
945                work_done_progress_options: Default::default(),
946            },
947        );
948    let inlay_hint_provider = if settings.enable_inlay_hints {
949        Some(OneOf::Right(InlayHintServerCapabilities::Options(
950            InlayHintOptions {
951                work_done_progress_options: Default::default(),
952                resolve_provider: None,
953            },
954        )))
955    } else {
956        None
957    };
958    let execute_command_provider = Some(ExecuteCommandOptions {
959        commands: crate::protocol::workspace::execute_command::USER_COMMANDS
960            .iter()
961            .map(|cmd| (*cmd).to_string())
962            .collect(),
963        work_done_progress_options: Default::default(),
964    });
965    ServerCapabilities {
966        position_encoding: Some(PositionEncodingKind::UTF16),
967        hover_provider: Some(HoverProviderCapability::Simple(true)),
968        definition_provider: Some(OneOf::Left(true)),
969        references_provider: Some(OneOf::Left(true)),
970        type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
971        document_symbol_provider: Some(OneOf::Left(true)),
972        workspace_symbol_provider: Some(OneOf::Left(true)),
973        completion_provider: Some(completion_provider),
974        signature_help_provider: Some(signature_help_provider),
975        code_action_provider: Some(code_action_provider),
976        rename_provider: Some(rename_provider),
977        document_formatting_provider: Some(OneOf::Left(true)),
978        semantic_tokens_provider: Some(semantic_tokens_provider),
979        inlay_hint_provider,
980        execute_command_provider,
981        text_document_sync: Some(TextDocumentSyncCapability::Options(text_sync)),
982        ..Default::default()
983    }
984}
985
986fn run_session(connection: Connection, registry: &ProjectRegistry) -> anyhow::Result<()> {
987    let (init_id, init_params) = connection
988        .initialize_start()
989        .context("waiting for initialize")?;
990    let params: InitializeParams =
991        serde_json::from_value(init_params).context("invalid initialize params")?;
992
993    let session_init = registry.register_session(&params)?;
994    let capabilities = advertised_capabilities(session_init.config.plugin());
995    let init_result = InitializeResult {
996        server_info: Some(lsp_types::ServerInfo {
997            name: "ts-bridge".to_string(),
998            version: Some(env!("CARGO_PKG_VERSION").to_string()),
999        }),
1000        capabilities,
1001    };
1002    connection
1003        .initialize_finish(init_id, serde_json::to_value(init_result)?)
1004        .context("failed to send initialize result")?;
1005
1006    let mut session = SessionState::new(connection, session_init, registry.clone());
1007    let result = session.run();
1008    session.project.unregister_session(session.session_id);
1009    result
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014    use super::*;
1015    use lsp_types::{Uri, WorkspaceFolder};
1016
1017    #[test]
1018    fn advertised_capabilities_include_inlay_hints_when_enabled() {
1019        let settings = PluginSettings::default();
1020        let caps = advertised_capabilities(&settings);
1021
1022        assert!(caps.inlay_hint_provider.is_some());
1023        assert_eq!(
1024            caps.position_encoding,
1025            Some(PositionEncodingKind::UTF16),
1026            "initialize should advertise UTF-16 positions"
1027        );
1028        match caps.text_document_sync {
1029            Some(TextDocumentSyncCapability::Options(options)) => {
1030                assert_eq!(options.change, Some(TextDocumentSyncKind::INCREMENTAL));
1031            }
1032            other => panic!("unexpected sync capability: {other:?}"),
1033        }
1034    }
1035
1036    #[test]
1037    fn advertised_capabilities_disable_inlay_hints_when_setting_is_false() {
1038        let settings = PluginSettings {
1039            enable_inlay_hints: false,
1040            ..Default::default()
1041        };
1042
1043        let caps = advertised_capabilities(&settings);
1044        assert!(
1045            caps.inlay_hint_provider.is_none(),
1046            "initialize must omit inlay hint capability when disabled"
1047        );
1048    }
1049
1050    #[test]
1051    fn tsserver_configure_args_override_inlay_hint_preferences() {
1052        let mut preferences = Map::new();
1053        preferences.insert(
1054            "includeInlayParameterNameHints".to_string(),
1055            Value::String("literals".to_string()),
1056        );
1057        preferences.insert(
1058            "quotePreference".to_string(),
1059            Value::String("auto".to_string()),
1060        );
1061
1062        let config = Config::new(PluginSettings {
1063            enable_inlay_hints: false,
1064            tsserver_preferences: preferences,
1065            ..Default::default()
1066        });
1067
1068        let args = tsserver_configure_args(&config);
1069        let preferences = args
1070            .get("preferences")
1071            .and_then(|value| value.as_object())
1072            .expect("configure args should include preferences");
1073
1074        assert_eq!(
1075            preferences
1076                .get("includeInlayParameterNameHints")
1077                .and_then(|value| value.as_str()),
1078            Some("none")
1079        );
1080        assert_eq!(
1081            preferences
1082                .get("quotePreference")
1083                .and_then(|value| value.as_str()),
1084            Some("auto")
1085        );
1086    }
1087
1088    #[test]
1089    fn tsserver_configure_args_include_format_options_when_provided() {
1090        let mut format_options = Map::new();
1091        format_options.insert("indentSize".to_string(), Value::Number(2.into()));
1092
1093        let config = Config::new(PluginSettings {
1094            tsserver_format_options: format_options,
1095            ..Default::default()
1096        });
1097
1098        let args = tsserver_configure_args(&config);
1099        let format_options = args
1100            .get("formatOptions")
1101            .and_then(|value| value.as_object())
1102            .expect("configure args should include formatOptions");
1103
1104        assert_eq!(
1105            format_options
1106                .get("indentSize")
1107                .and_then(|value| value.as_i64()),
1108            Some(2)
1109        );
1110    }
1111
1112    #[test]
1113    #[allow(deprecated)]
1114    fn workspace_root_from_params_prefers_root_path() {
1115        let params = InitializeParams {
1116            root_path: Some("/tmp/root-path".to_string()),
1117            root_uri: Some(Uri::from_str("file:///tmp/root-uri").expect("valid uri")),
1118            workspace_folders: Some(vec![WorkspaceFolder {
1119                uri: Uri::from_str("file:///tmp/workspace-root").expect("valid uri"),
1120                name: "workspace".to_string(),
1121            }]),
1122            ..Default::default()
1123        };
1124
1125        let root = workspace_root_from_params(&params);
1126
1127        assert_eq!(root, Some(PathBuf::from("/tmp/root-path")));
1128    }
1129
1130    #[test]
1131    #[allow(deprecated)]
1132    fn workspace_root_from_params_uses_root_uri_when_root_path_missing() {
1133        let params = InitializeParams {
1134            root_uri: Some(Uri::from_str("file:///tmp/root-uri").expect("valid uri")),
1135            ..Default::default()
1136        };
1137
1138        let root = workspace_root_from_params(&params);
1139
1140        assert_eq!(root, Some(PathBuf::from("/tmp/root-uri")));
1141    }
1142
1143    #[test]
1144    fn workspace_root_from_params_uses_first_workspace_folder_when_needed() {
1145        let params = InitializeParams {
1146            workspace_folders: Some(vec![WorkspaceFolder {
1147                uri: Uri::from_str("file:///tmp/workspace-root").expect("valid uri"),
1148                name: "workspace".to_string(),
1149            }]),
1150            ..Default::default()
1151        };
1152
1153        let root = workspace_root_from_params(&params);
1154
1155        assert_eq!(root, Some(PathBuf::from("/tmp/workspace-root")));
1156    }
1157
1158    #[test]
1159    fn project_registry_status_snapshot_exposes_session_and_pid_details() {
1160        let (tx, rx) = unbounded();
1161        let last_used = Arc::new(AtomicU64::new(123));
1162        let session_count = Arc::new(AtomicUsize::new(0));
1163        let root = PathBuf::from("/tmp/status-project");
1164
1165        let handle = ProjectHandle {
1166            root: root.clone(),
1167            label: "project".to_string(),
1168            commands: tx,
1169            last_used: Arc::clone(&last_used),
1170            session_count: Arc::clone(&session_count),
1171        };
1172
1173        let entry = ProjectEntry {
1174            handle,
1175            last_used,
1176            session_count,
1177        };
1178
1179        let registry = ProjectRegistry {
1180            inner: Arc::new(Mutex::new(ProjectRegistryState {
1181                entries: HashMap::from([(root.clone(), entry)]),
1182                max_entries: None,
1183                idle_ttl: None,
1184            })),
1185        };
1186
1187        thread::spawn(move || {
1188            if let Ok(command) = rx.recv() {
1189                if let ProjectCommand::Status { reply } = command {
1190                    let _ = reply.send(ProjectThreadStatus {
1191                        session_count: 2,
1192                        session_ids: vec![1, 2],
1193                        tsserver_syntax_pid: Some(100),
1194                        tsserver_semantic_pid: Some(200),
1195                    });
1196                }
1197            }
1198        });
1199
1200        let snapshot = registry.status_snapshot();
1201
1202        assert_eq!(snapshot.len(), 1);
1203        let entry = snapshot
1204            .first()
1205            .and_then(|value| value.as_object())
1206            .expect("status entry should be an object");
1207        assert_eq!(entry.get("label").and_then(|v| v.as_str()), Some("project"));
1208        assert_eq!(
1209            entry.get("root").and_then(|v| v.as_str()),
1210            Some("/tmp/status-project")
1211        );
1212        assert_eq!(entry.get("session_count").and_then(|v| v.as_u64()), Some(2));
1213        assert_eq!(
1214            entry
1215                .get("last_used_epoch_seconds")
1216                .and_then(|v| v.as_u64()),
1217            Some(123)
1218        );
1219        let tsserver = entry
1220            .get("tsserver")
1221            .and_then(|value| value.as_object())
1222            .expect("status entry should include tsserver object");
1223        assert_eq!(
1224            tsserver.get("syntax_pid").and_then(|v| v.as_u64()),
1225            Some(100)
1226        );
1227        assert_eq!(
1228            tsserver.get("semantic_pid").and_then(|v| v.as_u64()),
1229            Some(200)
1230        );
1231    }
1232}
1233
1234#[allow(deprecated)]
1235fn workspace_root_from_params(params: &InitializeParams) -> Option<PathBuf> {
1236    // Prefer the modern URIs so Neovim/VSCode multi-root setups resolve to
1237    // the correct project instead of wherever `ts-bridge` happens to run.
1238    if let Some(root_path) = &params.root_path {
1239        return Some(Path::new(root_path).to_path_buf());
1240    }
1241
1242    if let Some(root_uri) = &params.root_uri {
1243        if let Some(path) = uri_to_file_path(root_uri.as_str()) {
1244            return Some(PathBuf::from(path));
1245        }
1246    }
1247
1248    if let Some(folders) = &params.workspace_folders {
1249        for folder in folders {
1250            if let Some(path) = uri_to_file_path(folder.uri.as_str()) {
1251                return Some(PathBuf::from(path));
1252            }
1253        }
1254    }
1255
1256    None
1257}
1258
1259struct SessionState {
1260    connection: Connection,
1261    project: ProjectHandle,
1262    events: Receiver<ProjectEvent>,
1263    config: Config,
1264    workspace_root: PathBuf,
1265    session_id: SessionId,
1266    project_label: String,
1267    pending: PendingRequests,
1268    diag_state: DiagnosticsState,
1269    progress: LoadingProgress,
1270    restart_progress: RestartProgress,
1271    documents: DocumentStore,
1272    inlay_cache: InlayHintCache,
1273    tsserver_configure: TsserverConfigureState,
1274    registry: ProjectRegistry,
1275}
1276
1277impl SessionState {
1278    fn new(connection: Connection, init: SessionInit, registry: ProjectRegistry) -> Self {
1279        let project_label = init.project.label().to_string();
1280        Self {
1281            connection,
1282            project: init.project,
1283            events: init.events,
1284            config: init.config,
1285            workspace_root: init.workspace_root,
1286            session_id: init.session_id,
1287            project_label,
1288            pending: PendingRequests::default(),
1289            diag_state: DiagnosticsState::default(),
1290            progress: LoadingProgress::new(init.session_id),
1291            restart_progress: RestartProgress::new(init.session_id),
1292            documents: DocumentStore::default(),
1293            inlay_cache: InlayHintCache::default(),
1294            tsserver_configure: TsserverConfigureState::default(),
1295            registry,
1296        }
1297    }
1298
1299    fn run(&mut self) -> anyhow::Result<()> {
1300        if let Err(err) = self.progress.begin(
1301            &self.connection,
1302            "ts-bridge",
1303            &format!("Booting {}", self.project_label),
1304        ) {
1305            log::debug!("work-done progress begin failed: {err:?}");
1306        }
1307
1308        let poll_interval = Duration::from_millis(10);
1309        loop {
1310            self.drain_project_events()?;
1311
1312            match self.connection.receiver.recv_timeout(poll_interval) {
1313                Ok(message) => match message {
1314                    Message::Request(req) => {
1315                        if self.handle_request(req)? {
1316                            break;
1317                        }
1318                    }
1319                    Message::Response(resp) => {
1320                        log::debug!("ignoring stray response: {:?}", resp);
1321                    }
1322                    Message::Notification(notif) => {
1323                        if self.handle_notification(notif)? {
1324                            break;
1325                        }
1326                    }
1327                },
1328                Err(RecvTimeoutError::Timeout) => continue,
1329                Err(RecvTimeoutError::Disconnected) => break,
1330            }
1331        }
1332
1333        Ok(())
1334    }
1335
1336    fn drain_project_events(&mut self) -> anyhow::Result<()> {
1337        loop {
1338            match self.events.try_recv() {
1339                Ok(event) => self.handle_project_event(event)?,
1340                Err(TryRecvError::Empty) => break,
1341                Err(TryRecvError::Disconnected) => {
1342                    log::warn!("project event channel closed for {}", self.project_label);
1343                    break;
1344                }
1345            }
1346        }
1347        Ok(())
1348    }
1349
1350    fn handle_project_event(&mut self, event: ProjectEvent) -> anyhow::Result<()> {
1351        match event {
1352            ProjectEvent::Server(event) => self.handle_server_event(event),
1353            ProjectEvent::ConfigUpdated(config) => {
1354                self.config = config;
1355                self.tsserver_configure.invalidate();
1356                self.inlay_cache.clear();
1357                Ok(())
1358            }
1359            ProjectEvent::Restarting { kind } => self.handle_restart_start(kind),
1360            ProjectEvent::Restarted { kind } => self.handle_restart_complete(kind),
1361            ProjectEvent::RestartFailed { kind, message } => {
1362                self.handle_restart_failure(kind, &message)
1363            }
1364        }
1365    }
1366
1367    fn handle_server_event(&mut self, event: ServerEvent) -> anyhow::Result<()> {
1368        if let Some(diag_event) = protocol::diagnostics::parse_tsserver_event(&event.payload) {
1369            let stage_label = match &diag_event {
1370                DiagnosticsEvent::Report { kind, .. } => Some(stage_text(*kind)),
1371                DiagnosticsEvent::Completed { .. } => Some("finalizing diagnostics"),
1372            };
1373            self.diag_state.handle_event(event.server, diag_event);
1374            while let Some((uri, diagnostics)) = self.diag_state.take_ready() {
1375                if !self.documents.is_open(&uri) {
1376                    self.diag_state.clear_file(&uri);
1377                    continue;
1378                }
1379                publish_diagnostics(
1380                    &self.connection,
1381                    PublishDiagnosticsParams {
1382                        uri,
1383                        diagnostics,
1384                        version: None,
1385                    },
1386                )?;
1387            }
1388            if self.diag_state.has_pending() {
1389                let message = if let Some(stage) = stage_label {
1390                    format!("Analyzing {} — {stage}", self.project_label)
1391                } else {
1392                    format!("Analyzing {}", self.project_label)
1393                };
1394                if let Err(err) = self.progress.report(
1395                    &self.connection,
1396                    &message,
1397                    self.diag_state.progress_percent(),
1398                ) {
1399                    log::debug!("work-done progress report failed: {err:?}");
1400                }
1401            } else {
1402                if let Err(err) = self.progress.end(
1403                    &self.connection,
1404                    &format!("Language features ready in {}", self.project_label),
1405                ) {
1406                    log::debug!("work-done progress end failed: {err:?}");
1407                }
1408                self.diag_state.reset_if_idle();
1409            }
1410            return Ok(());
1411        }
1412
1413        if let Some(response) = self.pending.resolve(
1414            event.server,
1415            &event.payload,
1416            &mut self.inlay_cache,
1417            &self.project,
1418        )? {
1419            self.connection.sender.send(response.into())?;
1420        } else {
1421            log::trace!("tsserver {:?} -> {}", event.server, event.payload);
1422        }
1423        Ok(())
1424    }
1425
1426    fn handle_notification(&mut self, notif: ServerNotification) -> anyhow::Result<bool> {
1427        if notif.method == "exit" {
1428            return Ok(true);
1429        }
1430        if notif.method == "ts-bridge/control" {
1431            self.handle_control_notification(notif.params)?;
1432            return Ok(false);
1433        }
1434        if notif.method == DidOpenTextDocument::METHOD {
1435            let params: crate::types::DidOpenTextDocumentParams =
1436                serde_json::from_value(notif.params)?;
1437            if let Ok(uri) = lsp_types::Uri::from_str(&params.text_document.uri) {
1438                self.documents.open(
1439                    &uri,
1440                    &params.text_document.text,
1441                    Some(params.text_document.version),
1442                    params.text_document.language_id.clone(),
1443                );
1444                self.inlay_cache.invalidate(&uri);
1445            }
1446            let file_for_diagnostics = uri_to_file_path(params.text_document.uri.as_str())
1447                .unwrap_or_else(|| params.text_document.uri.to_string());
1448            let spec =
1449                crate::protocol::text_document::did_open::handle(params, &self.workspace_root);
1450            if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) {
1451                log::warn!("failed to configure tsserver: {err}");
1452            }
1453            if let Err(err) = self
1454                .project
1455                .dispatch_request(spec.route, spec.payload, spec.priority)
1456            {
1457                log::warn!("failed to dispatch didOpen: {err}");
1458            }
1459            self.request_file_diagnostics(&file_for_diagnostics);
1460            if let Err(err) = self.progress.report(
1461                &self.connection,
1462                &format!("Analyzing {} — scheduling diagnostics", self.project_label),
1463                self.diag_state.progress_percent(),
1464            ) {
1465                log::debug!("work-done progress report failed: {err:?}");
1466            }
1467            return Ok(false);
1468        }
1469        if notif.method == DidChangeTextDocument::METHOD {
1470            let params: crate::types::DidChangeTextDocumentParams =
1471                serde_json::from_value(notif.params)?;
1472            if let Ok(uri) = lsp_types::Uri::from_str(&params.text_document.uri) {
1473                self.documents.apply_changes(
1474                    &uri,
1475                    &params.content_changes,
1476                    params.text_document.version,
1477                );
1478                self.inlay_cache.invalidate(&uri);
1479            }
1480            let file_for_diagnostics = uri_to_file_path(params.text_document.uri.as_str())
1481                .unwrap_or_else(|| params.text_document.uri.to_string());
1482            let spec =
1483                crate::protocol::text_document::did_change::handle(params, &self.workspace_root);
1484            if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) {
1485                log::warn!("failed to configure tsserver: {err}");
1486            }
1487            if let Err(err) = self
1488                .project
1489                .dispatch_request(spec.route, spec.payload, spec.priority)
1490            {
1491                log::warn!("failed to dispatch didChange: {err}");
1492            }
1493            self.request_file_diagnostics(&file_for_diagnostics);
1494            if let Err(err) = self.progress.report(
1495                &self.connection,
1496                &format!("Analyzing {} — scheduling diagnostics", self.project_label),
1497                self.diag_state.progress_percent(),
1498            ) {
1499                log::debug!("work-done progress report failed: {err:?}");
1500            }
1501            return Ok(false);
1502        }
1503        if notif.method == DidCloseTextDocument::METHOD {
1504            let params: crate::types::DidCloseTextDocumentParams =
1505                serde_json::from_value(notif.params)?;
1506            let uri = params.text_document.uri.clone();
1507            if let Ok(parsed) = lsp_types::Uri::from_str(&uri) {
1508                self.documents.close(&parsed);
1509                self.inlay_cache.invalidate(&parsed);
1510                self.diag_state.clear_file(&parsed);
1511            }
1512            let spec =
1513                crate::protocol::text_document::did_close::handle(params, &self.workspace_root);
1514            if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) {
1515                log::warn!("failed to configure tsserver: {err}");
1516            }
1517            if let Err(err) = self
1518                .project
1519                .dispatch_request(spec.route, spec.payload, spec.priority)
1520            {
1521                log::warn!("failed to dispatch didClose: {err}");
1522            }
1523            clear_client_diagnostics(&self.connection, uri)?;
1524            return Ok(false);
1525        }
1526        if notif.method == DidChangeConfiguration::METHOD {
1527            let params: lsp_types::DidChangeConfigurationParams =
1528                serde_json::from_value(notif.params)?;
1529            let update = self.project.update_config(params.settings)?;
1530            self.config = update.config;
1531            if update.changed {
1532                log::info!("workspace settings reloaded from didChangeConfiguration");
1533                self.tsserver_configure.invalidate();
1534                // TODO: restart auxiliary tsserver processes when toggles require it.
1535            }
1536            return Ok(false);
1537        }
1538        if let Some(spec) = protocol::route_notification(&notif.method, notif.params.clone()) {
1539            if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) {
1540                log::warn!("failed to configure tsserver: {err}");
1541            }
1542            if let Err(err) = self
1543                .project
1544                .dispatch_request(spec.route, spec.payload, spec.priority)
1545            {
1546                log::warn!("failed to dispatch notification {}: {err}", notif.method);
1547            }
1548        } else {
1549            log::debug!("notification {} ignored", notif.method);
1550        }
1551        Ok(false)
1552    }
1553
1554    fn handle_control_notification(&mut self, params: Value) -> anyhow::Result<()> {
1555        let Some(action) = params.get("action").and_then(|value| value.as_str()) else {
1556            log::warn!("control notification missing action");
1557            return Ok(());
1558        };
1559        if action != "restart" {
1560            log::warn!("control notification action {action} ignored");
1561            return Ok(());
1562        }
1563
1564        let restart = match parse_restart_request(Some(&params)) {
1565            Ok(restart) => restart,
1566            Err(err) => {
1567                log::warn!("control restart params invalid: {err}");
1568                return Ok(());
1569            }
1570        };
1571        if let Some(root_uri) = &restart.root_uri {
1572            if !self.matches_root_uri(root_uri) {
1573                log::warn!(
1574                    "restart request ignored for non-matching root {}",
1575                    root_uri.as_str()
1576                );
1577                return Ok(());
1578            }
1579        }
1580        if let Err(err) = self.project.restart(restart.kind) {
1581            log::warn!("control restart failed: {err}");
1582        }
1583        Ok(())
1584    }
1585
1586    fn handle_request(&mut self, req: Request) -> anyhow::Result<bool> {
1587        let lsp_server::Request { id, method, params } = req;
1588
1589        if method == "shutdown" {
1590            let response = Response::new_ok(id, Value::Null);
1591            self.connection.sender.send(response.into())?;
1592            return Ok(true);
1593        }
1594
1595        if method == "initialize" {
1596            // Already handled via initialize_start, but the client might resend; respond with error.
1597            let response = Response::new_err(
1598                id,
1599                ErrorCode::InvalidRequest as i32,
1600                "initialize already completed".to_string(),
1601            );
1602            self.connection.sender.send(response.into())?;
1603            return Ok(false);
1604        }
1605
1606        if method == "ts-bridge/status" {
1607            let projects = self.registry.status_snapshot();
1608            let response = Response::new_ok(id, json!({ "projects": projects }));
1609            self.connection.sender.send(response.into())?;
1610            return Ok(false);
1611        }
1612
1613        if method == InlayHintRefreshRequest::METHOD {
1614            self.inlay_cache.clear();
1615            let response = Response::new_ok(id, Value::Null);
1616            self.connection.sender.send(response.into())?;
1617            return Ok(false);
1618        }
1619
1620        if method == lsp_types::request::ExecuteCommand::METHOD {
1621            let command_params: lsp_types::ExecuteCommandParams =
1622                serde_json::from_value(params.clone()).context("invalid execute command params")?;
1623            if command_params.command == "TSBRestartProject" {
1624                self.handle_restart_command(id, command_params)?;
1625                return Ok(false);
1626            }
1627        }
1628
1629        let params_value = params;
1630        let spec: Option<protocol::RequestSpec>;
1631        let mut postprocess = None;
1632
1633        if method == InlayHintRequest::METHOD {
1634            let enabled = self.config.plugin().enable_inlay_hints;
1635            if !enabled {
1636                let response = Response::new_ok(id, Value::Array(Vec::new()));
1637                self.connection.sender.send(response.into())?;
1638                return Ok(false);
1639            }
1640            let hint_params: lsp_types::InlayHintParams =
1641                serde_json::from_value(params_value.clone())
1642                    .context("invalid inlay hint params")?;
1643            if let Some(cached) = self.inlay_cache.lookup(&hint_params) {
1644                let response = Response::new_ok(id, serde_json::to_value(cached)?);
1645                self.connection.sender.send(response.into())?;
1646                return Ok(false);
1647            }
1648            let span = self
1649                .documents
1650                .span_for_range(&hint_params.text_document.uri, &hint_params.range)
1651                .unwrap_or_else(|| {
1652                    log::warn!(
1653                        "missing document snapshot for {}; requesting wide span",
1654                        hint_params.text_document.uri.as_str()
1655                    );
1656                    TextSpan::covering_length(DEFAULT_INLAY_HINT_SPAN)
1657                });
1658            postprocess = Some(PostProcess::inlay_hint(&hint_params));
1659            spec = Some(crate::protocol::text_document::inlay_hint::handle(
1660                hint_params,
1661                span,
1662            ));
1663        } else {
1664            spec = protocol::route_request(&method, params_value);
1665        }
1666
1667        if let Some(spec) = spec {
1668            if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) {
1669                let response = Response::new_err(
1670                    id,
1671                    ErrorCode::InternalError as i32,
1672                    format!("failed to configure tsserver: {err}"),
1673                );
1674                self.connection.sender.send(response.into())?;
1675                return Ok(false);
1676            }
1677            match self
1678                .project
1679                .dispatch_request(spec.route, spec.payload, spec.priority)
1680            {
1681                Ok(receipts) => {
1682                    if let Some(adapter) = spec.on_response {
1683                        if receipts.is_empty() {
1684                            let response = Response::new_err(
1685                                id,
1686                                ErrorCode::InternalError as i32,
1687                                "tsserver route produced no requests".to_string(),
1688                            );
1689                            self.connection.sender.send(response.into())?;
1690                        } else {
1691                            self.pending.track(
1692                                &receipts,
1693                                id,
1694                                adapter,
1695                                spec.response_context,
1696                                postprocess.clone(),
1697                            );
1698                        }
1699                    } else {
1700                        let response = Response::new_err(
1701                            id,
1702                            ErrorCode::InternalError as i32,
1703                            "handler missing response adapter".to_string(),
1704                        );
1705                        self.connection.sender.send(response.into())?;
1706                    }
1707                }
1708                Err(err) => {
1709                    let response = Response::new_err(
1710                        id,
1711                        ErrorCode::InternalError as i32,
1712                        format!("failed to dispatch tsserver request: {err}"),
1713                    );
1714                    self.connection.sender.send(response.into())?;
1715                }
1716            }
1717            return Ok(false);
1718        }
1719
1720        let response = Response::new_err(
1721            id,
1722            ErrorCode::MethodNotFound as i32,
1723            format!("method {method} is not implemented yet"),
1724        );
1725        self.connection.sender.send(response.into())?;
1726
1727        Ok(false)
1728    }
1729
1730    fn handle_restart_command(
1731        &mut self,
1732        id: RequestId,
1733        params: lsp_types::ExecuteCommandParams,
1734    ) -> anyhow::Result<()> {
1735        let restart = match parse_restart_request(params.arguments.first()) {
1736            Ok(restart) => restart,
1737            Err(err) => {
1738                let response = Response::new_err(
1739                    id,
1740                    ErrorCode::InvalidParams as i32,
1741                    format!("invalid restart command arguments: {err}"),
1742                );
1743                self.connection.sender.send(response.into())?;
1744                return Ok(());
1745            }
1746        };
1747        if let Some(root_uri) = &restart.root_uri {
1748            if !self.matches_root_uri(root_uri) {
1749                let response = Response::new_err(
1750                    id,
1751                    ErrorCode::InvalidParams as i32,
1752                    format!("rootUri {} does not match this session", root_uri.as_str()),
1753                );
1754                self.connection.sender.send(response.into())?;
1755                return Ok(());
1756            }
1757        }
1758
1759        match self.project.restart(restart.kind) {
1760            Ok(()) => {
1761                let response = Response::new_ok(id, Value::Null);
1762                self.connection.sender.send(response.into())?;
1763            }
1764            Err(err) => {
1765                let response = Response::new_err(
1766                    id,
1767                    ErrorCode::InternalError as i32,
1768                    format!("failed to restart project: {err}"),
1769                );
1770                self.connection.sender.send(response.into())?;
1771            }
1772        }
1773
1774        Ok(())
1775    }
1776
1777    fn handle_restart_start(&mut self, kind: RestartKind) -> anyhow::Result<()> {
1778        let responses = self
1779            .pending
1780            .fail_all("tsserver restart canceled outstanding requests");
1781        for response in responses {
1782            self.connection.sender.send(response.into())?;
1783        }
1784
1785        self.diag_state.clear();
1786        self.inlay_cache.clear();
1787        self.tsserver_configure.invalidate();
1788        if let Err(err) =
1789            self.restart_progress
1790                .begin(&self.connection, "Restarting TypeScript server", kind)
1791        {
1792            log::debug!("restart progress begin failed: {err:?}");
1793        }
1794        Ok(())
1795    }
1796
1797    fn handle_restart_complete(&mut self, kind: RestartKind) -> anyhow::Result<()> {
1798        self.reopen_documents()?;
1799        if let Err(err) =
1800            self.restart_progress
1801                .end(&self.connection, "TypeScript server restarted", kind)
1802        {
1803            log::debug!("restart progress end failed: {err:?}");
1804        }
1805        Ok(())
1806    }
1807
1808    fn handle_restart_failure(&mut self, kind: RestartKind, message: &str) -> anyhow::Result<()> {
1809        if let Err(err) =
1810            self.restart_progress
1811                .end(&self.connection, "TypeScript server restart failed", kind)
1812        {
1813            log::debug!("restart progress end failed: {err:?}");
1814        }
1815        show_message(
1816            &self.connection,
1817            &format!("ts-bridge restart failed: {message}"),
1818            lsp_types::MessageType::ERROR,
1819        )?;
1820        Ok(())
1821    }
1822
1823    fn request_file_diagnostics(&mut self, file: &str) {
1824        let spec = protocol::diagnostics::request_for_file(file);
1825        if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) {
1826            log::warn!("failed to configure tsserver: {err}");
1827        }
1828        match self
1829            .project
1830            .dispatch_request(spec.route, spec.payload, spec.priority)
1831        {
1832            Ok(receipts) => {
1833                for receipt in receipts {
1834                    self.diag_state
1835                        .register_pending(receipt.server, receipt.seq);
1836                }
1837            }
1838            Err(err) => {
1839                log::warn!("failed to dispatch geterr for {}: {err}", file);
1840            }
1841        }
1842    }
1843
1844    fn reopen_documents(&mut self) -> anyhow::Result<()> {
1845        let open_documents = self.documents.open_documents();
1846        for snapshot in open_documents {
1847            self.reopen_document(snapshot)?;
1848        }
1849        Ok(())
1850    }
1851
1852    fn reopen_document(&mut self, snapshot: OpenDocumentSnapshot) -> anyhow::Result<()> {
1853        let params = crate::types::DidOpenTextDocumentParams {
1854            text_document: crate::types::TextDocumentItem {
1855                uri: snapshot.uri.clone(),
1856                language_id: snapshot.language_id,
1857                version: snapshot.version.unwrap_or(0),
1858                text: snapshot.text,
1859            },
1860        };
1861        let spec = crate::protocol::text_document::did_open::handle(params, &self.workspace_root);
1862        if let Err(err) = self.tsserver_configure.ensure(&self.config, &self.project) {
1863            log::warn!("failed to configure tsserver: {err}");
1864        }
1865        if let Err(err) = self
1866            .project
1867            .dispatch_request(spec.route, spec.payload, spec.priority)
1868        {
1869            log::warn!("failed to dispatch reopened didOpen: {err}");
1870            return Ok(());
1871        }
1872        let file_for_diagnostics = uri_to_file_path(snapshot.uri.as_str()).unwrap_or(snapshot.uri);
1873        self.request_file_diagnostics(&file_for_diagnostics);
1874        Ok(())
1875    }
1876
1877    fn matches_root_uri(&self, root_uri: &lsp_types::Uri) -> bool {
1878        let Some(path) = uri_to_file_path(root_uri.as_str()) else {
1879            return false;
1880        };
1881        normalize_root(PathBuf::from(path)) == normalize_root(self.project.root().to_path_buf())
1882    }
1883}
1884
1885struct RestartRequest {
1886    kind: RestartKind,
1887    root_uri: Option<lsp_types::Uri>,
1888}
1889
1890fn parse_restart_request(value: Option<&Value>) -> anyhow::Result<RestartRequest> {
1891    let mut root_uri = None;
1892    let mut kind = RestartKind::Both;
1893
1894    if let Some(value) = value {
1895        let obj = value
1896            .as_object()
1897            .ok_or_else(|| anyhow!("restart params must be an object"))?;
1898
1899        if let Some(root_str) = obj.get("rootUri").and_then(|value| value.as_str()) {
1900            root_uri = Some(
1901                lsp_types::Uri::from_str(root_str)
1902                    .context("restart params rootUri must be a URI")?,
1903            );
1904        }
1905
1906        if let Some(kind_str) = obj.get("kind").and_then(|value| value.as_str()) {
1907            kind = RestartKind::from_str(kind_str)
1908                .ok_or_else(|| anyhow!("invalid restart kind {kind_str}"))?;
1909        }
1910    }
1911
1912    Ok(RestartRequest { kind, root_uri })
1913}
1914
1915fn show_message(
1916    connection: &Connection,
1917    message: &str,
1918    kind: lsp_types::MessageType,
1919) -> anyhow::Result<()> {
1920    let params = lsp_types::ShowMessageParams {
1921        typ: kind,
1922        message: message.to_string(),
1923    };
1924    let notif = ServerNotification::new(
1925        lsp_types::notification::ShowMessage::METHOD.to_string(),
1926        serde_json::to_value(params)?,
1927    );
1928    connection.sender.send(Message::Notification(notif))?;
1929    Ok(())
1930}
1931
1932fn clear_client_diagnostics(connection: &Connection, uri_str: String) -> anyhow::Result<()> {
1933    let uri =
1934        lsp_types::Uri::from_str(&uri_str).context("invalid URI while clearing diagnostics")?;
1935    publish_diagnostics(
1936        connection,
1937        PublishDiagnosticsParams {
1938            uri,
1939            diagnostics: Vec::new(),
1940            version: None,
1941        },
1942    )
1943}
1944
1945fn publish_diagnostics(
1946    connection: &Connection,
1947    params: PublishDiagnosticsParams,
1948) -> anyhow::Result<()> {
1949    let notif = ServerNotification::new(
1950        PublishDiagnostics::METHOD.to_string(),
1951        serde_json::to_value(params)?,
1952    );
1953    connection.sender.send(Message::Notification(notif))?;
1954    Ok(())
1955}
1956
1957#[derive(Default)]
1958struct PendingRequests {
1959    entries: HashMap<PendingKey, PendingEntry>,
1960}
1961
1962impl PendingRequests {
1963    fn track(
1964        &mut self,
1965        receipts: &[DispatchReceipt],
1966        id: RequestId,
1967        adapter: ResponseAdapter,
1968        context: Option<Value>,
1969        postprocess: Option<PostProcess>,
1970    ) {
1971        for receipt in receipts {
1972            self.entries.insert(
1973                PendingKey {
1974                    server: receipt.server,
1975                    seq: receipt.seq,
1976                },
1977                PendingEntry {
1978                    id: id.clone(),
1979                    adapter,
1980                    context: context.clone(),
1981                    postprocess: postprocess.clone(),
1982                },
1983            );
1984        }
1985    }
1986
1987    fn resolve(
1988        &mut self,
1989        server: ServerKind,
1990        payload: &Value,
1991        inlay_cache: &mut InlayHintCache,
1992        project: &ProjectHandle,
1993    ) -> anyhow::Result<Option<Response>> {
1994        if payload
1995            .get("type")
1996            .and_then(|kind| kind.as_str())
1997            .map(|kind| kind != "response")
1998            .unwrap_or(true)
1999        {
2000            return Ok(None);
2001        }
2002
2003        let request_seq = match payload.get("request_seq").and_then(|seq| seq.as_u64()) {
2004            Some(seq) => seq,
2005            None => return Ok(None),
2006        };
2007
2008        let entry = match self.entries.remove(&PendingKey {
2009            server,
2010            seq: request_seq,
2011        }) {
2012            Some(entry) => entry,
2013            None => return Ok(None),
2014        };
2015
2016        let success = payload
2017            .get("success")
2018            .and_then(|value| value.as_bool())
2019            .unwrap_or(false);
2020
2021        if success {
2022            match (entry.adapter)(payload, entry.context.as_ref()) {
2023                Ok(AdapterResult::Ready(result)) => {
2024                    if let Some(postprocess) = entry.postprocess {
2025                        postprocess.apply(&result, inlay_cache)?;
2026                    }
2027                    Ok(Some(Response::new_ok(entry.id, result)))
2028                }
2029                Ok(AdapterResult::Continue(next_spec)) => {
2030                    let request_id = entry.id;
2031                    let postprocess = entry.postprocess;
2032                    let Some(adapter) = next_spec.on_response else {
2033                        return Ok(Some(Response::new_err(
2034                            request_id,
2035                            ErrorCode::InternalError as i32,
2036                            "handler missing response adapter".to_string(),
2037                        )));
2038                    };
2039                    match project.dispatch_request(
2040                        next_spec.route,
2041                        next_spec.payload,
2042                        next_spec.priority,
2043                    ) {
2044                        Ok(receipts) => {
2045                            if receipts.is_empty() {
2046                                Ok(Some(Response::new_err(
2047                                    request_id,
2048                                    ErrorCode::InternalError as i32,
2049                                    "tsserver route produced no requests".to_string(),
2050                                )))
2051                            } else {
2052                                self.track(
2053                                    &receipts,
2054                                    request_id,
2055                                    adapter,
2056                                    next_spec.response_context,
2057                                    postprocess,
2058                                );
2059                                Ok(None)
2060                            }
2061                        }
2062                        Err(err) => Ok(Some(Response::new_err(
2063                            request_id,
2064                            ErrorCode::InternalError as i32,
2065                            format!("failed to dispatch tsserver request: {err}"),
2066                        ))),
2067                    }
2068                }
2069                Err(err) => Ok(Some(Response::new_err(
2070                    entry.id,
2071                    ErrorCode::InternalError as i32,
2072                    format!("failed to adapt tsserver response: {err}"),
2073                ))),
2074            }
2075        } else {
2076            let message = payload
2077                .get("message")
2078                .and_then(|m| m.as_str())
2079                .unwrap_or("tsserver request failed");
2080            Ok(Some(Response::new_err(
2081                entry.id,
2082                ErrorCode::InternalError as i32,
2083                message.to_string(),
2084            )))
2085        }
2086    }
2087
2088    fn fail_all(&mut self, message: &str) -> Vec<Response> {
2089        let mut responses = Vec::new();
2090        let mut seen = HashSet::new();
2091        for entry in self.entries.values() {
2092            if seen.insert(entry.id.clone()) {
2093                responses.push(Response::new_err(
2094                    entry.id.clone(),
2095                    ErrorCode::InternalError as i32,
2096                    message.to_string(),
2097                ));
2098            }
2099        }
2100        self.entries.clear();
2101        responses
2102    }
2103}
2104
2105#[derive(Debug, Hash, PartialEq, Eq)]
2106struct PendingKey {
2107    server: ServerKind,
2108    seq: u64,
2109}
2110
2111struct PendingEntry {
2112    id: RequestId,
2113    adapter: ResponseAdapter,
2114    context: Option<Value>,
2115    postprocess: Option<PostProcess>,
2116}
2117
2118#[derive(Clone)]
2119enum PostProcess {
2120    InlayHints { key: HintCacheKey },
2121}
2122
2123impl PostProcess {
2124    fn inlay_hint(params: &lsp_types::InlayHintParams) -> Self {
2125        Self::InlayHints {
2126            key: HintCacheKey::new(&params.text_document.uri, &params.range),
2127        }
2128    }
2129
2130    fn apply(self, value: &Value, cache: &mut InlayHintCache) -> anyhow::Result<()> {
2131        match self {
2132            PostProcess::InlayHints { key } => {
2133                let hints: Vec<lsp_types::InlayHint> = serde_json::from_value(value.clone())
2134                    .context("failed to decode inlay hint response payload")?;
2135                cache.store(key, hints);
2136            }
2137        }
2138        Ok(())
2139    }
2140}
2141
2142#[derive(Default)]
2143struct InlayHintCache {
2144    entries: HashMap<HintCacheKey, Vec<lsp_types::InlayHint>>,
2145}
2146
2147#[derive(Default)]
2148struct TsserverConfigureState {
2149    last_args: Option<Map<String, Value>>,
2150}
2151
2152impl TsserverConfigureState {
2153    fn ensure(&mut self, config: &Config, project: &ProjectHandle) -> anyhow::Result<()> {
2154        let args = tsserver_configure_args(config);
2155        if self.last_args.as_ref() == Some(&args) {
2156            return Ok(());
2157        }
2158        let request = json!({
2159            "command": "configure",
2160            "arguments": args,
2161        });
2162        let _ = project
2163            .dispatch_request(Route::Both, request, Priority::Const)
2164            .context("failed to dispatch tsserver configure request")?;
2165        self.last_args = Some(args);
2166        Ok(())
2167    }
2168
2169    fn invalidate(&mut self) {
2170        self.last_args = None;
2171    }
2172}
2173
2174fn tsserver_configure_args(config: &Config) -> Map<String, Value> {
2175    let mut args = Map::new();
2176
2177    // Merge user preferences with the inlay hint gate so `enable_inlay_hints`
2178    // always wins for inlay-specific keys.
2179    let mut preferences = config.plugin().tsserver_preferences.clone();
2180    let inlay_preferences =
2181        crate::protocol::text_document::inlay_hint::preferences(config.plugin().enable_inlay_hints);
2182    if let Some(map) = inlay_preferences.as_object() {
2183        for (key, value) in map {
2184            preferences.insert(key.clone(), value.clone());
2185        }
2186    }
2187    args.insert("preferences".to_string(), Value::Object(preferences));
2188
2189    if !config.plugin().tsserver_format_options.is_empty() {
2190        args.insert(
2191            "formatOptions".to_string(),
2192            Value::Object(config.plugin().tsserver_format_options.clone()),
2193        );
2194    }
2195
2196    args
2197}
2198
2199impl InlayHintCache {
2200    fn lookup(&self, params: &lsp_types::InlayHintParams) -> Option<Vec<lsp_types::InlayHint>> {
2201        let key = HintCacheKey::new(&params.text_document.uri, &params.range);
2202        self.entries.get(&key).cloned()
2203    }
2204
2205    fn store(&mut self, key: HintCacheKey, hints: Vec<lsp_types::InlayHint>) {
2206        self.entries.insert(key, hints);
2207    }
2208
2209    fn invalidate(&mut self, uri: &lsp_types::Uri) {
2210        let needle = uri.to_string();
2211        self.entries.retain(|key, _| key.uri != needle);
2212    }
2213
2214    fn clear(&mut self) {
2215        self.entries.clear();
2216    }
2217}
2218
2219#[derive(Hash, PartialEq, Eq, Clone)]
2220struct HintCacheKey {
2221    uri: String,
2222    range: RangeFingerprint,
2223}
2224
2225impl HintCacheKey {
2226    fn new(uri: &lsp_types::Uri, range: &lsp_types::Range) -> Self {
2227        Self {
2228            uri: uri.to_string(),
2229            range: RangeFingerprint::from_range(range),
2230        }
2231    }
2232}
2233
2234#[derive(Hash, PartialEq, Eq, Clone)]
2235struct RangeFingerprint {
2236    start_line: u32,
2237    start_character: u32,
2238    end_line: u32,
2239    end_character: u32,
2240}
2241
2242impl RangeFingerprint {
2243    fn from_range(range: &lsp_types::Range) -> Self {
2244        Self {
2245            start_line: range.start.line,
2246            start_character: range.start.character,
2247            end_line: range.end.line,
2248            end_character: range.end.character,
2249        }
2250    }
2251}
2252
2253#[derive(Default)]
2254struct DiagnosticsState {
2255    pending: HashMap<(ServerKind, u64), PendingDiagnosticsEntry>,
2256    order: HashMap<ServerKind, VecDeque<u64>>,
2257    latest: HashMap<lsp_types::Uri, FileDiagnostics>,
2258    ready: VecDeque<(lsp_types::Uri, Vec<lsp_types::Diagnostic>)>,
2259    workload: Workload,
2260}
2261
2262impl DiagnosticsState {
2263    fn register_pending(&mut self, server: ServerKind, seq: u64) {
2264        self.order.entry(server).or_default().push_back(seq);
2265        let entry = PendingDiagnosticsEntry::new(server);
2266        self.workload.add_expected(entry.progress.expected_count());
2267        self.pending.insert((server, seq), entry);
2268    }
2269
2270    fn handle_event(&mut self, server: ServerKind, event: DiagnosticsEvent) {
2271        match event {
2272            DiagnosticsEvent::Report {
2273                uri,
2274                diagnostics,
2275                request_seq,
2276                kind,
2277            } => {
2278                let key = request_seq.map(|seq| (server, seq)).or_else(|| {
2279                    self.order
2280                        .get(&server)
2281                        .and_then(|queue| queue.front().copied())
2282                        .map(|seq| (server, seq))
2283                });
2284                if let Some(key) = key {
2285                    if let Some(entry) = self.pending.get_mut(&key) {
2286                        entry
2287                            .files
2288                            .entry(uri.clone())
2289                            .or_insert_with(FileDiagnostics::default)
2290                            .update_kind(kind, diagnostics);
2291                        if entry.progress.mark(kind) {
2292                            self.workload.add_completed(1);
2293                        }
2294                        return;
2295                    }
2296                }
2297                let mut latest = self.latest.remove(&uri).unwrap_or_default();
2298                latest.update_kind(kind, diagnostics);
2299                let combined = latest.collect();
2300                if !combined.is_empty() {
2301                    self.latest.insert(uri.clone(), latest);
2302                }
2303                self.ready.push_back((uri, combined));
2304            }
2305            DiagnosticsEvent::Completed { request_seq } => {
2306                let key = (server, request_seq);
2307                if let Some(mut entry) = self.pending.remove(&key) {
2308                    if let Some(queue) = self.order.get_mut(&server) {
2309                        if let Some(pos) = queue.iter().position(|seq| *seq == request_seq) {
2310                            queue.remove(pos);
2311                        }
2312                    }
2313                    for (uri, diags) in entry.files.into_iter() {
2314                        let combined = diags.collect();
2315                        if combined.is_empty() {
2316                            self.latest.remove(&uri);
2317                        } else {
2318                            self.latest.insert(uri.clone(), diags);
2319                        }
2320                        self.ready.push_back((uri, combined));
2321                    }
2322                    let forced = entry.progress.finish_outstanding();
2323                    if forced > 0 {
2324                        self.workload.add_completed(forced);
2325                    }
2326                }
2327            }
2328        }
2329    }
2330
2331    fn take_ready(&mut self) -> Option<(lsp_types::Uri, Vec<lsp_types::Diagnostic>)> {
2332        self.ready.pop_front()
2333    }
2334
2335    fn progress_percent(&self) -> Option<u32> {
2336        if self.workload.expected == 0 {
2337            None
2338        } else {
2339            Some(
2340                (self.workload.completed.saturating_mul(100) / self.workload.expected)
2341                    .clamp(0, 100),
2342            )
2343        }
2344    }
2345
2346    fn has_pending(&self) -> bool {
2347        !self.pending.is_empty()
2348    }
2349
2350    fn reset_if_idle(&mut self) {
2351        if self.pending.is_empty() {
2352            self.workload.reset();
2353        }
2354    }
2355
2356    fn clear(&mut self) {
2357        self.pending.clear();
2358        self.order.clear();
2359        self.latest.clear();
2360        self.ready.clear();
2361        self.workload.reset();
2362    }
2363
2364    fn clear_file(&mut self, uri: &lsp_types::Uri) {
2365        self.latest.remove(uri);
2366        self.ready.retain(|(ready_uri, _)| ready_uri != uri);
2367        for entry in self.pending.values_mut() {
2368            entry.files.remove(uri);
2369        }
2370    }
2371}
2372
2373struct PendingDiagnosticsEntry {
2374    files: HashMap<lsp_types::Uri, FileDiagnostics>,
2375    progress: StepProgress,
2376}
2377
2378impl PendingDiagnosticsEntry {
2379    fn new(server: ServerKind) -> Self {
2380        Self {
2381            files: HashMap::new(),
2382            progress: StepProgress::for_server(server),
2383        }
2384    }
2385}
2386
2387#[derive(Clone, Default)]
2388struct FileDiagnostics {
2389    syntax: Vec<lsp_types::Diagnostic>,
2390    semantic: Vec<lsp_types::Diagnostic>,
2391    suggestion: Vec<lsp_types::Diagnostic>,
2392}
2393
2394#[derive(Clone, Copy)]
2395struct StepProgress {
2396    syntax: StepState,
2397    semantic: StepState,
2398    suggestion: StepState,
2399}
2400
2401impl StepProgress {
2402    fn for_server(server: ServerKind) -> Self {
2403        match server {
2404            ServerKind::Syntax => Self {
2405                syntax: StepState::expected(true),
2406                semantic: StepState::expected(false),
2407                suggestion: StepState::expected(true),
2408            },
2409            ServerKind::Semantic => Self {
2410                syntax: StepState::expected(false),
2411                semantic: StepState::expected(true),
2412                suggestion: StepState::expected(false),
2413            },
2414        }
2415    }
2416
2417    fn expected_count(&self) -> u32 {
2418        self.syntax.expected_count()
2419            + self.semantic.expected_count()
2420            + self.suggestion.expected_count()
2421    }
2422
2423    fn mark(&mut self, kind: DiagnosticsKind) -> bool {
2424        match kind {
2425            DiagnosticsKind::Syntax => self.syntax.mark_done(),
2426            DiagnosticsKind::Semantic => self.semantic.mark_done(),
2427            DiagnosticsKind::Suggestion => self.suggestion.mark_done(),
2428        }
2429    }
2430
2431    fn finish_outstanding(&mut self) -> u32 {
2432        let mut added = 0;
2433        if self.syntax.finish() {
2434            added += 1;
2435        }
2436        if self.semantic.finish() {
2437            added += 1;
2438        }
2439        if self.suggestion.finish() {
2440            added += 1;
2441        }
2442        added
2443    }
2444}
2445
2446#[derive(Clone, Copy)]
2447struct StepState {
2448    expected: bool,
2449    done: bool,
2450}
2451
2452impl StepState {
2453    fn expected(expected: bool) -> Self {
2454        Self {
2455            expected,
2456            done: !expected,
2457        }
2458    }
2459
2460    fn expected_count(&self) -> u32 {
2461        if self.expected { 1 } else { 0 }
2462    }
2463
2464    fn mark_done(&mut self) -> bool {
2465        if self.expected && !self.done {
2466            self.done = true;
2467            true
2468        } else {
2469            false
2470        }
2471    }
2472
2473    fn finish(&mut self) -> bool {
2474        self.mark_done()
2475    }
2476}
2477
2478#[derive(Clone, Copy, Default)]
2479struct Workload {
2480    expected: u32,
2481    completed: u32,
2482}
2483
2484impl Workload {
2485    fn add_expected(&mut self, count: u32) {
2486        self.expected = self.expected.saturating_add(count);
2487    }
2488
2489    fn add_completed(&mut self, count: u32) {
2490        if count == 0 {
2491            return;
2492        }
2493        self.completed = (self.completed + count).min(self.expected);
2494    }
2495
2496    fn reset(&mut self) {
2497        self.expected = 0;
2498        self.completed = 0;
2499    }
2500}
2501
2502struct LoadingProgress {
2503    token: ProgressToken,
2504    created: bool,
2505    active: bool,
2506}
2507
2508impl LoadingProgress {
2509    fn new(session_id: SessionId) -> Self {
2510        let token = ProgressToken::String(format!("ts-bridge:{}:{session_id}", std::process::id()));
2511        Self {
2512            token,
2513            created: false,
2514            active: false,
2515        }
2516    }
2517
2518    fn begin(&mut self, connection: &Connection, title: &str, message: &str) -> anyhow::Result<()> {
2519        if self.active {
2520            return Ok(());
2521        }
2522        self.ensure_token(connection)?;
2523        let params = ProgressParams {
2524            token: self.token.clone(),
2525            value: ProgressParamsValue::WorkDone(LspWorkDoneProgress::Begin(
2526                WorkDoneProgressBegin {
2527                    title: title.to_string(),
2528                    message: Some(message.to_string()),
2529                    ..WorkDoneProgressBegin::default()
2530                },
2531            )),
2532        };
2533        send_progress(connection, params)?;
2534        self.active = true;
2535        Ok(())
2536    }
2537
2538    fn report(
2539        &mut self,
2540        connection: &Connection,
2541        message: &str,
2542        percent: Option<u32>,
2543    ) -> anyhow::Result<()> {
2544        if !self.active {
2545            return Ok(());
2546        }
2547        let params = ProgressParams {
2548            token: self.token.clone(),
2549            value: ProgressParamsValue::WorkDone(LspWorkDoneProgress::Report(
2550                WorkDoneProgressReport {
2551                    message: Some(message.to_string()),
2552                    percentage: percent,
2553                    ..WorkDoneProgressReport::default()
2554                },
2555            )),
2556        };
2557        send_progress(connection, params)
2558    }
2559
2560    fn end(&mut self, connection: &Connection, message: &str) -> anyhow::Result<()> {
2561        if !self.active {
2562            return Ok(());
2563        }
2564        let params = ProgressParams {
2565            token: self.token.clone(),
2566            value: ProgressParamsValue::WorkDone(LspWorkDoneProgress::End(WorkDoneProgressEnd {
2567                message: Some(message.to_string()),
2568            })),
2569        };
2570        send_progress(connection, params)?;
2571        self.active = false;
2572        Ok(())
2573    }
2574
2575    fn ensure_token(&mut self, connection: &Connection) -> anyhow::Result<()> {
2576        if self.created {
2577            return Ok(());
2578        }
2579        let params = WorkDoneProgressCreateParams {
2580            token: self.token.clone(),
2581        };
2582        let request = Request::new(
2583            next_request_id(),
2584            <WorkDoneProgressCreate as LspRequest>::METHOD.to_string(),
2585            serde_json::to_value(params)?,
2586        );
2587        connection.sender.send(Message::Request(request))?;
2588        self.created = true;
2589        Ok(())
2590    }
2591}
2592
2593struct RestartProgress {
2594    token: ProgressToken,
2595    created: bool,
2596    active: bool,
2597}
2598
2599impl RestartProgress {
2600    fn new(session_id: SessionId) -> Self {
2601        let token = ProgressToken::String(format!(
2602            "ts-bridge-restart:{}:{session_id}",
2603            std::process::id()
2604        ));
2605        Self {
2606            token,
2607            created: false,
2608            active: false,
2609        }
2610    }
2611
2612    fn begin(
2613        &mut self,
2614        connection: &Connection,
2615        message: &str,
2616        kind: RestartKind,
2617    ) -> anyhow::Result<()> {
2618        if self.active {
2619            return Ok(());
2620        }
2621        self.ensure_token(connection)?;
2622        let params = ProgressParams {
2623            token: self.token.clone(),
2624            value: ProgressParamsValue::WorkDone(LspWorkDoneProgress::Begin(
2625                WorkDoneProgressBegin {
2626                    title: "ts-bridge".to_string(),
2627                    message: Some(format!("{message} ({})", kind.label())),
2628                    ..WorkDoneProgressBegin::default()
2629                },
2630            )),
2631        };
2632        send_progress(connection, params)?;
2633        self.active = true;
2634        Ok(())
2635    }
2636
2637    fn end(
2638        &mut self,
2639        connection: &Connection,
2640        message: &str,
2641        kind: RestartKind,
2642    ) -> anyhow::Result<()> {
2643        if !self.active {
2644            return Ok(());
2645        }
2646        let params = ProgressParams {
2647            token: self.token.clone(),
2648            value: ProgressParamsValue::WorkDone(LspWorkDoneProgress::End(WorkDoneProgressEnd {
2649                message: Some(format!("{message} ({})", kind.label())),
2650            })),
2651        };
2652        send_progress(connection, params)?;
2653        self.active = false;
2654        Ok(())
2655    }
2656
2657    fn ensure_token(&mut self, connection: &Connection) -> anyhow::Result<()> {
2658        if self.created {
2659            return Ok(());
2660        }
2661        let params = WorkDoneProgressCreateParams {
2662            token: self.token.clone(),
2663        };
2664        let request = Request::new(
2665            next_request_id(),
2666            <WorkDoneProgressCreate as LspRequest>::METHOD.to_string(),
2667            serde_json::to_value(params)?,
2668        );
2669        connection.sender.send(Message::Request(request))?;
2670        self.created = true;
2671        Ok(())
2672    }
2673}
2674
2675fn send_progress(connection: &Connection, params: ProgressParams) -> anyhow::Result<()> {
2676    let notif =
2677        ServerNotification::new(Progress::METHOD.to_string(), serde_json::to_value(params)?);
2678    connection.sender.send(Message::Notification(notif))?;
2679    Ok(())
2680}
2681
2682static SERVER_REQUEST_IDS: AtomicU64 = AtomicU64::new(1);
2683
2684fn next_request_id() -> RequestId {
2685    let seq = SERVER_REQUEST_IDS.fetch_add(1, Ordering::Relaxed);
2686    RequestId::from(format!("ts-bridge-request-{seq}"))
2687}
2688
2689impl FileDiagnostics {
2690    fn update_kind(&mut self, kind: DiagnosticsKind, diagnostics: Vec<lsp_types::Diagnostic>) {
2691        match kind {
2692            DiagnosticsKind::Syntax => self.syntax = diagnostics,
2693            DiagnosticsKind::Semantic => self.semantic = diagnostics,
2694            DiagnosticsKind::Suggestion => self.suggestion = diagnostics,
2695        }
2696    }
2697
2698    fn collect(&self) -> Vec<lsp_types::Diagnostic> {
2699        let mut all =
2700            Vec::with_capacity(self.syntax.len() + self.semantic.len() + self.suggestion.len());
2701        all.extend(self.syntax.iter().cloned());
2702        all.extend(self.semantic.iter().cloned());
2703        all.extend(self.suggestion.iter().cloned());
2704        all
2705    }
2706}
2707
2708fn friendly_project_name(root: &Path) -> String {
2709    root.file_name()
2710        .and_then(|name| name.to_str())
2711        .map(|name| name.to_string())
2712        .unwrap_or_else(|| root.display().to_string())
2713}
2714
2715fn stage_text(kind: DiagnosticsKind) -> &'static str {
2716    match kind {
2717        DiagnosticsKind::Syntax => "running syntax checks",
2718        DiagnosticsKind::Semantic => "evaluating semantic diagnostics",
2719        DiagnosticsKind::Suggestion => "collecting suggestions",
2720    }
2721}