use lsp_types::*;
use serde_json::Value;
pub use perl_lsp_feature_flags::{AdvertisedFeatures, BuildFlags};
#[allow(clippy::field_reassign_with_default)]
pub fn capabilities_for(build: BuildFlags) -> ServerCapabilities {
let mut caps = ServerCapabilities::default();
caps.text_document_sync = Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions {
open_close: Some(true),
change: Some(TextDocumentSyncKind::FULL),
will_save: None,
will_save_wait_until: None,
save: None,
}));
if build.hover {
caps.hover_provider = Some(HoverProviderCapability::Simple(true));
}
if build.document_highlight {
caps.document_highlight_provider = Some(OneOf::Left(true));
}
if build.signature_help {
caps.signature_help_provider = Some(SignatureHelpOptions {
trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
retrigger_characters: Some(vec![
",".to_string(),
"@".to_string(),
"%".to_string(),
"{".to_string(),
"[".to_string(),
]),
work_done_progress_options: WorkDoneProgressOptions::default(),
});
}
if build.declaration {
caps.declaration_provider = Some(DeclarationCapability::Simple(true));
}
if build.completion {
caps.completion_provider = Some(CompletionOptions {
resolve_provider: Some(true),
trigger_characters: Some(vec![
"$".to_string(),
"@".to_string(),
"%".to_string(),
">".to_string(),
":".to_string(),
"-".to_string(),
]),
all_commit_characters: None,
work_done_progress_options: WorkDoneProgressOptions::default(),
completion_item: None,
});
}
if build.definition {
caps.definition_provider = Some(OneOf::Left(true));
}
if build.type_definition {
caps.type_definition_provider =
Some(lsp_types::TypeDefinitionProviderCapability::Simple(true));
}
if build.implementation {
caps.implementation_provider =
Some(lsp_types::ImplementationProviderCapability::Simple(true));
}
if build.references {
caps.references_provider = Some(OneOf::Left(true));
}
if build.document_symbol {
caps.document_symbol_provider = Some(OneOf::Left(true));
}
if build.workspace_symbol {
caps.workspace_symbol_provider = Some(OneOf::Left(true));
}
if build.notebook_document_sync {
caps.notebook_document_sync = Some(OneOf::Left(NotebookDocumentSyncOptions {
notebook_selector: vec![NotebookSelector::ByNotebook {
notebook: Notebook::String("jupyter-notebook".to_string()),
cells: Some(vec![NotebookCellSelector { language: "perl".to_string() }]),
}],
save: Some(true),
}));
}
if build.formatting {
caps.document_formatting_provider = Some(OneOf::Left(true));
}
if build.range_formatting {
caps.document_range_formatting_provider = Some(OneOf::Left(true));
}
if build.folding_range {
caps.folding_range_provider = Some(FoldingRangeProviderCapability::Simple(true));
}
if build.inlay_hints {
caps.inlay_hint_provider =
Some(OneOf::Right(InlayHintServerCapabilities::Options(InlayHintOptions {
resolve_provider: Some(true), work_done_progress_options: WorkDoneProgressOptions::default(),
})));
}
if build.pull_diagnostics {
caps.diagnostic_provider = Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
inter_file_dependencies: false,
workspace_diagnostics: true,
work_done_progress_options: WorkDoneProgressOptions::default(),
identifier: Some("perl-lsp".to_string()),
}));
}
if build.workspace_symbol_resolve {
caps.workspace_symbol_provider = Some(OneOf::Right(WorkspaceSymbolOptions {
resolve_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
}));
}
if build.semantic_tokens {
caps.semantic_tokens_provider =
Some(SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions {
work_done_progress_options: WorkDoneProgressOptions::default(),
legend: SemanticTokensLegend {
token_types: vec![
SemanticTokenType::NAMESPACE,
SemanticTokenType::TYPE,
SemanticTokenType::CLASS,
SemanticTokenType::INTERFACE,
SemanticTokenType::ENUM,
SemanticTokenType::ENUM_MEMBER,
SemanticTokenType::TYPE_PARAMETER,
SemanticTokenType::FUNCTION,
SemanticTokenType::METHOD,
SemanticTokenType::PROPERTY,
SemanticTokenType::MACRO,
SemanticTokenType::VARIABLE,
SemanticTokenType::PARAMETER,
SemanticTokenType::KEYWORD,
SemanticTokenType::MODIFIER,
SemanticTokenType::COMMENT,
SemanticTokenType::STRING,
SemanticTokenType::NUMBER,
SemanticTokenType::REGEXP,
SemanticTokenType::OPERATOR,
SemanticTokenType::new("sql_string"), SemanticTokenType::new("sql_heredoc_keyword"), SemanticTokenType::new("json_heredoc_key"), ],
token_modifiers: vec![
SemanticTokenModifier::DECLARATION,
SemanticTokenModifier::DEFINITION,
SemanticTokenModifier::READONLY,
SemanticTokenModifier::STATIC,
SemanticTokenModifier::DEPRECATED,
SemanticTokenModifier::ABSTRACT,
SemanticTokenModifier::ASYNC,
SemanticTokenModifier::MODIFICATION,
SemanticTokenModifier::DOCUMENTATION,
SemanticTokenModifier::DEFAULT_LIBRARY,
SemanticTokenModifier::new("scalarVariable"),
SemanticTokenModifier::new("arrayVariable"),
SemanticTokenModifier::new("hashVariable"),
],
},
range: Some(true),
full: Some(SemanticTokensFullOptions::Bool(true)),
}));
}
if build.code_actions {
let mut kinds = vec![CodeActionKind::QUICKFIX];
if build.source_organize_imports {
kinds.push(CodeActionKind::SOURCE_ORGANIZE_IMPORTS);
}
kinds.push(CodeActionKind::REFACTOR_EXTRACT);
caps.code_action_provider =
Some(CodeActionProviderCapability::Options(CodeActionOptions {
code_action_kinds: Some(kinds),
resolve_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
}));
}
#[cfg(not(target_arch = "wasm32"))]
if build.execute_command {
let commands = get_supported_commands();
caps.execute_command_provider = Some(ExecuteCommandOptions {
commands,
work_done_progress_options: WorkDoneProgressOptions::default(),
});
}
if build.rename {
caps.rename_provider = Some(OneOf::Right(RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
}));
}
if build.document_links {
caps.document_link_provider = Some(DocumentLinkOptions {
resolve_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
});
}
if build.selection_ranges {
caps.selection_range_provider = Some(SelectionRangeProviderCapability::Simple(true));
}
if build.on_type_formatting {
caps.document_on_type_formatting_provider = Some(DocumentOnTypeFormattingOptions {
first_trigger_character: "}".to_string(),
more_trigger_character: Some(vec![";".to_string(), "\n".to_string()]),
});
}
if build.code_lens {
caps.code_lens_provider = Some(CodeLensOptions { resolve_provider: Some(true) });
}
if build.linked_editing {
caps.linked_editing_range_provider =
Some(lsp_types::LinkedEditingRangeServerCapabilities::Simple(true));
}
if build.inline_completion {
let mut experimental = caps.experimental.take().unwrap_or_else(|| serde_json::json!({}));
if let Some(obj) = experimental.as_object_mut() {
obj.insert("inlineCompletionProvider".to_string(), serde_json::json!({}));
}
caps.experimental = Some(experimental);
}
if build.inline_values {
caps.inline_value_provider = Some(OneOf::Left(true));
}
if build.moniker {
caps.moniker_provider = Some(OneOf::Left(true));
}
if build.document_color {
caps.color_provider = Some(ColorProviderCapability::Simple(true));
}
if build.call_hierarchy {
caps.call_hierarchy_provider = Some(CallHierarchyServerCapability::Simple(true));
}
caps
}
pub fn capabilities_json(build: BuildFlags) -> Value {
let caps = capabilities_for(build.clone());
let mut json = serde_json::to_value(caps).unwrap_or_else(|e| {
tracing::error!(error = %e, "Failed to serialize capabilities to JSON");
serde_json::json!({})
});
if build.type_hierarchy {
json["typeHierarchyProvider"] = serde_json::json!({
"workDoneProgressOptions": {}
});
}
if build.range_formatting {
json["documentRangesFormattingProvider"] = serde_json::json!(true);
}
json
}
pub fn get_supported_commands() -> Vec<String> {
vec![
"perl.runTests".to_string(),
"perl.runFile".to_string(),
"perl.runTestSub".to_string(),
"perl.runCritic".to_string(),
"perl.runTest".to_string(),
"perl.runTestFile".to_string(),
"perl.runSubtest".to_string(),
"perl.debugFile".to_string(),
"perl.debugTest".to_string(),
"perl.goToTest".to_string(),
"perl.goToImplementation".to_string(),
]
}
pub fn cap_bool_or_object(caps: &Value, key: &str) -> bool {
caps.get(key).is_some_and(|v| v.is_boolean() || v.is_object())
}
pub fn default_capabilities() -> ServerCapabilities {
#[cfg(feature = "lsp-ga-lock")]
let flags = BuildFlags::ga_lock();
#[cfg(not(feature = "lsp-ga-lock"))]
let flags = BuildFlags::production();
capabilities_for(flags)
}
#[cfg(test)]
mod tests {
use super::*;
use perl_lsp_feature_contracts::feature_ids_from_caps;
use std::collections::BTreeSet;
const KNOWN_STRUCTURAL_GAPS: &[&str] = &[
"lsp.inline_completion",
"lsp.notebook_cell_execution",
"lsp.ranges_formatting",
"lsp.type_hierarchy",
];
fn assert_feature_id_alignment(profile: &str, flags: BuildFlags) {
let flag_ids: BTreeSet<&str> = flags.to_feature_ids().into_iter().collect();
let caps = capabilities_for(flags);
let cap_ids: BTreeSet<&str> = feature_ids_from_caps(&caps).into_iter().collect();
let gaps: BTreeSet<&str> = KNOWN_STRUCTURAL_GAPS.iter().copied().collect();
let in_flags_not_caps: BTreeSet<_> =
flag_ids.difference(&cap_ids).copied().filter(|id| !gaps.contains(id)).collect();
let in_caps_not_flags: BTreeSet<_> = cap_ids.difference(&flag_ids).collect();
assert!(
in_flags_not_caps.is_empty() && in_caps_not_flags.is_empty(),
"feature ID mismatch for {profile} profile:\n \
in to_feature_ids() but not in capabilities: {in_flags_not_caps:?}\n \
in capabilities but not in to_feature_ids(): {in_caps_not_flags:?}",
);
}
#[test]
fn feature_id_alignment_ga_lock() {
assert_feature_id_alignment("ga-lock", BuildFlags::ga_lock());
}
#[test]
fn feature_id_alignment_production() {
assert_feature_id_alignment("production", BuildFlags::production());
}
#[test]
fn feature_id_alignment_all() {
assert_feature_id_alignment("all", BuildFlags::all());
}
#[test]
fn ranges_formatting_advertised_in_json_when_enabled() {
let flags = BuildFlags { range_formatting: true, ..BuildFlags::default() };
let json = capabilities_json(flags);
assert!(
json.get("documentRangesFormattingProvider").is_some(),
"documentRangesFormattingProvider must be present in capabilities JSON when \
range_formatting is enabled"
);
}
#[test]
fn ranges_formatting_absent_in_json_when_disabled() {
let flags = BuildFlags { range_formatting: false, ..BuildFlags::default() };
let json = capabilities_json(flags);
assert!(
json.get("documentRangesFormattingProvider").is_none(),
"documentRangesFormattingProvider must not be present when range_formatting is disabled"
);
}
#[test]
fn test_subtest_lens_command_id_is_registered() {
let cmds = get_supported_commands();
assert!(
cmds.iter().any(|c| c == "perl.runSubtest"),
"perl.runSubtest must be in get_supported_commands"
);
}
#[test]
fn resolve_providers_advertised_in_full_profile() {
let json = capabilities_json(BuildFlags::all());
assert!(
json["completionProvider"]["resolveProvider"].as_bool().unwrap_or(false),
"completionProvider.resolveProvider must be true"
);
assert!(
json["codeActionProvider"]["resolveProvider"].as_bool().unwrap_or(false),
"codeActionProvider.resolveProvider must be true"
);
assert!(
json["codeLensProvider"]["resolveProvider"].as_bool().unwrap_or(false),
"codeLensProvider.resolveProvider must be true"
);
}
}