use nautilus_schema::{
analysis::{CompletionItem, CompletionKind, HoverInfo, SemanticKind, SemanticToken},
diagnostic::{Diagnostic, Severity},
Span,
};
use tower_lsp::lsp_types::{
self, CompletionItemKind, DiagnosticSeverity, InsertTextFormat, Position, Range,
SemanticToken as LspSemanticToken,
};
pub fn offset_to_position(source: &str, offset: usize) -> Position {
let safe = offset.min(source.len());
let prefix = &source[..safe];
let line = prefix.bytes().filter(|&b| b == b'\n').count() as u32;
let last_nl = prefix.rfind('\n').map(|i| i + 1).unwrap_or(0);
let character = prefix[last_nl..].chars().count() as u32;
Position { line, character }
}
pub fn position_to_offset(source: &str, pos: Position) -> usize {
let mut current_line = 0u32;
let mut line_start = 0usize;
for (i, ch) in source.char_indices() {
if current_line == pos.line {
break;
}
if ch == '\n' {
current_line += 1;
line_start = i + ch.len_utf8();
}
}
let rest = &source[line_start..];
let byte_offset = rest
.char_indices()
.nth(pos.character as usize)
.map(|(i, _)| i)
.unwrap_or(rest.len());
(line_start + byte_offset).min(source.len())
}
pub fn span_to_range(source: &str, span: &Span) -> Range {
Range {
start: offset_to_position(source, span.start),
end: offset_to_position(source, span.end),
}
}
pub fn nautilus_diagnostic_to_lsp(source: &str, d: &Diagnostic) -> lsp_types::Diagnostic {
let severity = match d.severity {
Severity::Error => DiagnosticSeverity::ERROR,
Severity::Warning => DiagnosticSeverity::WARNING,
};
lsp_types::Diagnostic {
range: span_to_range(source, &d.span),
severity: Some(severity),
message: d.message.clone(),
source: Some("nautilus-schema".to_string()),
..Default::default()
}
}
pub fn nautilus_completion_to_lsp(item: &CompletionItem) -> lsp_types::CompletionItem {
let kind = match item.kind {
CompletionKind::Keyword => CompletionItemKind::KEYWORD,
CompletionKind::Type => CompletionItemKind::CLASS,
CompletionKind::FieldAttribute => CompletionItemKind::PROPERTY,
CompletionKind::ModelAttribute => CompletionItemKind::PROPERTY,
CompletionKind::ModelName => CompletionItemKind::STRUCT,
CompletionKind::EnumName => CompletionItemKind::ENUM,
CompletionKind::FieldName => CompletionItemKind::FIELD,
};
lsp_types::CompletionItem {
label: item.label.clone(),
kind: Some(kind),
detail: item.detail.clone(),
insert_text: item.insert_text.clone(),
insert_text_format: if item.is_snippet {
Some(InsertTextFormat::SNIPPET)
} else {
None
},
..Default::default()
}
}
pub fn hover_info_to_lsp(source: &str, h: &HoverInfo) -> lsp_types::Hover {
let range = h.span.as_ref().map(|s| span_to_range(source, s));
lsp_types::Hover {
contents: lsp_types::HoverContents::Markup(lsp_types::MarkupContent {
kind: lsp_types::MarkupKind::Markdown,
value: h.content.clone(),
}),
range,
}
}
pub fn semantic_tokens_to_lsp(source: &str, tokens: &[SemanticToken]) -> Vec<LspSemanticToken> {
let mut result = Vec::with_capacity(tokens.len());
let mut prev_line = 0u32;
let mut prev_start = 0u32;
for token in tokens {
let pos = offset_to_position(source, token.span.start);
let length = (token.span.end - token.span.start) as u32;
let delta_line = pos.line - prev_line;
let delta_start = if delta_line == 0 {
pos.character - prev_start
} else {
pos.character
};
let token_type = match token.kind {
SemanticKind::ModelRef => 0,
SemanticKind::EnumRef => 1,
SemanticKind::CompositeTypeRef => 2,
};
result.push(LspSemanticToken {
delta_line,
delta_start,
length,
token_type,
token_modifiers_bitset: 0,
});
prev_line = pos.line;
prev_start = pos.character;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_offset_position() {
let source = "model User {\n id Int\n name String\n}";
let pos = offset_to_position(source, 13);
assert_eq!(
pos,
Position {
line: 1,
character: 0
}
);
let back = position_to_offset(source, pos);
assert_eq!(back, 13);
}
#[test]
fn offset_to_position_at_col() {
let source = "model User {\n id Int\n}";
let pos = offset_to_position(source, 5);
assert_eq!(
pos,
Position {
line: 0,
character: 5
}
);
}
}