use lsp_types::{
DocumentColorParams, DocumentFormattingParams, DocumentSymbolParams, FoldingRangeParams,
InlayHintParams, Position, Range, SemanticTokensParams, TextDocumentIdentifier,
};
use rustledger_lsp::handlers::document_color::handle_document_color;
use rustledger_lsp::handlers::folding::handle_folding_ranges;
use rustledger_lsp::handlers::formatting::handle_formatting;
use rustledger_lsp::handlers::inlay_hints::handle_inlay_hints;
use rustledger_lsp::handlers::semantic_tokens::handle_semantic_tokens;
use rustledger_lsp::handlers::symbols::handle_document_symbols;
use rustledger_lsp::handlers::utils::{LineIndex, PositionEncoding};
use rustledger_parser::parse;
const SOURCE: &str = "\
2024-01-01 open Активы:Банк USD
2024-01-15 * \"Кофе ☕\"
Активы:Банк -5.00 USD
Расходы:Еда 5.00 USD
";
fn uri() -> lsp_types::Uri {
"file:///encoding-coverage.beancount".parse().unwrap()
}
fn assert_position_round_trips(pos: Position, source: &str, encoding: PositionEncoding) {
let idx = LineIndex::new(source, encoding);
let offset = idx.position_to_offset(pos.line, pos.character);
assert!(
offset.is_some(),
"Position {pos:?} under {encoding:?} doesn't round-trip into a byte offset \
— handler likely emitted a column in the wrong encoding (UTF-16 columns \
interpreted as UTF-8 bytes, or vice versa)"
);
}
fn assert_range_round_trips(range: Range, source: &str, encoding: PositionEncoding) {
assert_position_round_trips(range.start, source, encoding);
assert_position_round_trips(range.end, source, encoding);
}
#[test]
fn formatting_positions_round_trip_under_both_encodings() {
let parsed = parse(SOURCE);
let params = DocumentFormattingParams {
text_document: TextDocumentIdentifier { uri: uri() },
options: Default::default(),
work_done_progress_params: Default::default(),
};
for encoding in [PositionEncoding::Utf8, PositionEncoding::Utf16] {
if let Some(edits) = handle_formatting(¶ms, SOURCE, &parsed, encoding) {
for edit in edits {
assert_range_round_trips(edit.range, SOURCE, encoding);
}
}
}
}
#[test]
fn semantic_tokens_positions_round_trip_under_both_encodings() {
let parsed = parse(SOURCE);
let params = SemanticTokensParams {
text_document: TextDocumentIdentifier { uri: uri() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
for encoding in [PositionEncoding::Utf8, PositionEncoding::Utf16] {
let Some(result) = handle_semantic_tokens(¶ms, SOURCE, &parsed, encoding) else {
continue;
};
let tokens = match result {
lsp_types::SemanticTokensResult::Tokens(t) => t.data,
lsp_types::SemanticTokensResult::Partial(p) => p.data,
};
let mut line = 0u32;
let mut col = 0u32;
for tok in tokens {
if tok.delta_line != 0 {
line += tok.delta_line;
col = tok.delta_start;
} else {
col += tok.delta_start;
}
assert_position_round_trips(Position::new(line, col), SOURCE, encoding);
assert_position_round_trips(Position::new(line, col + tok.length), SOURCE, encoding);
}
}
}
#[test]
fn document_symbols_ranges_round_trip_under_both_encodings() {
let parsed = parse(SOURCE);
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri: uri() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
for encoding in [PositionEncoding::Utf8, PositionEncoding::Utf16] {
let Some(response) = handle_document_symbols(¶ms, SOURCE, &parsed, encoding) else {
continue;
};
let symbols = match response {
lsp_types::DocumentSymbolResponse::Flat(s) => s
.into_iter()
.map(|si| si.location.range)
.collect::<Vec<_>>(),
lsp_types::DocumentSymbolResponse::Nested(s) => s
.into_iter()
.flat_map(|ds| vec![ds.range, ds.selection_range])
.collect::<Vec<_>>(),
};
for range in symbols {
assert_range_round_trips(range, SOURCE, encoding);
}
}
}
#[test]
fn document_color_ranges_round_trip_under_both_encodings() {
let parsed = parse(SOURCE);
let params = DocumentColorParams {
text_document: TextDocumentIdentifier { uri: uri() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
for encoding in [PositionEncoding::Utf8, PositionEncoding::Utf16] {
let Some(colors) = handle_document_color(¶ms, SOURCE, &parsed, encoding) else {
continue;
};
for color in colors {
assert_range_round_trips(color.range, SOURCE, encoding);
}
}
}
#[test]
fn inlay_hints_positions_round_trip_under_both_encodings() {
let parsed = parse(SOURCE);
let params = InlayHintParams {
text_document: TextDocumentIdentifier { uri: uri() },
range: Range::new(Position::new(0, 0), Position::new(100, 0)),
work_done_progress_params: Default::default(),
};
for encoding in [PositionEncoding::Utf8, PositionEncoding::Utf16] {
let Some(hints) = handle_inlay_hints(¶ms, SOURCE, &parsed, encoding) else {
continue;
};
for hint in hints {
assert_position_round_trips(hint.position, SOURCE, encoding);
}
}
}
#[test]
fn folding_ranges_emit_line_numbers_within_source_under_both_encodings() {
let parsed = parse(SOURCE);
let params = FoldingRangeParams {
text_document: TextDocumentIdentifier { uri: uri() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let line_count = SOURCE.lines().count() as u32;
for encoding in [PositionEncoding::Utf8, PositionEncoding::Utf16] {
if let Some(ranges) = handle_folding_ranges(¶ms, SOURCE, &parsed, encoding) {
for r in ranges {
assert!(r.start_line <= line_count, "fold start_line out of bounds");
assert!(r.end_line <= line_count, "fold end_line out of bounds");
}
}
}
}