use eure::query::{
CompletionItem, CompletionKind, DiagnosticMessage, DiagnosticSeverity, GetFileDiagnostics,
GetSemanticTokens, SemanticToken, TextFile, get_completions, get_hover,
};
use lsp_types::{
CompletionItem as LspCompletionItem, CompletionItemKind, CompletionTextEdit, Diagnostic,
DiagnosticSeverity as LspSeverity, Documentation, Hover, HoverContents, MarkupContent,
MarkupKind, NumberOrString, Position, Range, SemanticToken as LspSemanticToken, SemanticTokens,
TextEdit,
};
use query_flow::{Db, QueryError, query};
pub fn lsp_completion(
db: &impl Db,
file: &TextFile,
offset: u32,
) -> Result<Vec<LspCompletionItem>, QueryError> {
let items = get_completions(db, file, offset)?;
let source: std::sync::Arc<eure::query::TextFileContent> = db.asset(file.clone())?;
let line_offsets = compute_line_offsets(source.get());
Ok(items
.iter()
.map(|item| convert_completion_item(item, source.get(), &line_offsets))
.collect())
}
fn convert_completion_item(
item: &CompletionItem,
source: &str,
line_offsets: &[usize],
) -> LspCompletionItem {
let range = Range {
start: offset_to_lsp_position(item.replace.start as usize, source, line_offsets),
end: offset_to_lsp_position(item.replace.end as usize, source, line_offsets),
};
LspCompletionItem {
label: item.label.clone(),
kind: Some(convert_completion_kind(item.kind)),
detail: item.detail.clone(),
documentation: item.documentation.as_ref().map(|value| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: value.clone(),
})
}),
deprecated: item.deprecated.then_some(true),
filter_text: Some(item.label.clone()),
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
range,
new_text: item.label.clone(),
})),
..Default::default()
}
}
fn convert_completion_kind(kind: CompletionKind) -> CompletionItemKind {
match kind {
CompletionKind::Field => CompletionItemKind::FIELD,
CompletionKind::Extension => CompletionItemKind::PROPERTY,
CompletionKind::Variant => CompletionItemKind::ENUM_MEMBER,
CompletionKind::Value => CompletionItemKind::VALUE,
}
}
pub fn lsp_hover(db: &impl Db, file: &TextFile, offset: u32) -> Result<Option<Hover>, QueryError> {
let Some(hover) = get_hover(db, file, offset)? else {
return Ok(None);
};
let source: std::sync::Arc<eure::query::TextFileContent> = db.asset(file.clone())?;
let line_offsets = compute_line_offsets(source.get());
Ok(Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover.contents,
}),
range: Some(Range {
start: offset_to_lsp_position(hover.span.start as usize, source.get(), &line_offsets),
end: offset_to_lsp_position(hover.span.end as usize, source.get(), &line_offsets),
}),
}))
}
pub fn lsp_definition(
db: &impl Db,
file: &TextFile,
offset: u32,
) -> Result<Vec<lsp_types::LocationLink>, QueryError> {
let definitions = eure::query::get_definition(db, file, offset)?;
let source = db.asset(file.clone())?;
let offsets = compute_line_offsets(source.get());
definitions
.into_iter()
.map(|definition| {
let target = db.asset(definition.file.clone())?;
let target_offsets = compute_line_offsets(target.get());
Ok(lsp_types::LocationLink {
origin_selection_range: Some(Range {
start: offset_to_lsp_position(
definition.origin.start as usize,
source.get(),
&offsets,
),
end: offset_to_lsp_position(
definition.origin.end as usize,
source.get(),
&offsets,
),
}),
target_uri: crate::uri_utils::text_file_to_uri(&definition.file).parse()?,
target_range: Range {
start: offset_to_lsp_position(
definition.range.start as usize,
target.get(),
&target_offsets,
),
end: offset_to_lsp_position(
definition.range.end as usize,
target.get(),
&target_offsets,
),
},
target_selection_range: Range {
start: offset_to_lsp_position(
definition.selection.start as usize,
target.get(),
&target_offsets,
),
end: offset_to_lsp_position(
definition.selection.end as usize,
target.get(),
&target_offsets,
),
},
})
})
.collect()
}
pub fn position_to_offset(source: &str, position: Position) -> usize {
let line_offsets = compute_line_offsets(source);
let Some(&line_start) = line_offsets.get(position.line as usize) else {
return source.len();
};
let line_end = line_offsets
.get(position.line as usize + 1)
.map(|&next| next - 1)
.unwrap_or(source.len());
let line = &source[line_start..line_end];
let mut utf16_units = 0u32;
for (byte_index, c) in line.char_indices() {
if utf16_units >= position.character {
return line_start + byte_index;
}
utf16_units += c.len_utf16() as u32;
}
line_end
}
#[query]
pub fn lsp_semantic_tokens(
db: &impl Db,
file: TextFile,
source: String,
) -> Result<SemanticTokens, QueryError> {
let tokens = db.query(GetSemanticTokens::new(file.clone()))?;
Ok(convert_tokens(&tokens, &source))
}
#[query]
pub fn lsp_diagnostics(
db: &impl Db,
file: TextFile,
) -> Result<Vec<(TextFile, Vec<Diagnostic>)>, QueryError> {
let diagnostics = db.query(GetFileDiagnostics::new(file.clone()))?;
let mut by_file: std::collections::HashMap<TextFile, Vec<DiagnosticMessage>> =
std::collections::HashMap::new();
by_file.insert(file, vec![]);
for d in diagnostics.iter() {
by_file.entry(d.file.clone()).or_default().push(d.clone());
}
let mut result = Vec::new();
for (diag_file, file_diagnostics) in by_file {
let source: std::sync::Arc<eure::query::TextFileContent> = db.asset(diag_file.clone())?;
let line_offsets = compute_line_offsets(source.get());
let lsp_diagnostics: Vec<Diagnostic> = file_diagnostics
.iter()
.map(|d| convert_diagnostic(d, source.get(), &line_offsets))
.collect();
result.push((diag_file, lsp_diagnostics));
}
Ok(result)
}
#[query]
pub fn lsp_file_diagnostics(db: &impl Db, file: TextFile) -> Result<Vec<Diagnostic>, QueryError> {
let diagnostics = db.query(GetFileDiagnostics::new(file.clone()))?;
let source: std::sync::Arc<eure::query::TextFileContent> = db.asset(file.clone())?;
let line_offsets = compute_line_offsets(source.get());
let lsp_diagnostics: Vec<Diagnostic> = diagnostics
.iter()
.filter(|d| d.file == file) .map(|d| convert_diagnostic(d, source.get(), &line_offsets))
.collect();
Ok(lsp_diagnostics)
}
fn convert_tokens(tokens: &[SemanticToken], source: &str) -> SemanticTokens {
let line_offsets = compute_line_offsets(source);
let mut data = Vec::new();
let mut prev_line = 0u32;
let mut prev_start = 0u32;
for token in tokens {
let start = token.start as usize;
let end = start + token.length as usize;
let (line, char) = offset_to_position(start, source, &line_offsets);
let length = byte_len_to_utf16_len(source, start, end);
let delta_line = line - prev_line;
let delta_start = if delta_line == 0 {
char - prev_start
} else {
char
};
data.push(LspSemanticToken {
delta_line,
delta_start,
length,
token_type: token.token_type as u32,
token_modifiers_bitset: token.modifiers,
});
prev_line = line;
prev_start = char;
}
SemanticTokens {
result_id: None,
data,
}
}
fn convert_diagnostic(msg: &DiagnosticMessage, source: &str, line_offsets: &[usize]) -> Diagnostic {
let start = offset_to_lsp_position(msg.start, source, line_offsets);
let end = offset_to_lsp_position(msg.end, source, line_offsets);
Diagnostic {
range: Range { start, end },
severity: Some(convert_severity(msg.severity)),
code: msg.code.clone().map(NumberOrString::String),
code_description: None,
source: Some("eure".to_string()),
message: msg.message.clone(),
related_information: None,
tags: None,
data: None,
}
}
fn convert_severity(severity: DiagnosticSeverity) -> LspSeverity {
match severity {
DiagnosticSeverity::Error => LspSeverity::ERROR,
DiagnosticSeverity::Warning => LspSeverity::WARNING,
DiagnosticSeverity::Info => LspSeverity::INFORMATION,
DiagnosticSeverity::Hint => LspSeverity::HINT,
}
}
fn compute_line_offsets(source: &str) -> Vec<usize> {
let mut offsets = vec![0];
for (i, c) in source.char_indices() {
if c == '\n' {
offsets.push(i + 1);
}
}
offsets
}
fn offset_to_position(offset: usize, source: &str, line_offsets: &[usize]) -> (u32, u32) {
let line = line_offsets.iter().rposition(|&o| o <= offset).unwrap_or(0);
let line_start = line_offsets[line];
let end = offset.min(source.len());
let line_content = &source[line_start..end];
let utf16_offset: usize = line_content.chars().map(|c| c.len_utf16()).sum();
(line as u32, utf16_offset as u32)
}
fn offset_to_lsp_position(offset: usize, source: &str, line_offsets: &[usize]) -> Position {
let (line, character) = offset_to_position(offset, source, line_offsets);
Position { line, character }
}
fn byte_len_to_utf16_len(source: &str, start: usize, end: usize) -> u32 {
let end = end.min(source.len());
let start = start.min(end);
source[start..end]
.chars()
.map(|c| c.len_utf16())
.sum::<usize>() as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_line_offsets() {
let source = "hello\nworld\n";
let offsets = compute_line_offsets(source);
assert_eq!(offsets, vec![0, 6, 12]);
}
#[test]
fn test_offset_to_position_ascii() {
let source = "hello\nworld\n";
let offsets = compute_line_offsets(source);
assert_eq!(offset_to_position(0, source, &offsets), (0, 0));
assert_eq!(offset_to_position(5, source, &offsets), (0, 5));
assert_eq!(offset_to_position(6, source, &offsets), (1, 0));
assert_eq!(offset_to_position(11, source, &offsets), (1, 5));
}
#[test]
fn test_offset_to_position_utf8() {
let source = "日本語\ntest";
let offsets = compute_line_offsets(source);
assert_eq!(offset_to_position(0, source, &offsets), (0, 0));
assert_eq!(offset_to_position(3, source, &offsets), (0, 1));
assert_eq!(offset_to_position(6, source, &offsets), (0, 2));
assert_eq!(offset_to_position(9, source, &offsets), (0, 3));
assert_eq!(offset_to_position(10, source, &offsets), (1, 0));
}
#[test]
fn test_offset_to_position_emoji() {
let source = "😀a";
let offsets = compute_line_offsets(source);
assert_eq!(offset_to_position(0, source, &offsets), (0, 0));
assert_eq!(offset_to_position(4, source, &offsets), (0, 2));
assert_eq!(offset_to_position(5, source, &offsets), (0, 3));
}
#[test]
fn test_position_to_offset() {
let source = "日本語\ntest";
assert_eq!(position_to_offset(source, Position::new(0, 0)), 0);
assert_eq!(position_to_offset(source, Position::new(0, 2)), 6);
assert_eq!(position_to_offset(source, Position::new(0, 3)), 9);
assert_eq!(position_to_offset(source, Position::new(0, 10)), 9);
assert_eq!(position_to_offset(source, Position::new(1, 4)), 14);
assert_eq!(position_to_offset(source, Position::new(5, 0)), 14);
assert_eq!(position_to_offset("😀a", Position::new(0, 2)), 4);
}
#[test]
fn test_byte_len_to_utf16_len() {
assert_eq!(byte_len_to_utf16_len("hello", 0, 5), 5);
assert_eq!(byte_len_to_utf16_len("日本語", 0, 9), 3);
assert_eq!(byte_len_to_utf16_len("😀", 0, 4), 2);
}
}