Skip to main content

perl_lsp_limits/
lib.rs

1#![warn(missing_docs)]
2//! Central configuration for LSP operation limits and bounded behavior
3//!
4//! This module provides a single source of truth for all resource limits,
5//! result caps, and deadlines used throughout the LSP server. This ensures
6//! consistent behavior and makes limit tuning straightforward.
7//!
8//! # Design Goals
9//!
10//! - **Bounded memory**: All caches have hard caps with LRU eviction
11//! - **Bounded latency**: All loops have deadlines to prevent blocking
12//! - **Bounded results**: All list operations have caps for client safety
13//! - **Graceful degradation**: Exceed limits → degrade, don't crash
14//!
15//! # Usage
16//!
17//! ```rust,ignore
18//! use perl_lsp_limits::LspLimits;
19//!
20//! let limits = LspLimits::default();
21//! let results = my_query().take(limits.references_result_cap);
22//! ```
23
24use std::time::Duration;
25
26/// Central configuration for all LSP operation limits
27///
28/// All handlers should reference these limits rather than defining their own
29/// constants. This enables consistent behavior and easy tuning.
30#[derive(Debug, Clone)]
31pub struct LspLimits {
32    // =========================================================================
33    // Result Caps
34    // =========================================================================
35    /// Maximum workspace/symbol results (default: 200)
36    pub workspace_symbol_cap: usize,
37
38    /// Maximum textDocument/references results (default: 500)
39    pub references_cap: usize,
40
41    /// Maximum textDocument/completion results (default: 100)
42    pub completion_cap: usize,
43
44    /// Maximum textDocument/documentSymbol results (default: 500)
45    pub document_symbol_cap: usize,
46
47    /// Maximum textDocument/codeLens results (default: 100)
48    pub code_lens_cap: usize,
49
50    /// Maximum diagnostics per file (default: 200)
51    pub diagnostics_per_file_cap: usize,
52
53    /// Maximum inlay hints per file (default: 500)
54    pub inlay_hints_cap: usize,
55
56    // =========================================================================
57    // Cache Limits
58    // =========================================================================
59    /// Maximum AST cache entries (default: 100)
60    pub ast_cache_max_entries: usize,
61
62    /// AST cache TTL in seconds (default: 300 = 5 minutes)
63    pub ast_cache_ttl_secs: u64,
64
65    /// Maximum symbol cache entries (default: 1000)
66    pub symbol_cache_max_entries: usize,
67
68    // =========================================================================
69    // Index Limits
70    // =========================================================================
71    /// Maximum files to index (default: 10,000)
72    pub max_indexed_files: usize,
73
74    /// Maximum symbols per file (default: 5,000)
75    pub max_symbols_per_file: usize,
76
77    /// Maximum total symbols in index (default: 500,000)
78    pub max_total_symbols: usize,
79
80    /// Parse storm threshold - pending parses before degradation (default: 10)
81    pub parse_storm_threshold: usize,
82
83    /// Maximum file size in bytes before skipping parse (default: 1MB)
84    ///
85    /// Files exceeding this limit will be stored with empty AST and
86    /// no diagnostics to prevent the parser from hanging on huge files.
87    pub max_file_size_bytes: usize,
88
89    // =========================================================================
90    // Deadlines
91    // =========================================================================
92    /// Deadline for workspace folder scan (default: 30s)
93    pub workspace_scan_deadline: Duration,
94
95    /// Deadline for single file indexing (default: 5s)
96    pub file_index_deadline: Duration,
97
98    /// Deadline for reference search across workspace (default: 2s)
99    pub reference_search_deadline: Duration,
100
101    /// Deadline for regex scan operations (default: 1s)
102    pub regex_scan_deadline: Duration,
103
104    /// Deadline for filesystem operations (default: 500ms)
105    pub fs_operation_deadline: Duration,
106
107    /// Deadline for semantic tokens computation (default: 2s)
108    pub semantic_tokens_deadline: Duration,
109
110    /// Deadline for code lens resolve operations (default: 1s)
111    pub code_lens_resolve_deadline: Duration,
112
113    /// Deadline for completion operations (default: 500ms)
114    pub completion_deadline: Duration,
115
116    // =========================================================================
117    // Degradation Behavior
118    // =========================================================================
119    /// Whether to return partial results on timeout (default: true)
120    pub return_partial_on_timeout: bool,
121
122    /// Whether to include open documents when index is degraded (default: true)
123    pub include_open_docs_when_degraded: bool,
124}
125
126impl Default for LspLimits {
127    fn default() -> Self {
128        Self {
129            // Result caps
130            workspace_symbol_cap: 200,
131            references_cap: 500,
132            completion_cap: 100,
133            document_symbol_cap: 500,
134            code_lens_cap: 100,
135            diagnostics_per_file_cap: 200,
136            inlay_hints_cap: 500,
137
138            // Cache limits
139            ast_cache_max_entries: 100,
140            ast_cache_ttl_secs: 300,
141            symbol_cache_max_entries: 1000,
142
143            // Index limits
144            max_indexed_files: 10_000,
145            max_symbols_per_file: 5_000,
146            max_total_symbols: 500_000,
147            parse_storm_threshold: 10,
148            max_file_size_bytes: 1_024 * 1_024, // 1MB
149
150            // Deadlines
151            workspace_scan_deadline: Duration::from_secs(30),
152            file_index_deadline: Duration::from_secs(5),
153            reference_search_deadline: Duration::from_secs(2),
154            regex_scan_deadline: Duration::from_secs(1),
155            fs_operation_deadline: Duration::from_millis(500),
156            semantic_tokens_deadline: Duration::from_secs(2),
157            code_lens_resolve_deadline: Duration::from_secs(1),
158            completion_deadline: Duration::from_millis(500),
159
160            // Degradation behavior
161            return_partial_on_timeout: true,
162            include_open_docs_when_degraded: true,
163        }
164    }
165}
166
167impl LspLimits {
168    /// Create limits optimized for large workspaces (10K+ files)
169    pub fn large_workspace() -> Self {
170        Self {
171            max_indexed_files: 50_000,
172            max_total_symbols: 2_000_000,
173            workspace_scan_deadline: Duration::from_secs(120),
174            ..Default::default()
175        }
176    }
177
178    /// Create limits optimized for resource-constrained environments
179    pub fn constrained() -> Self {
180        Self {
181            ast_cache_max_entries: 50,
182            max_indexed_files: 5_000,
183            max_total_symbols: 100_000,
184            workspace_scan_deadline: Duration::from_secs(15),
185            reference_search_deadline: Duration::from_secs(1),
186            ..Default::default()
187        }
188    }
189
190    /// Update limits from LSP settings
191    ///
192    /// Reads from the `perl.limits` section of settings.
193    pub fn update_from_value(&mut self, settings: &serde_json::Value) {
194        if let Some(limits) = settings.get("limits") {
195            // Result caps
196            if let Some(v) = limits.get("workspaceSymbolCap").and_then(|v| v.as_u64()) {
197                self.workspace_symbol_cap = v as usize;
198            }
199            if let Some(v) = limits.get("referencesCap").and_then(|v| v.as_u64()) {
200                self.references_cap = v as usize;
201            }
202            if let Some(v) = limits.get("completionCap").and_then(|v| v.as_u64()) {
203                self.completion_cap = v as usize;
204            }
205
206            // Cache limits
207            if let Some(v) = limits.get("astCacheMaxEntries").and_then(|v| v.as_u64()) {
208                self.ast_cache_max_entries = v as usize;
209            }
210
211            // Index limits
212            if let Some(v) = limits.get("maxIndexedFiles").and_then(|v| v.as_u64()) {
213                self.max_indexed_files = v as usize;
214            }
215            if let Some(v) = limits.get("maxTotalSymbols").and_then(|v| v.as_u64()) {
216                self.max_total_symbols = v as usize;
217            }
218
219            // File size limit
220            if let Some(v) = limits.get("maxFileSizeBytes").and_then(|v| v.as_u64()) {
221                self.max_file_size_bytes = v as usize;
222            }
223
224            // Deadlines (in milliseconds)
225            if let Some(v) = limits.get("workspaceScanDeadlineMs").and_then(|v| v.as_u64()) {
226                self.workspace_scan_deadline = Duration::from_millis(v);
227            }
228            if let Some(v) = limits.get("referenceSearchDeadlineMs").and_then(|v| v.as_u64()) {
229                self.reference_search_deadline = Duration::from_millis(v);
230            }
231        }
232    }
233}
234
235/// Global singleton for LSP limits
236///
237/// Initialized with default values, can be updated via LSP settings.
238/// Thread-safe via internal locking.
239pub static LSP_LIMITS: std::sync::LazyLock<std::sync::RwLock<LspLimits>> =
240    std::sync::LazyLock::new(|| std::sync::RwLock::new(LspLimits::default()));
241
242/// Get current workspace symbol cap
243#[inline]
244pub fn workspace_symbol_cap() -> usize {
245    LSP_LIMITS.read().map(|l| l.workspace_symbol_cap).unwrap_or(200)
246}
247
248/// Get current references cap
249#[inline]
250pub fn references_cap() -> usize {
251    LSP_LIMITS.read().map(|l| l.references_cap).unwrap_or(500)
252}
253
254/// Get current completion cap
255#[inline]
256pub fn completion_cap() -> usize {
257    LSP_LIMITS.read().map(|l| l.completion_cap).unwrap_or(100)
258}
259
260/// Get current reference search deadline
261#[inline]
262pub fn reference_search_deadline() -> Duration {
263    LSP_LIMITS.read().map(|l| l.reference_search_deadline).unwrap_or(Duration::from_secs(2))
264}
265
266/// Get current regex scan deadline
267#[inline]
268pub fn regex_scan_deadline() -> Duration {
269    LSP_LIMITS.read().map(|l| l.regex_scan_deadline).unwrap_or(Duration::from_secs(1))
270}
271
272/// Get current code lens cap
273#[inline]
274pub fn code_lens_cap() -> usize {
275    LSP_LIMITS.read().map(|l| l.code_lens_cap).unwrap_or(100)
276}
277
278/// Get current document symbol cap
279#[inline]
280pub fn document_symbol_cap() -> usize {
281    LSP_LIMITS.read().map(|l| l.document_symbol_cap).unwrap_or(500)
282}
283
284/// Get current semantic tokens deadline
285#[inline]
286pub fn semantic_tokens_deadline() -> Duration {
287    LSP_LIMITS.read().map(|l| l.semantic_tokens_deadline).unwrap_or(Duration::from_secs(2))
288}
289
290/// Get current code lens resolve deadline
291#[inline]
292pub fn code_lens_resolve_deadline() -> Duration {
293    LSP_LIMITS.read().map(|l| l.code_lens_resolve_deadline).unwrap_or(Duration::from_secs(1))
294}
295
296/// Get current completion deadline
297#[inline]
298pub fn completion_deadline() -> Duration {
299    LSP_LIMITS.read().map(|l| l.completion_deadline).unwrap_or(Duration::from_millis(500))
300}
301
302/// Get current inlay hints cap
303#[inline]
304pub fn inlay_hints_cap() -> usize {
305    LSP_LIMITS.read().map(|l| l.inlay_hints_cap).unwrap_or(500)
306}
307
308/// Get current diagnostics per file cap
309#[inline]
310pub fn diagnostics_per_file_cap() -> usize {
311    LSP_LIMITS.read().map(|l| l.diagnostics_per_file_cap).unwrap_or(200)
312}
313
314/// Get current maximum file size in bytes
315#[inline]
316pub fn max_file_size_bytes() -> usize {
317    LSP_LIMITS.read().map(|l| l.max_file_size_bytes).unwrap_or(1_024 * 1_024)
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_default_limits() {
326        let limits = LspLimits::default();
327        assert_eq!(limits.workspace_symbol_cap, 200);
328        assert_eq!(limits.references_cap, 500);
329        assert_eq!(limits.max_indexed_files, 10_000);
330        assert_eq!(limits.max_file_size_bytes, 1_024 * 1_024);
331    }
332
333    #[test]
334    fn test_large_workspace_limits() {
335        let limits = LspLimits::large_workspace();
336        assert_eq!(limits.max_indexed_files, 50_000);
337        assert_eq!(limits.max_total_symbols, 2_000_000);
338    }
339
340    #[test]
341    fn test_constrained_limits() {
342        let limits = LspLimits::constrained();
343        assert_eq!(limits.max_indexed_files, 5_000);
344        assert_eq!(limits.ast_cache_max_entries, 50);
345    }
346
347    #[test]
348    fn test_update_from_value() {
349        let mut limits = LspLimits::default();
350        let settings = serde_json::json!({
351            "limits": {
352                "workspaceSymbolCap": 300,
353                "maxIndexedFiles": 20000
354            }
355        });
356        limits.update_from_value(&settings);
357        assert_eq!(limits.workspace_symbol_cap, 300);
358        assert_eq!(limits.max_indexed_files, 20_000);
359    }
360}