#![expect(missing_docs, reason = "test code")]
#![expect(
dead_code,
reason = "common module has helpers not used by every test file"
)]
#![expect(
clippy::cast_possible_truncation,
reason = "test code — LSP line/col counts fit in u32 for any real YAML file"
)]
mod common;
use common::*;
use proptest::prelude::*;
use rlsp_yaml::editing::code_actions::code_actions;
use rlsp_yaml::editing::formatter::YamlFormatOptions;
use tower_lsp::lsp_types::{Position, Range};
fn whole_file_range(text: &str) -> Range {
let lines: Vec<&str> = text.lines().collect();
let last_line = lines.len().saturating_sub(1) as u32;
let last_char = lines.last().map_or(0, |l| l.len() as u32);
Range::new(Position::new(0, 0), Position::new(last_line, last_char))
}
fn apply_action_edit(
text: &str,
action: &tower_lsp::lsp_types::CodeAction,
uri: &tower_lsp::lsp_types::Url,
) -> Option<String> {
let edit = action.edit.as_ref()?;
let changes = edit.changes.as_ref()?;
let text_edits = changes.get(uri)?;
let first_edit = text_edits.first()?;
Some(apply_text_edit(text, first_edit))
}
fn simple_block_mapping() -> impl Strategy<Value = String> {
prop_oneof![
Just("enabled: \"true\"\n".to_string()),
Just("enabled: 'false'\n".to_string()),
Just("debug: \"true\"\nverbose: 'false'\n".to_string()),
Just("items:\n - one\n - two\n".to_string()),
Just("config:\n host: localhost\n port: \"8080\"\n".to_string()),
Just("description: this is a very long plain scalar value that exceeds forty chars for sure\n".to_string()),
Just("\tkey: value\n".to_string()),
]
}
fn simple_flow_yaml() -> impl Strategy<Value = String> {
prop_oneof![
Just("config: {enabled: \"true\"}\n".to_string()),
Just("items: [\"true\", \"false\"]\n".to_string()),
Just("items: [one, two]\n".to_string()),
]
}
fn yaml_strategy() -> impl Strategy<Value = String> {
prop_oneof![simple_block_mapping(), simple_flow_yaml(),]
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 64,
..ProptestConfig::default()
})]
#[test]
fn code_action_second_application_is_noop(input in yaml_strategy()) {
let uri = test_uri();
let opts = YamlFormatOptions::default();
let docs = docs_for(&input);
let range = whole_file_range(&input);
let first_actions = code_actions(&docs, &input, range, &[], &uri, &opts);
for action in &first_actions {
let title = &action.title;
let Some(edited) = apply_action_edit(&input, action, &uri) else {
continue; };
let edited_docs = docs_for(&edited);
let edited_range = whole_file_range(&edited);
let second_actions = code_actions(&edited_docs, &edited, edited_range, &[], &uri, &opts);
let repeated = second_actions.iter().find(|a| &a.title == title);
prop_assert!(
repeated.is_none(),
"action {:?} was offered again after first application.\n \
input: {:?}\n edited: {:?}",
title, input, edited,
);
}
}
}