use crate::{editor, server::ServerState};
use anyhow::Context;
use lsp_types::{Position, Range, SelectionRange, SelectionRangeParams};
pub fn handle(
state: &ServerState,
params: SelectionRangeParams,
) -> anyhow::Result<Option<Vec<SelectionRange>>> {
if let Some(document) = state.documents.get(params.text_document.uri.as_str()) {
let editor_buffer = editor::from_source(&document.text).with_context(|| {
format!(
"failed to analyze document `{:?}`",
params.text_document.uri
)
})?;
Ok(Some(
params
.positions
.iter()
.filter_map(|position| selection_range_for_position(&editor_buffer, *position))
.collect::<Vec<_>>(),
))
} else {
Ok(None)
}
}
fn selection_range_for_position(
analysis: &editor::EditorBuffer,
position: Position,
) -> Option<SelectionRange> {
let position = achitekfile::TextPosition {
line: usize::try_from(position.line).ok()?,
byte: usize::try_from(position.character).ok()?,
};
let mut candidates = Vec::new();
for symbol in analysis.symbols() {
collect_selection_candidates(symbol, position, &mut candidates);
}
candidates.sort_by_key(|range| {
(
range.end.line.saturating_sub(range.start.line),
range.end.byte.saturating_sub(range.start.byte),
)
});
let mut current = None;
for range in candidates.into_iter().rev() {
current = Some(SelectionRange {
range: to_lsp_range(range),
parent: current.map(Box::new),
});
}
current
}
fn collect_selection_candidates(
symbol: &editor::Symbol,
position: achitekfile::TextPosition,
candidates: &mut Vec<achitekfile::TextRange>,
) {
if contains_position(symbol.selection_range(), position) {
candidates.push(symbol.selection_range());
}
if contains_position(symbol.range(), position) {
candidates.push(symbol.range());
}
for child in symbol.children() {
collect_selection_candidates(child, position, candidates);
}
}
fn contains_position(range: achitekfile::TextRange, position: achitekfile::TextPosition) -> bool {
(position.line > range.start.line
|| (position.line == range.start.line && position.byte >= range.start.byte))
&& (position.line < range.end.line
|| (position.line == range.end.line && position.byte <= range.end.byte))
}
fn to_lsp_range(range: achitekfile::TextRange) -> Range {
Range {
start: to_lsp_position(range.start),
end: to_lsp_position(range.end),
}
}
fn to_lsp_position(position: achitekfile::TextPosition) -> Position {
Position {
line: u32::try_from(position.line).expect("line should fit into u32"),
character: u32::try_from(position.byte).expect("column should fit into u32"),
}
}