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
70pub 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, ®istry)?;
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, ®istry)?;
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, ®istry)?;
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#[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(¶ms)?;
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(¶ms);
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(¶ms);
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(¶ms);
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 if let Some(root_path) = ¶ms.root_path {
1239 return Some(Path::new(root_path).to_path_buf());
1240 }
1241
1242 if let Some(root_uri) = ¶ms.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) = ¶ms.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(¶ms.text_document.uri) {
1438 self.documents.open(
1439 &uri,
1440 ¶ms.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(¶ms.text_document.uri) {
1473 self.documents.apply_changes(
1474 &uri,
1475 ¶ms.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 }
1536 return Ok(false);
1537 }
1538 if let Some(spec) = protocol::route_notification(¬if.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(¶ms)) {
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 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(¶ms.text_document.uri, ¶ms.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 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(¶ms.text_document.uri, ¶ms.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}