Skip to main content

perl_lsp_feature_flags/
lib.rs

1#![warn(missing_docs)]
2//! Shared feature flag types for LSP profile-driven capability selection.
3//!
4//! The `BuildFlags` shape is the single source of truth for runtime-constructed
5//! capability toggles, while `AdvertisedFeatures` is the runtime-facing projection
6//! used by server startup and protocol responses.
7use perl_lsp_feature_ids::*;
8
9/// LSP features advertised to clients for Perl script development
10///
11/// Controls which capabilities are announced during server initialization,
12/// enabling clients to provide appropriate UI elements and functionality
13/// for Perl script editing within LSP workflows.
14#[derive(Debug, Clone, Default)]
15pub struct AdvertisedFeatures {
16    /// Code completion for variables, functions, and keywords
17    pub completion: bool,
18    /// Hover information for symbols and documentation
19    pub hover: bool,
20    /// Go-to-definition navigation for symbols
21    pub definition: bool,
22    /// Find-all-references for symbol usage analysis
23    pub references: bool,
24    /// Document symbol outline for Perl script structure
25    pub document_symbol: bool,
26    /// Workspace-wide symbol search across Perl parsing files
27    pub workspace_symbol: bool,
28    /// Automated code actions and refactoring suggestions
29    pub code_action: bool,
30    /// Code lens with reference counts and actionable information
31    pub code_lens: bool,
32    /// Full document formatting with perltidy integration
33    pub formatting: bool,
34    /// Range-specific formatting for selected code sections
35    pub range_formatting: bool,
36    /// Symbol renaming with workspace-wide updates
37    pub rename: bool,
38    /// Code folding for improved Perl script navigation
39    pub folding_range: bool,
40    /// Smart text selection expansion for efficient editing
41    pub selection_range: bool,
42    /// Linked editing for synchronized symbol updates
43    pub linked_editing: bool,
44    /// Inline type and parameter hints for clarity
45    pub inlay_hints: bool,
46    /// Semantic syntax highlighting for Perl scripts
47    pub semantic_tokens: bool,
48    /// Call hierarchy navigation for function relationships
49    pub call_hierarchy: bool,
50    /// Type hierarchy for object-oriented Perl parsing
51    pub type_hierarchy: bool,
52    /// Pull-based diagnostic reporting for error detection
53    pub diagnostic_provider: bool,
54    /// Document color detection for hex codes and ANSI colors
55    pub document_color: bool,
56    /// Notebook document sync (didOpen/didChange/didSave/didClose)
57    pub notebook_document_sync: bool,
58    /// Notebook cell execution summary tracking
59    pub notebook_cell_execution: bool,
60    /// Signature help for function parameters
61    pub signature_help: bool,
62    /// Document highlight for symbol occurrences
63    pub document_highlight: bool,
64    /// Go-to-declaration navigation
65    pub declaration: bool,
66}
67
68/// Build-time feature flags for conditional LSP capability compilation
69///
70/// Controls which capabilities are compiled into the LSP server binary,
71/// allowing for optimized builds targeted at specific Perl parsing
72/// deployment scenarios within enterprise LSP environments.
73#[derive(Debug, Clone, Default, PartialEq, Eq)]
74pub struct BuildFlags {
75    /// Code completion provider compilation flag
76    pub completion: bool,
77    /// Hover information provider compilation flag
78    pub hover: bool,
79    /// Go-to-definition provider compilation flag
80    pub definition: bool,
81    /// Type definition navigation compilation flag
82    pub type_definition: bool,
83    /// Implementation finding compilation flag
84    pub implementation: bool,
85    /// Find-all-references provider compilation flag
86    pub references: bool,
87    /// Document symbol outline provider compilation flag
88    pub document_symbol: bool,
89    /// Workspace symbol search provider compilation flag
90    pub workspace_symbol: bool,
91    /// Inlay hints provider compilation flag
92    pub inlay_hints: bool,
93    /// Pull-based diagnostics provider compilation flag
94    pub pull_diagnostics: bool,
95    /// Workspace symbol resolution provider compilation flag
96    pub workspace_symbol_resolve: bool,
97    /// Semantic token highlighting provider compilation flag
98    pub semantic_tokens: bool,
99    /// Code actions provider compilation flag
100    pub code_actions: bool,
101    /// Command execution provider compilation flag
102    pub execute_command: bool,
103    /// Symbol renaming provider compilation flag
104    pub rename: bool,
105    /// Document links provider compilation flag
106    pub document_links: bool,
107    /// Smart text selection ranges provider compilation flag
108    pub selection_ranges: bool,
109    /// On-type formatting provider compilation flag
110    pub on_type_formatting: bool,
111    /// Code lens provider compilation flag
112    pub code_lens: bool,
113    /// Call hierarchy navigation provider compilation flag
114    pub call_hierarchy: bool,
115    /// Type hierarchy navigation provider compilation flag
116    pub type_hierarchy: bool,
117    /// Linked editing ranges provider compilation flag
118    pub linked_editing: bool,
119    /// Inline completion suggestions provider compilation flag
120    pub inline_completion: bool,
121    /// Inline values for debugging provider compilation flag
122    pub inline_values: bool,
123    /// Notebook document sync provider compilation flag
124    pub notebook_document_sync: bool,
125    /// Notebook cell execution summary tracking compilation flag
126    pub notebook_cell_execution: bool,
127    /// Stable symbol identifiers provider compilation flag
128    pub moniker: bool,
129    /// Document color provider compilation flag for color swatches in strings and comments
130    pub document_color: bool,
131    /// Source organize imports capability (GA-lock excludes this)
132    pub source_organize_imports: bool,
133    /// Document formatting provider compilation flag
134    pub formatting: bool,
135    /// Range formatting provider compilation flag
136    pub range_formatting: bool,
137    /// Folding range provider compilation flag
138    pub folding_range: bool,
139    /// Signature help provider compilation flag
140    pub signature_help: bool,
141    /// Document highlight provider compilation flag
142    pub document_highlight: bool,
143    /// Declaration provider compilation flag
144    pub declaration: bool,
145}
146
147impl BuildFlags {
148    /// Convert build flags to advertised features.
149    pub fn to_advertised_features(&self) -> AdvertisedFeatures {
150        AdvertisedFeatures {
151            completion: self.completion,
152            hover: self.hover,
153            definition: self.definition,
154            references: self.references,
155            document_symbol: self.document_symbol,
156            workspace_symbol: self.workspace_symbol,
157            code_action: self.code_actions,
158            code_lens: self.code_lens,
159            formatting: self.formatting,
160            range_formatting: self.range_formatting,
161            rename: self.rename,
162            folding_range: self.folding_range,
163            selection_range: self.selection_ranges,
164            linked_editing: self.linked_editing,
165            inlay_hints: self.inlay_hints,
166            semantic_tokens: self.semantic_tokens,
167            call_hierarchy: self.call_hierarchy,
168            type_hierarchy: self.type_hierarchy,
169            diagnostic_provider: self.pull_diagnostics,
170            document_color: self.document_color,
171            notebook_document_sync: self.notebook_document_sync,
172            notebook_cell_execution: self.notebook_cell_execution,
173            signature_help: self.signature_help,
174            document_highlight: self.document_highlight,
175            declaration: self.declaration,
176        }
177    }
178
179    /// Convert build flags to advertised feature ids used by BDD and tooling layers.
180    pub fn to_feature_ids(&self) -> Vec<&'static str> {
181        let mut ids = Vec::new();
182
183        if self.completion {
184            ids.push(LSP_COMPLETION);
185        }
186        if self.hover {
187            ids.push(LSP_HOVER);
188        }
189        if self.definition {
190            ids.push(LSP_DEFINITION);
191        }
192        if self.type_definition {
193            ids.push(LSP_TYPE_DEFINITION);
194        }
195        if self.implementation {
196            ids.push(LSP_IMPLEMENTATION);
197        }
198        if self.references {
199            ids.push(LSP_REFERENCES);
200        }
201        if self.document_symbol {
202            ids.push(LSP_DOCUMENT_SYMBOL);
203        }
204        if self.workspace_symbol {
205            ids.push(LSP_WORKSPACE_SYMBOL);
206        }
207        if self.inlay_hints {
208            ids.push(LSP_INLAY_HINT);
209        }
210        if self.pull_diagnostics {
211            ids.push(LSP_PULL_DIAGNOSTICS);
212        }
213        if self.semantic_tokens {
214            ids.push(LSP_SEMANTIC_TOKENS);
215        }
216        if self.code_actions {
217            ids.push(LSP_CODE_ACTION);
218        }
219        if self.execute_command {
220            ids.push(LSP_EXECUTE_COMMAND);
221        }
222        if self.rename {
223            ids.push(LSP_RENAME);
224        }
225        if self.document_links {
226            ids.push(LSP_DOCUMENT_LINK);
227        }
228        if self.selection_ranges {
229            ids.push(LSP_SELECTION_RANGE);
230        }
231        if self.on_type_formatting {
232            ids.push(LSP_ON_TYPE_FORMATTING);
233        }
234        if self.code_lens {
235            ids.push(LSP_CODE_LENS);
236        }
237        if self.call_hierarchy {
238            ids.push(LSP_CALL_HIERARCHY);
239        }
240        if self.type_hierarchy {
241            ids.push(LSP_TYPE_HIERARCHY);
242        }
243        if self.linked_editing {
244            ids.push(LSP_LINKED_EDITING_RANGE);
245        }
246        if self.inline_completion {
247            ids.push(LSP_INLINE_COMPLETION);
248        }
249        if self.inline_values {
250            ids.push(LSP_INLINE_VALUE);
251        }
252        if self.notebook_document_sync {
253            ids.push(LSP_NOTEBOOK_DOCUMENT_SYNC);
254        }
255        if self.notebook_cell_execution {
256            ids.push(LSP_NOTEBOOK_CELL_EXECUTION);
257        }
258        if self.moniker {
259            ids.push(LSP_MONIKER);
260        }
261        if self.document_color {
262            ids.push(LSP_DOCUMENT_COLOR);
263        }
264        if self.formatting {
265            ids.push(LSP_FORMATTING);
266        }
267        if self.range_formatting {
268            ids.push(LSP_RANGE_FORMATTING);
269            // ranges formatting (multi-range, LSP 3.18) is gated on the same flag because
270            // both features require perltidy and the handler already exists.
271            ids.push(LSP_RANGES_FORMATTING);
272        }
273        if self.folding_range {
274            ids.push(LSP_FOLDING_RANGE);
275        }
276        if self.signature_help {
277            ids.push(LSP_SIGNATURE_HELP);
278        }
279        if self.document_highlight {
280            ids.push(LSP_DOCUMENT_HIGHLIGHT);
281        }
282        if self.declaration {
283            ids.push(LSP_DECLARATION);
284        }
285
286        ids.sort_unstable();
287        ids.dedup();
288        ids
289    }
290
291    /// Default production-ready capabilities.
292    pub fn production() -> Self {
293        Self {
294            completion: true,
295            hover: true,
296            definition: true,
297            type_definition: true,
298            implementation: true,
299            references: true,
300            document_symbol: true,
301            workspace_symbol: true,
302            inlay_hints: true,
303            pull_diagnostics: true,
304            workspace_symbol_resolve: true,
305            semantic_tokens: true,
306            code_actions: true,
307            execute_command: true,
308            rename: true,
309            document_links: true,
310            selection_ranges: true,
311            on_type_formatting: true,
312            code_lens: true,
313            call_hierarchy: true,
314            type_hierarchy: true,
315            linked_editing: true,
316            inline_completion: true,
317            inline_values: true,
318            notebook_document_sync: true,
319            notebook_cell_execution: true,
320            moniker: true,
321            document_color: true,
322            source_organize_imports: true,
323            formatting: false,
324            range_formatting: false,
325            folding_range: true,
326            signature_help: true,
327            document_highlight: true,
328            declaration: true,
329        }
330    }
331
332    /// All capabilities for testing.
333    pub fn all() -> Self {
334        Self {
335            completion: true,
336            hover: true,
337            definition: true,
338            type_definition: true,
339            implementation: true,
340            references: true,
341            document_symbol: true,
342            workspace_symbol: true,
343            inlay_hints: true,
344            pull_diagnostics: true,
345            workspace_symbol_resolve: true,
346            semantic_tokens: true,
347            code_actions: true,
348            execute_command: true,
349            rename: true,
350            document_links: true,
351            selection_ranges: true,
352            on_type_formatting: true,
353            code_lens: true,
354            call_hierarchy: true,
355            type_hierarchy: true,
356            linked_editing: true,
357            inline_completion: true,
358            inline_values: true,
359            notebook_document_sync: true,
360            notebook_cell_execution: true,
361            moniker: true,
362            document_color: true,
363            source_organize_imports: true,
364            formatting: true,
365            range_formatting: true,
366            folding_range: true,
367            signature_help: true,
368            document_highlight: true,
369            declaration: true,
370        }
371    }
372
373    /// Conservative GA-lock capabilities.
374    pub fn ga_lock() -> Self {
375        Self {
376            completion: true,
377            hover: true,
378            definition: true,
379            type_definition: true,
380            implementation: true,
381            references: true,
382            document_symbol: true,
383            workspace_symbol: true,
384            inlay_hints: true,
385            pull_diagnostics: true,
386            workspace_symbol_resolve: true,
387            semantic_tokens: true,
388            code_actions: true,
389            execute_command: true,
390            rename: true,
391            document_links: true,
392            selection_ranges: true,
393            on_type_formatting: true,
394            code_lens: true,
395            call_hierarchy: true,
396            type_hierarchy: true,
397            linked_editing: true,
398            inline_completion: true,
399            inline_values: false,
400            notebook_document_sync: true,
401            notebook_cell_execution: true,
402            moniker: true,
403            document_color: true,
404            source_organize_imports: true,
405            formatting: true,
406            range_formatting: true,
407            folding_range: true,
408            signature_help: true,
409            document_highlight: true,
410            declaration: true,
411        }
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::{AdvertisedFeatures, BuildFlags};
418    use perl_lsp_feature_contracts::all_features;
419    use perl_lsp_feature_ids::LSP_DOCUMENT_COLOR;
420
421    #[test]
422    fn feature_ids_are_stable_and_sorted() {
423        let ga_lock = BuildFlags::ga_lock();
424        let ids = ga_lock.to_feature_ids();
425
426        let mut sorted = ids.clone();
427        sorted.sort_unstable();
428        sorted.dedup();
429
430        assert_eq!(ids, sorted);
431        assert!(
432            sorted.windows(2).all(|window| window[0] < window[1]),
433            "expected strictly increasing feature id ordering",
434        );
435    }
436
437    #[test]
438    fn feature_ids_are_valid_in_catalog() {
439        let ids = BuildFlags::all().to_feature_ids();
440        let known_ids: std::collections::HashSet<_> =
441            all_features().iter().map(|feature| feature.id).collect();
442        let unknown: Vec<_> = ids.iter().copied().filter(|id| !known_ids.contains(id)).collect();
443        assert!(unknown.is_empty(), "non-catalog feature IDs emitted: {:?}", unknown);
444    }
445
446    #[test]
447    fn document_color_uses_bdd_catalog_id() {
448        let flags = BuildFlags { document_color: true, ..Default::default() };
449        assert_eq!(flags.to_feature_ids(), vec![LSP_DOCUMENT_COLOR]);
450    }
451
452    #[test]
453    fn production_has_expected_profile_shape() {
454        let production = BuildFlags::production();
455        assert!(production.completion);
456        assert!(!production.formatting);
457        assert_eq!(
458            production,
459            BuildFlags {
460                completion: true,
461                hover: true,
462                definition: true,
463                type_definition: true,
464                implementation: true,
465                references: true,
466                document_symbol: true,
467                workspace_symbol: true,
468                inlay_hints: true,
469                pull_diagnostics: true,
470                workspace_symbol_resolve: true,
471                semantic_tokens: true,
472                code_actions: true,
473                execute_command: true,
474                rename: true,
475                document_links: true,
476                selection_ranges: true,
477                on_type_formatting: true,
478                code_lens: true,
479                call_hierarchy: true,
480                type_hierarchy: true,
481                linked_editing: true,
482                inline_completion: true,
483                inline_values: true,
484                notebook_document_sync: true,
485                notebook_cell_execution: true,
486                moniker: true,
487                document_color: true,
488                source_organize_imports: true,
489                formatting: false,
490                range_formatting: false,
491                folding_range: true,
492                signature_help: true,
493                document_highlight: true,
494                declaration: true
495            }
496        );
497    }
498
499    // ── ga_lock profile shape ───────────────────────────────────────
500
501    #[test]
502    fn ga_lock_has_expected_profile_shape() {
503        let ga = BuildFlags::ga_lock();
504        assert!(ga.completion);
505        assert!(ga.hover);
506        assert!(ga.definition);
507        assert!(ga.formatting, "ga-lock should include formatting");
508        assert!(ga.range_formatting, "ga-lock should include range_formatting");
509        assert!(!ga.inline_values, "ga-lock should exclude inline_values");
510    }
511
512    // ── all() profile enables everything ────────────────────────────
513
514    #[test]
515    fn all_profile_enables_every_flag() {
516        let all = BuildFlags::all();
517        assert!(all.completion);
518        assert!(all.hover);
519        assert!(all.definition);
520        assert!(all.type_definition);
521        assert!(all.implementation);
522        assert!(all.references);
523        assert!(all.document_symbol);
524        assert!(all.workspace_symbol);
525        assert!(all.inlay_hints);
526        assert!(all.pull_diagnostics);
527        assert!(all.workspace_symbol_resolve);
528        assert!(all.semantic_tokens);
529        assert!(all.code_actions);
530        assert!(all.execute_command);
531        assert!(all.rename);
532        assert!(all.document_links);
533        assert!(all.selection_ranges);
534        assert!(all.on_type_formatting);
535        assert!(all.code_lens);
536        assert!(all.call_hierarchy);
537        assert!(all.type_hierarchy);
538        assert!(all.linked_editing);
539        assert!(all.inline_completion);
540        assert!(all.inline_values);
541        assert!(all.notebook_document_sync);
542        assert!(all.notebook_cell_execution);
543        assert!(all.moniker);
544        assert!(all.document_color);
545        assert!(all.source_organize_imports);
546        assert!(all.formatting);
547        assert!(all.range_formatting);
548        assert!(all.folding_range);
549        assert!(all.signature_help);
550        assert!(all.document_highlight);
551        assert!(all.declaration);
552    }
553
554    // ── Default is all-false ────────────────────────────────────────
555
556    #[test]
557    fn default_flags_are_all_false() {
558        let default = BuildFlags::default();
559        assert!(default.to_feature_ids().is_empty(), "default flags should yield no features");
560    }
561
562    // ── all() produces strictly more IDs than ga_lock() ─────────────
563
564    #[test]
565    fn all_produces_superset_of_ga_lock_ids() {
566        let all_ids = BuildFlags::all().to_feature_ids();
567        let ga_ids = BuildFlags::ga_lock().to_feature_ids();
568        for id in &ga_ids {
569            assert!(all_ids.contains(id), "'all' profile should contain ga-lock feature '{id}'");
570        }
571        assert!(all_ids.len() >= ga_ids.len());
572    }
573
574    // ── to_advertised_features mapping ──────────────────────────────
575
576    #[test]
577    fn to_advertised_features_maps_completion() {
578        let flags = BuildFlags { completion: true, ..Default::default() };
579        let adv = flags.to_advertised_features();
580        assert!(adv.completion);
581        assert!(!adv.hover);
582    }
583
584    #[test]
585    fn to_advertised_features_maps_code_actions_to_code_action() {
586        let flags = BuildFlags { code_actions: true, ..Default::default() };
587        let adv = flags.to_advertised_features();
588        assert!(
589            adv.code_action,
590            "BuildFlags.code_actions should map to AdvertisedFeatures.code_action"
591        );
592    }
593
594    #[test]
595    fn to_advertised_features_maps_pull_diagnostics_to_diagnostic_provider() {
596        let flags = BuildFlags { pull_diagnostics: true, ..Default::default() };
597        let adv = flags.to_advertised_features();
598        assert!(
599            adv.diagnostic_provider,
600            "BuildFlags.pull_diagnostics should map to AdvertisedFeatures.diagnostic_provider"
601        );
602    }
603
604    #[test]
605    fn to_advertised_features_maps_selection_ranges_to_selection_range() {
606        let flags = BuildFlags { selection_ranges: true, ..Default::default() };
607        let adv = flags.to_advertised_features();
608        assert!(
609            adv.selection_range,
610            "BuildFlags.selection_ranges should map to AdvertisedFeatures.selection_range"
611        );
612    }
613
614    #[test]
615    fn default_advertised_features_are_all_false() {
616        let adv = AdvertisedFeatures::default();
617        assert!(!adv.completion);
618        assert!(!adv.hover);
619        assert!(!adv.definition);
620        assert!(!adv.references);
621        assert!(!adv.document_symbol);
622        assert!(!adv.workspace_symbol);
623        assert!(!adv.code_action);
624        assert!(!adv.code_lens);
625        assert!(!adv.formatting);
626        assert!(!adv.rename);
627    }
628
629    // ── Individual flag toggling produces exactly one feature ID ─────
630
631    #[test]
632    fn single_flag_produces_single_id() {
633        let cases: Vec<(&str, BuildFlags)> = vec![
634            ("completion", BuildFlags { completion: true, ..Default::default() }),
635            ("hover", BuildFlags { hover: true, ..Default::default() }),
636            ("definition", BuildFlags { definition: true, ..Default::default() }),
637            ("references", BuildFlags { references: true, ..Default::default() }),
638            ("rename", BuildFlags { rename: true, ..Default::default() }),
639            ("formatting", BuildFlags { formatting: true, ..Default::default() }),
640            ("signature_help", BuildFlags { signature_help: true, ..Default::default() }),
641            ("declaration", BuildFlags { declaration: true, ..Default::default() }),
642        ];
643        for (label, flags) in cases {
644            let ids = flags.to_feature_ids();
645            assert_eq!(
646                ids.len(),
647                1,
648                "flag '{label}' should produce exactly 1 feature id, got {ids:?}"
649            );
650        }
651    }
652
653    // ── Production vs all diff ──────────────────────────────────────
654
655    #[test]
656    fn production_and_all_differ_on_formatting() {
657        let prod = BuildFlags::production();
658        let all = BuildFlags::all();
659        assert!(!prod.formatting);
660        assert!(all.formatting);
661        assert!(!prod.range_formatting);
662        assert!(all.range_formatting);
663    }
664
665    #[test]
666    fn ga_lock_and_production_differ_on_inline_values() {
667        let ga = BuildFlags::ga_lock();
668        let prod = BuildFlags::production();
669        assert!(!ga.inline_values, "ga-lock excludes inline_values");
670        assert!(prod.inline_values, "production includes inline_values");
671    }
672}