use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::{
CodeAction, CodeActionParams, CodeActionProviderCapability, CodeActionResponse, CodeLens,
CodeLensOptions, CodeLensParams, ColorInformation, ColorPresentation, ColorPresentationParams,
ColorProviderCapability, CompletionOptions, CompletionParams, CompletionResponse, Diagnostic,
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
DidChangeWatchedFilesRegistrationOptions, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DocumentColorParams, DocumentFormattingParams, DocumentLink,
DocumentLinkOptions, DocumentLinkParams, DocumentOnTypeFormattingOptions,
DocumentOnTypeFormattingParams, DocumentRangeFormattingParams, DocumentSymbolParams,
DocumentSymbolResponse, FileSystemWatcher, FoldingRange, FoldingRangeParams, GlobPattern,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, InitializeParams,
InitializeResult, InitializedParams, Location, NumberOrString, OneOf, Position,
PrepareRenameResponse, Range, ReferenceParams, Registration, RenameOptions, RenameParams,
SelectionRange, SelectionRangeParams, SelectionRangeProviderCapability, SemanticTokens,
SemanticTokensFullOptions, SemanticTokensOptions, SemanticTokensParams, SemanticTokensResult,
SemanticTokensServerCapabilities, ServerCapabilities, TextDocumentSyncCapability,
TextDocumentSyncKind, TextEdit, Url, WatchKind, WorkDoneProgressOptions, WorkspaceEdit,
};
use tower_lsp::{Client, LanguageServer};
use crate::document_store::DocumentStore;
use crate::parser;
use crate::schema::{self, ParseContext, SchemaCache, SchemaStoreCatalog};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum YamlVersion {
V1_1,
V1_2,
}
#[derive(Debug, Default, Clone, serde::Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Settings {
pub custom_tags: Vec<String>,
pub key_ordering: bool,
pub schemas: HashMap<String, String>,
pub kubernetes_version: Option<String>,
pub schema_store: Option<bool>,
pub format_print_width: Option<usize>,
pub format_single_quote: Option<bool>,
pub format_preserve_quotes: Option<bool>,
pub http_proxy: Option<String>,
pub color_decorators: Option<bool>,
pub format_validation: Option<bool>,
pub yaml_version: Option<String>,
pub validate: Option<bool>,
pub hover: Option<bool>,
pub completion: Option<bool>,
pub max_items_computed: Option<usize>,
pub flow_style: Option<String>,
pub duplicate_keys: Option<String>,
pub format_enforce_block_style: Option<bool>,
pub format_remove_duplicate_keys: Option<bool>,
pub format_bracket_spacing: Option<bool>,
}
const DEFAULT_KUBERNETES_VERSION: &str = "master";
pub struct Backend {
client: Client,
document_store: Mutex<DocumentStore>,
schema_associations: Mutex<HashMap<Url, String>>,
schema_cache: Mutex<SchemaCache>,
diagnostics: Mutex<HashMap<Url, Vec<Diagnostic>>>,
settings: Mutex<Settings>,
schemastore_catalog: Mutex<Option<SchemaStoreCatalog>>,
}
impl Backend {
#[must_use]
pub fn new(client: Client) -> Self {
Self {
client,
document_store: Mutex::new(DocumentStore::new()),
schema_associations: Mutex::new(HashMap::new()),
schema_cache: Mutex::new(SchemaCache::new()),
diagnostics: Mutex::new(HashMap::new()),
settings: Mutex::new(Settings::default()),
schemastore_catalog: Mutex::new(None),
}
}
pub(crate) fn get_custom_tags(&self) -> Vec<String> {
self.settings
.lock()
.ok()
.map(|s| s.custom_tags.clone())
.unwrap_or_default()
}
pub(crate) fn get_key_ordering(&self) -> bool {
self.settings.lock().ok().is_some_and(|s| s.key_ordering)
}
pub(crate) fn get_schema_associations(&self) -> Vec<crate::schema::SchemaAssociation> {
self.settings
.lock()
.ok()
.map(|s| {
s.schemas
.iter()
.map(|(url, pattern)| crate::schema::SchemaAssociation {
pattern: pattern.clone(),
url: url.clone(),
})
.collect()
})
.unwrap_or_default()
}
pub(crate) fn get_kubernetes_version(&self) -> String {
self.settings
.lock()
.ok()
.and_then(|s| s.kubernetes_version.clone())
.unwrap_or_else(|| DEFAULT_KUBERNETES_VERSION.to_string())
}
pub(crate) fn get_schema_store_enabled(&self) -> bool {
self.settings
.lock()
.ok()
.is_none_or(|s| s.schema_store.unwrap_or(true))
}
pub(crate) fn get_http_proxy(&self) -> Option<String> {
self.settings.lock().ok().and_then(|s| s.http_proxy.clone())
}
pub(crate) fn get_format_validation(&self) -> bool {
self.settings
.lock()
.ok()
.is_none_or(|s| s.format_validation.unwrap_or(true))
}
pub(crate) fn get_yaml_version(&self, text: &str) -> YamlVersion {
let parse_version = |s: &str| {
if s == "1.1" {
YamlVersion::V1_1
} else {
YamlVersion::V1_2
}
};
if let Some(v) = schema::extract_yaml_version(text) {
return parse_version(&v);
}
self.settings
.lock()
.ok()
.and_then(|s| s.yaml_version.clone())
.map_or(YamlVersion::V1_2, |v| parse_version(&v))
}
async fn get_or_fetch_schemastore_catalog(&self) -> Option<SchemaStoreCatalog> {
let cached = self
.schemastore_catalog
.lock()
.ok()
.and_then(|guard| guard.clone());
if cached.is_some() {
return cached;
}
let proxy = self.get_http_proxy();
let fetched = tokio::task::spawn_blocking(move || {
crate::schema::fetch_schemastore_catalog(proxy.as_deref())
})
.await;
match fetched {
Ok(Ok(catalog)) => {
if let Ok(mut guard) = self.schemastore_catalog.lock() {
*guard = Some(catalog.clone());
}
Some(catalog)
}
Ok(Err(e)) => {
self.client
.log_message(
tower_lsp::lsp_types::MessageType::WARNING,
format!("SchemaStore catalog fetch failed: {e}"),
)
.await;
None
}
Err(_) => None,
}
}
pub fn get_document_text(&self, uri: &str) -> Option<String> {
let parsed = Url::parse(uri).ok()?;
let store = self.document_store.lock().ok()?;
store.get(&parsed).map(str::to_string)
}
pub fn get_diagnostics(&self, uri: &str) -> Option<Vec<Diagnostic>> {
let parsed = Url::parse(uri).ok()?;
let diags = self.diagnostics.lock().ok()?;
diags.get(&parsed).cloned()
}
#[doc(hidden)]
pub fn seed_schema_cache(&self, schema_url: &str, schema: crate::schema::JsonSchema) {
if let Ok(mut cache) = self.schema_cache.lock() {
cache.insert(schema_url.to_string(), serde_json::Value::Null, schema);
}
}
async fn process_schema(
&self,
uri: &Url,
schema_url: &str,
diagnostics: &mut Vec<Diagnostic>,
documents: &[rlsp_yaml_parser::node::Document<rlsp_yaml_parser::Span>],
yaml_version: YamlVersion,
) {
let normalised = crate::schema::validate_and_normalize_url(schema_url).ok();
if let Some(url) = normalised {
if let Ok(mut assoc) = self.schema_associations.lock() {
assoc.insert(uri.clone(), url.clone());
}
let cached = self
.schema_cache
.lock()
.ok()
.and_then(|cache| cache.get(&url).cloned());
let schema = if let Some(s) = cached {
Some(s)
} else {
let mut taken_cache = self
.schema_cache
.lock()
.ok()
.map(|mut g| std::mem::take(&mut *g))
.unwrap_or_default();
let url_clone = url.clone();
let proxy = self.get_http_proxy();
let join_result = tokio::task::spawn_blocking(move || {
let mut ctx = ParseContext::new(&mut taken_cache, proxy.as_deref());
let result = crate::schema::fetch_schema_raw(
&url_clone,
proxy.as_deref(),
Some(&mut ctx),
);
(result, taken_cache)
})
.await;
let Ok((fetch_result, returned_cache)) = join_result else {
return;
};
if let Ok(mut guard) = self.schema_cache.lock() {
for (key, (value, schema)) in returned_cache.into_inner() {
guard.insert(key, value, schema);
}
}
let fetched: Option<(serde_json::Value, crate::schema::JsonSchema)> =
fetch_result.ok();
if let Some((ref v, ref s)) = fetched
&& let Ok(mut cache) = self.schema_cache.lock()
{
cache.insert(url, v.clone(), s.clone());
}
fetched.map(|(_, s)| s)
};
if let Some(s) = schema {
let format_validation = self.get_format_validation();
diagnostics.extend(crate::schema_validation::validate_schema(
documents,
&s,
format_validation,
yaml_version,
));
}
}
}
async fn run_schema_validation(
&self,
uri: &Url,
text: &str,
documents: &[rlsp_yaml_parser::node::Document<rlsp_yaml_parser::Span>],
diagnostics: &mut Vec<Diagnostic>,
yaml_version: YamlVersion,
) {
if let Some(schema_url) = crate::schema::extract_schema_url(text) {
if schema_url.eq_ignore_ascii_case("none") {
if let Ok(mut assoc) = self.schema_associations.lock() {
assoc.remove(uri);
}
} else {
self.process_schema(uri, &schema_url, diagnostics, documents, yaml_version)
.await;
}
return;
}
let associations = self.get_schema_associations();
let k8s_version = self.get_kubernetes_version();
let schema_store_enabled = self.get_schema_store_enabled();
let filename = uri.path();
if let Some(schema_url) = crate::schema::match_schema_by_filename(filename, &associations) {
self.process_schema(uri, &schema_url, diagnostics, documents, yaml_version)
.await;
} else if let Some((api_version, kind)) =
crate::schema::detect_kubernetes_resource(documents)
{
let schema_url =
crate::schema::kubernetes_schema_url(&api_version, &kind, &k8s_version);
self.process_schema(uri, &schema_url, diagnostics, documents, yaml_version)
.await;
} else if schema_store_enabled {
if let Some(catalog) = self.get_or_fetch_schemastore_catalog().await {
if let Some(schema_url) = crate::schema::match_schemastore(filename, &catalog) {
self.process_schema(uri, &schema_url, diagnostics, documents, yaml_version)
.await;
}
}
}
}
async fn parse_and_publish(&self, uri: Url, text: &str) {
if !self.get_validate() {
if let Ok(mut diags) = self.diagnostics.lock() {
diags.insert(uri.clone(), Vec::new());
}
self.client.publish_diagnostics(uri, Vec::new(), None).await;
return;
}
let result = parser::parse_yaml(text);
let mut diagnostics = result.diagnostics.clone();
diagnostics.extend(crate::validation::validators::validate_unused_anchors(
&result.documents,
));
let flow_style_setting = self.get_flow_style();
if flow_style_setting.as_deref() != Some("off") {
let mut flow_diags =
crate::validation::validators::validate_flow_style(&result.documents);
if flow_style_setting.as_deref() == Some("error") {
for diag in &mut flow_diags {
diag.severity = Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR);
}
}
diagnostics.extend(flow_diags);
}
let duplicate_keys_setting = self.get_duplicate_keys();
if duplicate_keys_setting.as_deref() != Some("off") {
let mut dup_diags =
crate::validation::validators::validate_duplicate_keys(&result.documents);
if duplicate_keys_setting.as_deref() == Some("warning") {
for diag in &mut dup_diags {
diag.severity = Some(tower_lsp::lsp_types::DiagnosticSeverity::WARNING);
}
}
diagnostics.extend(dup_diags);
}
let key_ordering = self.get_key_ordering();
if key_ordering {
diagnostics.extend(crate::validation::validators::validate_key_ordering(
&result.documents,
));
}
let mut allowed_tags: HashSet<String> = self.get_custom_tags().into_iter().collect();
allowed_tags.extend(crate::schema::extract_custom_tags(text));
diagnostics.extend(crate::validation::validators::validate_custom_tags(
&result.documents,
&allowed_tags,
));
let yaml_version = self.get_yaml_version(text);
if yaml_version == YamlVersion::V1_2 {
diagnostics.extend(crate::validation::validators::validate_yaml11_compat(
&result.documents,
));
}
self.run_schema_validation(
&uri,
text,
&result.documents,
&mut diagnostics,
yaml_version,
)
.await;
let suppressions = crate::validation::suppression::build_suppression_map(text);
diagnostics.retain(|diag| {
let code = diag.code.as_ref().and_then(|c| match c {
NumberOrString::String(s) => Some(s.as_str()),
NumberOrString::Number(_) => None,
});
!suppressions.is_suppressed(diag.range.start.line, code.unwrap_or(""))
});
if let Ok(mut diags) = self.diagnostics.lock() {
diags.insert(uri.clone(), diagnostics.clone());
}
self.client
.publish_diagnostics(uri, diagnostics, None)
.await;
}
#[must_use]
pub fn capabilities() -> ServerCapabilities {
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
hover_provider: Some(tower_lsp::lsp_types::HoverProviderCapability::Simple(true)),
document_symbol_provider: Some(OneOf::Left(true)),
completion_provider: Some(CompletionOptions {
resolve_provider: Some(false),
..CompletionOptions::default()
}),
definition_provider: Some(OneOf::Left(true)),
references_provider: Some(OneOf::Left(true)),
folding_range_provider: Some(
tower_lsp::lsp_types::FoldingRangeProviderCapability::Simple(true),
),
rename_provider: Some(OneOf::Right(RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
})),
document_link_provider: Some(DocumentLinkOptions {
resolve_provider: Some(false),
work_done_progress_options: WorkDoneProgressOptions::default(),
}),
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(false),
}),
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: Some(OneOf::Left(true)),
document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
first_trigger_character: "\n".to_string(),
more_trigger_character: None,
}),
semantic_tokens_provider: Some(
SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions {
legend: crate::analysis::semantic_tokens::legend(),
full: Some(SemanticTokensFullOptions::Bool(true)),
..SemanticTokensOptions::default()
}),
),
color_provider: Some(ColorProviderCapability::Simple(true)),
..ServerCapabilities::default()
}
}
pub(crate) fn get_color_decorators_enabled(&self) -> bool {
self.settings
.lock()
.ok()
.is_none_or(|s| s.color_decorators.unwrap_or(true))
}
pub(crate) fn get_validate(&self) -> bool {
self.settings
.lock()
.ok()
.is_none_or(|s| s.validate.unwrap_or(true))
}
pub(crate) fn get_hover_enabled(&self) -> bool {
self.settings
.lock()
.ok()
.is_none_or(|s| s.hover.unwrap_or(true))
}
pub(crate) fn get_completion_enabled(&self) -> bool {
self.settings
.lock()
.ok()
.is_none_or(|s| s.completion.unwrap_or(true))
}
pub(crate) fn get_max_items_computed(&self) -> usize {
self.settings
.lock()
.ok()
.and_then(|s| s.max_items_computed)
.unwrap_or(5000)
}
pub(crate) fn get_flow_style(&self) -> Option<String> {
self.settings.lock().ok().and_then(|s| s.flow_style.clone())
}
pub(crate) fn get_duplicate_keys(&self) -> Option<String> {
self.settings
.lock()
.ok()
.and_then(|s| s.duplicate_keys.clone())
}
}
#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
let settings = params
.initialization_options
.and_then(|v| serde_json::from_value::<Settings>(v).ok())
.unwrap_or_default();
if let Ok(mut s) = self.settings.lock() {
*s = settings;
}
Ok(InitializeResult {
capabilities: Self::capabilities(),
..InitializeResult::default()
})
}
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
if let Ok(settings) = serde_json::from_value::<Settings>(params.settings)
&& let Ok(mut s) = self.settings.lock()
{
*s = settings;
}
}
async fn initialized(&self, _: InitializedParams) {
let watchers = vec![
FileSystemWatcher {
glob_pattern: GlobPattern::String("**/*.yaml".to_string()),
kind: Some(WatchKind::all()),
},
FileSystemWatcher {
glob_pattern: GlobPattern::String("**/*.yml".to_string()),
kind: Some(WatchKind::all()),
},
];
let registration = Registration {
id: "yaml-file-watcher".to_string(),
method: "workspace/didChangeWatchedFiles".to_string(),
register_options: serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
watchers,
})
.ok(),
};
let client = self.client.clone();
tokio::spawn(async move {
let _ = client.register_capability(vec![registration]).await;
});
}
async fn shutdown(&self) -> Result<()> {
Ok(())
}
async fn did_change_watched_files(&self, _params: DidChangeWatchedFilesParams) {
let docs: Vec<(Url, String)> = if let Ok(store) = self.document_store.lock() {
store.all_documents()
} else {
return;
};
for (uri, text) in docs {
self.parse_and_publish(uri, &text).await;
}
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri;
let text = params.text_document.text;
if let Ok(mut store) = self.document_store.lock() {
store.open(uri.clone(), text.clone());
}
self.parse_and_publish(uri, &text).await;
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri;
if let Some(change) = params.content_changes.into_iter().last() {
if let Ok(mut store) = self.document_store.lock() {
store.change(&uri, change.text.clone());
}
self.parse_and_publish(uri, &change.text).await;
}
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
if let Ok(mut store) = self.document_store.lock() {
store.close(&uri);
}
if let Ok(mut diags) = self.diagnostics.lock() {
diags.remove(&uri);
}
self.client.publish_diagnostics(uri, Vec::new(), None).await;
}
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
if !self.get_hover_enabled() {
return Ok(None);
}
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
let text_present = store.get(&uri).is_some();
let docs = store.get_documents(&uri).cloned();
(docs, text_present)
} else {
return Ok(None);
};
if !text_present {
return Ok(None);
}
let Some(docs) = docs else {
return Ok(None);
};
let schema_url = self
.schema_associations
.lock()
.ok()
.and_then(|assoc| assoc.get(&uri).cloned());
let schema = schema_url.and_then(|url| {
self.schema_cache
.lock()
.ok()
.and_then(|cache| cache.get(&url).cloned())
});
Ok(crate::hover::hover_at(&docs, position, schema.as_ref()))
}
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
if !self.get_completion_enabled() {
return Ok(None);
}
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let docs = if let Ok(store) = self.document_store.lock() {
let text = store.get(&uri);
if text.is_none() {
return Ok(None);
}
store.get_documents(&uri).cloned()
} else {
return Ok(None);
};
let schema_url = self
.schema_associations
.lock()
.ok()
.and_then(|assoc| assoc.get(&uri).cloned());
let schema = schema_url.and_then(|url| {
self.schema_cache
.lock()
.ok()
.and_then(|cache| cache.get(&url).cloned())
});
let items = crate::completion::complete_at(
docs.as_deref().unwrap_or(&[]),
position,
schema.as_ref(),
);
if items.is_empty() {
return Ok(None);
}
Ok(Some(CompletionResponse::Array(items)))
}
async fn goto_definition(
&self,
params: GotoDefinitionParams,
) -> Result<Option<GotoDefinitionResponse>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
let text_present = store.get(&uri).is_some();
let docs = store.get_documents(&uri).cloned();
(docs, text_present)
} else {
return Ok(None);
};
if !text_present {
return Ok(None);
}
let Some(docs) = docs else {
return Ok(None);
};
let location = crate::navigation::references::goto_definition(&docs, &uri, position);
Ok(location.map(GotoDefinitionResponse::Scalar))
}
async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let include_declaration = params.context.include_declaration;
let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
let text_present = store.get(&uri).is_some();
let docs = store.get_documents(&uri).cloned();
(docs, text_present)
} else {
return Ok(None);
};
if !text_present {
return Ok(None);
}
let Some(docs) = docs else {
return Ok(None);
};
let locations = crate::navigation::references::find_references(
&docs,
&uri,
position,
include_declaration,
);
if locations.is_empty() {
return Ok(None);
}
Ok(Some(locations))
}
async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
let uri = params.text_document.uri;
let (text, docs) = if let Ok(store) = self.document_store.lock() {
let text = store.get(&uri).map(str::to_string);
let docs = store.get_documents(&uri).cloned();
(text, docs)
} else {
return Ok(None);
};
let Some(text) = text else {
return Ok(None);
};
let mut ranges =
crate::analysis::folding::folding_ranges(docs.as_deref().unwrap_or(&[]), &text);
let limit = self.get_max_items_computed();
ranges.truncate(limit);
if ranges.is_empty() {
return Ok(None);
}
Ok(Some(ranges))
}
async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
let uri = params.text_document.uri;
let docs = if let Ok(store) = self.document_store.lock() {
if store.get(&uri).is_none() {
return Ok(None);
}
store.get_documents(&uri).cloned()
} else {
return Ok(None);
};
let links = crate::decorators::document_links::find_document_links(
docs.as_deref().unwrap_or(&[]),
Some(&uri),
);
if links.is_empty() {
return Ok(None);
}
Ok(Some(links))
}
async fn selection_range(
&self,
params: SelectionRangeParams,
) -> Result<Option<Vec<SelectionRange>>> {
let uri = params.text_document.uri;
let docs = if let Ok(store) = self.document_store.lock() {
if store.get(&uri).is_none() {
return Ok(None);
}
store.get_documents(&uri).cloned()
} else {
return Ok(None);
};
let result = crate::analysis::selection::selection_ranges(
docs.as_deref().unwrap_or(&[]),
¶ms.positions,
);
if result.is_empty() {
return Ok(None);
}
Ok(Some(result))
}
async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
let uri = params.text_document.uri;
let range = params.range;
let text = if let Ok(store) = self.document_store.lock() {
store.get(&uri).map(str::to_string)
} else {
return Ok(None);
};
let Some(text) = text else {
return Ok(None);
};
let diagnostics = self.get_diagnostics(uri.as_str()).unwrap_or_default();
let docs = crate::parser::parse_yaml(&text).documents;
let actions =
crate::editing::code_actions::code_actions(&docs, &text, range, &diagnostics, &uri);
if actions.is_empty() {
return Ok(None);
}
Ok(Some(actions.into_iter().map(CodeAction::into).collect()))
}
async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
let uri = params.text_document.uri;
let schema_url = self
.schema_associations
.lock()
.ok()
.and_then(|assoc| assoc.get(&uri).cloned());
let Some(url) = schema_url else {
return Ok(None);
};
let schema = self
.schema_cache
.lock()
.ok()
.and_then(|cache| cache.get(&url).cloned());
let lenses = crate::decorators::code_lens::code_lenses(&url, schema.as_ref());
if lenses.is_empty() {
return Ok(None);
}
Ok(Some(lenses))
}
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
let uri = params.text_document.uri;
let text = if let Ok(store) = self.document_store.lock() {
store.get(&uri).map(str::to_string)
} else {
return Ok(None);
};
let Some(text) = text else {
return Ok(None);
};
let tab_size = params.options.tab_size as usize;
let insert_spaces = params.options.insert_spaces;
let yaml_version = self.get_yaml_version(&text);
let settings = self.settings.lock().ok();
let options = crate::editing::formatter::YamlFormatOptions {
print_width: settings
.as_ref()
.and_then(|s| s.format_print_width)
.unwrap_or(80),
tab_width: tab_size,
use_tabs: !insert_spaces,
single_quote: settings
.as_ref()
.and_then(|s| s.format_single_quote)
.unwrap_or(false),
preserve_quotes: settings
.as_ref()
.and_then(|s| s.format_preserve_quotes)
.unwrap_or(false),
bracket_spacing: settings
.as_ref()
.and_then(|s| s.format_bracket_spacing)
.unwrap_or(true),
yaml_version,
format_enforce_block_style: settings
.as_ref()
.and_then(|s| s.format_enforce_block_style)
.unwrap_or(false),
format_remove_duplicate_keys: settings
.as_ref()
.and_then(|s| s.format_remove_duplicate_keys)
.unwrap_or(false),
};
drop(settings);
let formatted = crate::editing::formatter::format_yaml(&text, &options);
if formatted == text {
return Ok(None);
}
let lines: Vec<&str> = text.lines().collect();
let end = if text.ends_with('\n') {
Position {
line: u32::try_from(lines.len()).unwrap_or(u32::MAX),
character: 0,
}
} else {
let last_col = lines.last().map_or(0, |l| l.len());
Position {
line: u32::try_from(lines.len().saturating_sub(1)).unwrap_or(u32::MAX),
character: u32::try_from(last_col).unwrap_or(u32::MAX),
}
};
Ok(Some(vec![TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end,
},
new_text: formatted,
}]))
}
async fn range_formatting(
&self,
params: DocumentRangeFormattingParams,
) -> Result<Option<Vec<TextEdit>>> {
let uri = params.text_document.uri;
let requested = params.range;
let text = if let Ok(store) = self.document_store.lock() {
store.get(&uri).map(str::to_string)
} else {
return Ok(None);
};
let Some(text) = text else {
return Ok(None);
};
let tab_size = params.options.tab_size as usize;
let insert_spaces = params.options.insert_spaces;
let yaml_version = self.get_yaml_version(&text);
let settings = self.settings.lock().ok();
let options = crate::editing::formatter::YamlFormatOptions {
print_width: settings
.as_ref()
.and_then(|s| s.format_print_width)
.unwrap_or(80),
tab_width: tab_size,
use_tabs: !insert_spaces,
single_quote: settings
.as_ref()
.and_then(|s| s.format_single_quote)
.unwrap_or(false),
preserve_quotes: settings
.as_ref()
.and_then(|s| s.format_preserve_quotes)
.unwrap_or(false),
bracket_spacing: settings
.as_ref()
.and_then(|s| s.format_bracket_spacing)
.unwrap_or(true),
yaml_version,
format_enforce_block_style: settings
.as_ref()
.and_then(|s| s.format_enforce_block_style)
.unwrap_or(false),
format_remove_duplicate_keys: settings
.as_ref()
.and_then(|s| s.format_remove_duplicate_keys)
.unwrap_or(false),
};
drop(settings);
let formatted = crate::editing::formatter::format_yaml(&text, &options);
let orig_lines: Vec<&str> = text.lines().collect();
let fmt_lines: Vec<&str> = formatted.lines().collect();
let start_line = requested.start.line as usize;
let end_line = requested.end.line as usize;
let orig_end = end_line.min(orig_lines.len().saturating_sub(1));
let fmt_end = end_line.min(fmt_lines.len().saturating_sub(1));
let orig_slice = orig_lines
.get(start_line..=orig_end)
.unwrap_or_default()
.join("\n");
let fmt_slice = fmt_lines
.get(start_line..=fmt_end)
.unwrap_or_default()
.join("\n");
if orig_slice == fmt_slice {
return Ok(None);
}
let new_text = format!("{fmt_slice}\n");
let edit_start = Position {
line: u32::try_from(start_line).unwrap_or(u32::MAX),
character: 0,
};
let edit_end = Position {
line: u32::try_from(end_line + 1).unwrap_or(u32::MAX),
character: 0,
};
Ok(Some(vec![TextEdit {
range: Range {
start: edit_start,
end: edit_end,
},
new_text,
}]))
}
async fn on_type_formatting(
&self,
params: DocumentOnTypeFormattingParams,
) -> Result<Option<Vec<TextEdit>>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let ch = ¶ms.ch;
let tab_size = params.options.tab_size;
let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
let text_present = store.get(&uri).is_some();
let docs = store.get_documents(&uri).cloned();
(docs, text_present)
} else {
return Ok(None);
};
if !text_present {
return Ok(None);
}
let edits = crate::editing::on_type_formatting::format_on_type(
docs.as_deref().unwrap_or(&[]),
position,
ch,
tab_size,
);
if edits.is_empty() {
return Ok(None);
}
Ok(Some(edits))
}
async fn semantic_tokens_full(
&self,
params: SemanticTokensParams,
) -> Result<Option<SemanticTokensResult>> {
let uri = params.text_document.uri;
let (text, docs) = if let Ok(store) = self.document_store.lock() {
let text = store.get(&uri).map(str::to_string);
let docs = store.get_documents(&uri).cloned();
(text, docs)
} else {
return Ok(None);
};
let Some(text) = text else {
return Ok(None);
};
let tokens = crate::analysis::semantic_tokens::semantic_tokens(
docs.as_deref().unwrap_or(&[]),
&text,
);
if tokens.is_empty() {
return Ok(None);
}
Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
result_id: None,
data: tokens,
})))
}
async fn document_symbol(
&self,
params: DocumentSymbolParams,
) -> Result<Option<DocumentSymbolResponse>> {
let uri = params.text_document.uri;
let (text, docs) = if let Ok(store) = self.document_store.lock() {
let text = store.get(&uri).map(str::to_string);
let docs = store.get_documents(&uri).cloned();
(text, docs)
} else {
return Ok(None);
};
let Some(_text) = text else {
return Ok(None);
};
let mut symbols =
crate::analysis::symbols::document_symbols(docs.as_deref().unwrap_or(&[]));
let limit = self.get_max_items_computed();
symbols.truncate(limit);
if symbols.is_empty() {
return Ok(None);
}
Ok(Some(DocumentSymbolResponse::Nested(symbols)))
}
async fn prepare_rename(
&self,
params: tower_lsp::lsp_types::TextDocumentPositionParams,
) -> Result<Option<PrepareRenameResponse>> {
let uri = params.text_document.uri;
let position = params.position;
let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
let text_present = store.get(&uri).is_some();
let docs = store.get_documents(&uri).cloned();
(docs, text_present)
} else {
return Ok(None);
};
if !text_present {
return Ok(None);
}
let Some(docs) = docs else {
return Ok(None);
};
let range = crate::navigation::rename::prepare_rename(&docs, position);
Ok(range.map(PrepareRenameResponse::Range))
}
async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let new_name = params.new_name;
let (docs, text_present) = if let Ok(store) = self.document_store.lock() {
let text_present = store.get(&uri).is_some();
let docs = store.get_documents(&uri).cloned();
(docs, text_present)
} else {
return Ok(None);
};
if !text_present {
return Ok(None);
}
let Some(docs) = docs else {
return Ok(None);
};
Ok(crate::navigation::rename::rename(
&docs, &uri, position, &new_name,
))
}
async fn document_color(&self, params: DocumentColorParams) -> Result<Vec<ColorInformation>> {
if !self.get_color_decorators_enabled() {
return Ok(Vec::new());
}
let uri = params.text_document.uri;
let text = if let Ok(store) = self.document_store.lock() {
store.get(&uri).map(str::to_string)
} else {
return Ok(Vec::new());
};
let Some(text) = text else {
return Ok(Vec::new());
};
let docs = crate::parser::parse_yaml(&text).documents;
let colors = crate::decorators::color::find_colors(&docs)
.into_iter()
.map(|m| ColorInformation {
range: m.range,
color: m.color,
})
.collect();
Ok(colors)
}
async fn color_presentation(
&self,
params: ColorPresentationParams,
) -> Result<Vec<ColorPresentation>> {
Ok(crate::decorators::color::color_presentations(params.color))
}
}
#[cfg(test)]
#[expect(
clippy::indexing_slicing,
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
reason = "test code"
)]
mod tests {
use super::*;
use tower_lsp::lsp_types::{
HoverProviderCapability, OneOf, TextDocumentSyncCapability, TextDocumentSyncKind,
};
#[test]
fn should_advertise_full_text_document_sync() {
let caps = Backend::capabilities();
assert_eq!(
caps.text_document_sync,
Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL))
);
}
#[test]
fn should_advertise_hover_provider() {
let caps = Backend::capabilities();
assert_eq!(
caps.hover_provider,
Some(HoverProviderCapability::Simple(true))
);
}
#[test]
fn should_advertise_document_symbol_provider() {
let caps = Backend::capabilities();
assert!(matches!(
caps.document_symbol_provider,
Some(OneOf::Left(true))
));
}
#[test]
fn should_advertise_completion_provider() {
let caps = Backend::capabilities();
assert!(
caps.completion_provider.is_some(),
"capabilities should include completion_provider"
);
}
#[test]
fn should_advertise_definition_provider() {
let caps = Backend::capabilities();
assert!(
caps.definition_provider.is_some(),
"capabilities should include definition_provider"
);
}
#[test]
fn should_advertise_references_provider() {
let caps = Backend::capabilities();
assert!(
caps.references_provider.is_some(),
"capabilities should include references_provider"
);
}
#[test]
fn should_advertise_folding_range_provider() {
let caps = Backend::capabilities();
assert!(
caps.folding_range_provider.is_some(),
"capabilities should include folding_range_provider"
);
}
#[test]
fn should_advertise_selection_range_provider() {
let caps = Backend::capabilities();
assert!(
caps.selection_range_provider.is_some(),
"capabilities should include selection_range_provider"
);
}
#[test]
fn should_advertise_rename_provider_with_prepare_support() {
let caps = Backend::capabilities();
assert!(
caps.rename_provider.is_some(),
"capabilities should include rename_provider"
);
if let Some(OneOf::Right(rename_opts)) = caps.rename_provider {
assert_eq!(
rename_opts.prepare_provider,
Some(true),
"rename_provider should have prepare_provider set to true"
);
} else {
panic!("rename_provider should be RenameOptions with prepare_provider");
}
}
#[test]
fn should_advertise_document_link_provider() {
let caps = Backend::capabilities();
assert!(
caps.document_link_provider.is_some(),
"capabilities should include document_link_provider"
);
}
#[test]
fn settings_deserializes_custom_tags_from_json() {
let json = serde_json::json!({"customTags": ["!include", "!ref"]});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.custom_tags, vec!["!include", "!ref"]);
}
#[test]
fn settings_defaults_to_empty_custom_tags_when_field_missing() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.custom_tags.is_empty());
}
#[test]
fn settings_accepts_empty_custom_tags_array() {
let json = serde_json::json!({"customTags": []});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.custom_tags.is_empty());
}
#[test]
fn settings_deserializes_key_ordering_true() {
let json = serde_json::json!({"keyOrdering": true});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.key_ordering);
}
#[test]
fn settings_defaults_key_ordering_to_false_when_missing() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(!settings.key_ordering);
}
#[test]
fn settings_deserializes_schemas_from_json() {
let json = serde_json::json!({"schemas": {"https://example.com/schema.json": "*.yaml"}});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(
settings
.schemas
.get("https://example.com/schema.json")
.map(String::as_str),
Some("*.yaml")
);
}
#[test]
fn settings_defaults_to_empty_schemas_when_missing() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.schemas.is_empty());
}
#[test]
fn get_schema_associations_returns_empty_by_default() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert!(backend.get_schema_associations().is_empty());
}
#[test]
fn get_schema_associations_converts_settings_to_vec() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"schemas": {"https://example.com/schema.json": "*.yaml"}});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
let associations = backend.get_schema_associations();
assert_eq!(associations.len(), 1);
assert_eq!(associations[0].url, "https://example.com/schema.json");
assert_eq!(associations[0].pattern, "*.yaml");
}
#[test]
fn default_kubernetes_version_is_master() {
assert_eq!(DEFAULT_KUBERNETES_VERSION, "master");
}
#[test]
fn settings_deserializes_kubernetes_version() {
let json = serde_json::json!({"kubernetesVersion": "1.29.0"});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.kubernetes_version.as_deref(), Some("1.29.0"));
}
#[test]
fn settings_defaults_kubernetes_version_to_none_when_missing() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.kubernetes_version.is_none());
}
#[test]
fn get_kubernetes_version_returns_default_when_not_configured() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert_eq!(backend.get_kubernetes_version(), DEFAULT_KUBERNETES_VERSION);
}
#[test]
fn get_kubernetes_version_returns_configured_value() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"kubernetesVersion": "1.29.0"});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert_eq!(backend.get_kubernetes_version(), "1.29.0");
}
#[test]
fn settings_deserializes_yaml_version() {
let json = serde_json::json!({"yamlVersion": "1.1"});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.yaml_version.as_deref(), Some("1.1"));
}
#[test]
fn settings_defaults_yaml_version_to_none_when_missing() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.yaml_version.is_none());
}
#[test]
fn get_yaml_version_returns_v1_2_when_nothing_configured() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert_eq!(backend.get_yaml_version("key: value\n"), YamlVersion::V1_2);
}
#[test]
fn get_yaml_version_returns_v1_1_when_setting_is_1_1() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"yamlVersion": "1.1"});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert_eq!(backend.get_yaml_version("key: value\n"), YamlVersion::V1_1);
}
#[test]
fn get_yaml_version_returns_v1_2_when_setting_is_1_2() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"yamlVersion": "1.2"});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert_eq!(backend.get_yaml_version("key: value\n"), YamlVersion::V1_2);
}
#[test]
fn get_yaml_version_returns_v1_1_from_modeline_overriding_v1_2_setting() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"yamlVersion": "1.2"});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
let text = "# yaml-language-server: $yamlVersion=1.1\nkey: value\n";
assert_eq!(backend.get_yaml_version(text), YamlVersion::V1_1);
}
#[test]
fn get_yaml_version_returns_v1_2_from_modeline_overriding_v1_1_setting() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"yamlVersion": "1.1"});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
let text = "# yaml-language-server: $yamlVersion=1.2\nkey: value\n";
assert_eq!(backend.get_yaml_version(text), YamlVersion::V1_2);
}
#[test]
fn get_yaml_version_returns_v1_2_default_when_setting_has_invalid_value() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"yamlVersion": "2.0"});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert_eq!(backend.get_yaml_version("key: value\n"), YamlVersion::V1_2);
}
#[test]
fn get_yaml_version_ignores_invalid_modeline_and_uses_setting() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"yamlVersion": "1.1"});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
let text = "# yaml-language-server: $yamlVersion=2.0\nkey: value\n";
assert_eq!(backend.get_yaml_version(text), YamlVersion::V1_1);
}
#[test]
fn get_custom_tags_returns_empty_vec_by_default() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert!(backend.get_custom_tags().is_empty());
}
#[test]
fn should_advertise_code_action_provider() {
let caps = Backend::capabilities();
assert!(
caps.code_action_provider.is_some(),
"capabilities should include code_action_provider"
);
}
#[test]
fn should_advertise_code_lens_provider() {
let caps = Backend::capabilities();
assert!(caps.code_lens_provider.is_some());
}
#[test]
fn should_advertise_on_type_formatting_provider() {
let caps = Backend::capabilities();
assert!(caps.document_on_type_formatting_provider.is_some());
}
#[test]
fn should_advertise_semantic_tokens_provider() {
let caps = Backend::capabilities();
assert!(
caps.semantic_tokens_provider.is_some(),
"capabilities should include semantic_tokens_provider"
);
}
#[test]
fn settings_deserializes_schema_store_true() {
let json = serde_json::json!({"schemaStore": true});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.schema_store, Some(true));
}
#[test]
fn settings_deserializes_schema_store_false() {
let json = serde_json::json!({"schemaStore": false});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.schema_store, Some(false));
}
#[test]
fn settings_defaults_schema_store_to_none_when_missing() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.schema_store, None);
}
#[test]
fn get_schema_store_enabled_returns_true_by_default() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert!(backend.get_schema_store_enabled());
}
#[test]
fn get_schema_store_enabled_returns_false_when_disabled() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"schemaStore": false});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert!(!backend.get_schema_store_enabled());
}
#[test]
fn get_schema_store_enabled_returns_true_when_explicitly_enabled() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({"schemaStore": true});
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert!(backend.get_schema_store_enabled());
}
#[test]
fn settings_deserializes_format_print_width() {
let json = serde_json::json!({ "formatPrintWidth": 120 });
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.format_print_width, Some(120));
}
#[test]
fn settings_deserializes_format_single_quote() {
let json = serde_json::json!({ "formatSingleQuote": true });
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.format_single_quote, Some(true));
}
#[test]
fn settings_deserializes_format_preserve_quotes() {
let json = serde_json::json!({ "formatPreserveQuotes": true });
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.format_preserve_quotes, Some(true));
}
#[test]
fn settings_format_fields_default_to_none_when_absent() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.format_print_width, None);
assert_eq!(settings.format_single_quote, None);
}
#[test]
fn settings_deserializes_http_proxy() {
let json = serde_json::json!({ "httpProxy": "http://proxy.corp:8080" });
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(
settings.http_proxy.as_deref(),
Some("http://proxy.corp:8080")
);
}
#[test]
fn settings_defaults_http_proxy_to_none_when_absent() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.http_proxy.is_none());
}
#[test]
fn get_http_proxy_returns_none_by_default() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert!(backend.get_http_proxy().is_none());
}
#[test]
fn get_http_proxy_returns_configured_value() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({ "httpProxy": "http://proxy.corp:8080" });
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert_eq!(
backend.get_http_proxy().as_deref(),
Some("http://proxy.corp:8080")
);
}
#[test]
fn should_advertise_document_formatting_provider() {
let caps = Backend::capabilities();
assert!(
caps.document_formatting_provider.is_some(),
"capabilities should include document_formatting_provider"
);
}
#[test]
fn should_advertise_document_range_formatting_provider() {
let caps = Backend::capabilities();
assert!(
caps.document_range_formatting_provider.is_some(),
"capabilities should include document_range_formatting_provider"
);
}
#[tokio::test]
async fn range_formatting_returns_none_when_range_already_formatted() {
use tower_lsp::lsp_types::{
DocumentRangeFormattingParams, FormattingOptions, TextDocumentIdentifier,
WorkDoneProgressParams,
};
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let uri = Url::parse("file:///test.yaml").unwrap();
if let Ok(mut store) = backend.document_store.lock() {
store.open(uri.clone(), "key: value\nother: 1\n".to_string());
}
let params = DocumentRangeFormattingParams {
text_document: TextDocumentIdentifier { uri },
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
},
options: FormattingOptions {
tab_size: 2,
insert_spaces: true,
..FormattingOptions::default()
},
work_done_progress_params: WorkDoneProgressParams::default(),
};
let result = LanguageServer::range_formatting(backend, params)
.await
.unwrap();
assert!(
result.is_none(),
"already-formatted range should return None"
);
}
#[tokio::test]
async fn range_formatting_returns_edit_scoped_to_requested_lines() {
use tower_lsp::lsp_types::{
DocumentRangeFormattingParams, FormattingOptions, TextDocumentIdentifier,
WorkDoneProgressParams,
};
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let uri = Url::parse("file:///test.yaml").unwrap();
let text = "a: 1\nb: 2\nc: 3\n";
if let Ok(mut store) = backend.document_store.lock() {
store.open(uri.clone(), text.to_string());
}
let params = DocumentRangeFormattingParams {
text_document: TextDocumentIdentifier { uri },
range: Range {
start: Position {
line: 1,
character: 0,
},
end: Position {
line: 1,
character: 0,
},
},
options: FormattingOptions {
tab_size: 2,
insert_spaces: true,
..FormattingOptions::default()
},
work_done_progress_params: WorkDoneProgressParams::default(),
};
let result = LanguageServer::range_formatting(backend, params)
.await
.unwrap();
let edits = result.expect("formatter must produce an edit for `b: 2`");
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].range.start.line, 1, "edit start must be line 1");
assert_eq!(
edits[0].range.end.line, 2,
"edit end must be line 2 (exclusive)"
);
assert!(
edits[0].new_text.contains("b: 2"),
"formatted line must normalise double-space: {:?}",
edits[0].new_text
);
}
#[tokio::test]
async fn formatting_handler_uses_v1_1_from_modeline() {
use tower_lsp::lsp_types::{
DocumentFormattingParams, FormattingOptions, TextDocumentIdentifier,
WorkDoneProgressParams,
};
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let uri = Url::parse("file:///test.yaml").unwrap();
let text = "# yaml-language-server: $yamlVersion=1.1\nvalue: \"yes\"\n";
if let Ok(mut store) = backend.document_store.lock() {
store.open(uri.clone(), text.to_string());
}
let params = DocumentFormattingParams {
text_document: TextDocumentIdentifier { uri },
options: FormattingOptions {
tab_size: 2,
insert_spaces: true,
..FormattingOptions::default()
},
work_done_progress_params: WorkDoneProgressParams::default(),
};
let result = LanguageServer::formatting(backend, params).await.unwrap();
let edits = result.expect("formatter must produce an edit for double-space input");
let new_text = edits
.iter()
.map(|e| e.new_text.as_str())
.collect::<String>();
assert!(
new_text.contains("\"yes\""),
"V1.1 modeline: yes must stay quoted in formatted output: {new_text:?}"
);
}
#[test]
fn should_advertise_color_provider() {
let caps = Backend::capabilities();
assert!(
caps.color_provider.is_some(),
"capabilities should include color_provider"
);
}
#[test]
fn settings_deserializes_color_decorators_false() {
let json = serde_json::json!({ "colorDecorators": false });
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.color_decorators, Some(false));
}
#[test]
fn settings_defaults_color_decorators_to_none_when_absent() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.color_decorators.is_none());
}
#[test]
fn get_color_decorators_enabled_returns_true_by_default() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert!(backend.get_color_decorators_enabled());
}
#[test]
fn get_color_decorators_enabled_returns_false_when_disabled() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({ "colorDecorators": false });
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert!(!backend.get_color_decorators_enabled());
}
#[tokio::test]
async fn document_color_returns_colors_for_yaml_with_hex_values() {
use tower_lsp::lsp_types::{
PartialResultParams, TextDocumentIdentifier, WorkDoneProgressParams,
};
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let uri = Url::parse("file:///test.yaml").unwrap();
if let Ok(mut store) = backend.document_store.lock() {
store.open(uri.clone(), "color: '#ff0000'\n".to_string());
}
let params = DocumentColorParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let colors = LanguageServer::document_color(backend, params)
.await
.unwrap();
assert!(!colors.is_empty(), "should detect hex color in YAML value");
}
#[tokio::test]
async fn document_color_returns_empty_when_color_decorators_disabled() {
use tower_lsp::lsp_types::{
PartialResultParams, TextDocumentIdentifier, WorkDoneProgressParams,
};
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let json = serde_json::json!({ "colorDecorators": false });
let new_settings: Settings = serde_json::from_value(json).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
let uri = Url::parse("file:///test.yaml").unwrap();
if let Ok(mut store) = backend.document_store.lock() {
store.open(uri.clone(), "color: '#ff0000'\n".to_string());
}
let params = DocumentColorParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
};
let colors = LanguageServer::document_color(backend, params)
.await
.unwrap();
assert!(
colors.is_empty(),
"should return empty when color decorators are disabled"
);
}
#[test]
fn settings_deserializes_validate_false() {
let json = serde_json::json!({"validate": false});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.validate, Some(false));
}
#[test]
fn settings_deserializes_validate_true() {
let json = serde_json::json!({"validate": true});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.validate, Some(true));
}
#[test]
fn settings_defaults_validate_to_none_when_missing() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.validate.is_none());
}
#[test]
fn get_validate_returns_true_by_default() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert!(backend.get_validate());
}
#[test]
fn get_validate_returns_false_when_disabled() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let new_settings: Settings =
serde_json::from_value(serde_json::json!({"validate": false})).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert!(!backend.get_validate());
}
#[test]
fn get_validate_returns_true_when_explicitly_enabled() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let new_settings: Settings =
serde_json::from_value(serde_json::json!({"validate": true})).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert!(backend.get_validate());
}
#[test]
fn settings_deserializes_hover_false() {
let json = serde_json::json!({"hover": false});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.hover, Some(false));
}
#[test]
fn settings_defaults_hover_to_none_when_missing() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.hover.is_none());
}
#[test]
fn get_hover_enabled_returns_true_by_default() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert!(backend.get_hover_enabled());
}
#[test]
fn get_hover_enabled_returns_false_when_disabled() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let new_settings: Settings =
serde_json::from_value(serde_json::json!({"hover": false})).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert!(!backend.get_hover_enabled());
}
#[test]
fn settings_deserializes_completion_false() {
let json = serde_json::json!({"completion": false});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.completion, Some(false));
}
#[test]
fn settings_defaults_completion_to_none_when_missing() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.completion.is_none());
}
#[test]
fn get_completion_enabled_returns_true_by_default() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert!(backend.get_completion_enabled());
}
#[test]
fn get_completion_enabled_returns_false_when_disabled() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let new_settings: Settings =
serde_json::from_value(serde_json::json!({"completion": false})).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert!(!backend.get_completion_enabled());
}
#[test]
fn settings_deserializes_max_items_computed() {
let json = serde_json::json!({"maxItemsComputed": 100});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.max_items_computed, Some(100));
}
#[test]
fn settings_defaults_max_items_computed_to_none_when_absent() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.max_items_computed.is_none());
}
#[test]
fn settings_accepts_max_items_computed_zero() {
let json = serde_json::json!({"maxItemsComputed": 0});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.max_items_computed, Some(0));
}
#[test]
fn get_max_items_computed_returns_5000_when_setting_absent() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert_eq!(backend.get_max_items_computed(), 5000);
}
#[test]
fn get_max_items_computed_returns_configured_value() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let new_settings: Settings =
serde_json::from_value(serde_json::json!({"maxItemsComputed": 100})).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert_eq!(backend.get_max_items_computed(), 100);
}
#[test]
fn get_max_items_computed_returns_5000_when_field_is_none() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let new_settings: Settings = serde_json::from_value(serde_json::json!({})).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert_eq!(backend.get_max_items_computed(), 5000);
}
#[test]
fn get_max_items_computed_returns_zero_when_configured_to_zero() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let new_settings: Settings =
serde_json::from_value(serde_json::json!({"maxItemsComputed": 0})).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert_eq!(backend.get_max_items_computed(), 0);
}
#[test]
fn settings_deserializes_duplicate_keys_off() {
let json = serde_json::json!({"duplicateKeys": "off"});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.duplicate_keys.as_deref(), Some("off"));
}
#[test]
fn settings_deserializes_duplicate_keys_warning() {
let json = serde_json::json!({"duplicateKeys": "warning"});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.duplicate_keys.as_deref(), Some("warning"));
}
#[test]
fn settings_deserializes_duplicate_keys_error() {
let json = serde_json::json!({"duplicateKeys": "error"});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.duplicate_keys.as_deref(), Some("error"));
}
#[test]
fn settings_defaults_duplicate_keys_to_none_when_absent() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.duplicate_keys.is_none());
}
#[test]
fn get_duplicate_keys_returns_none_by_default() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
assert!(backend.get_duplicate_keys().is_none());
}
#[test]
fn get_duplicate_keys_returns_configured_value() {
let (service, _) = tower_lsp::LspService::new(Backend::new);
let backend = service.inner();
let new_settings: Settings =
serde_json::from_value(serde_json::json!({"duplicateKeys": "warning"})).unwrap();
if let Ok(mut s) = backend.settings.lock() {
*s = new_settings;
}
assert_eq!(backend.get_duplicate_keys().as_deref(), Some("warning"));
}
#[test]
fn settings_deserializes_format_remove_duplicate_keys_true() {
let json = serde_json::json!({"formatRemoveDuplicateKeys": true});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.format_remove_duplicate_keys, Some(true));
}
#[test]
fn settings_deserializes_format_remove_duplicate_keys_false() {
let json = serde_json::json!({"formatRemoveDuplicateKeys": false});
let settings: Settings = serde_json::from_value(json).unwrap();
assert_eq!(settings.format_remove_duplicate_keys, Some(false));
}
#[test]
fn settings_defaults_format_remove_duplicate_keys_to_none_when_absent() {
let json = serde_json::json!({});
let settings: Settings = serde_json::from_value(json).unwrap();
assert!(settings.format_remove_duplicate_keys.is_none());
}
}