use codespan::{
ByteIndex, ByteOffset, ColumnIndex, FileId, Files, LineIndex, LineIndexOutOfBoundsError,
LocationError, RawIndex, RawOffset, Span, SpanOutOfBoundsError,
};
use codespan_reporting::diagnostic::{Diagnostic, Severity};
use lsp_types as lsp;
use std::{error, fmt};
use url::Url;
#[derive(Debug, PartialEq)]
pub enum Error {
UnableToCorrelateFilename(String),
ColumnOutOfBounds {
given: ColumnIndex,
max: ColumnIndex,
},
Location(LocationError),
LineIndexOutOfBounds(LineIndexOutOfBoundsError),
SpanOutOfBounds(SpanOutOfBoundsError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::UnableToCorrelateFilename(s) => {
write!(f, "Unable to correlate filename `{}` to url", s)
},
Error::ColumnOutOfBounds { given, max } => {
write!(f, "Column out of bounds - given: {}, max: {}", given, max)
},
Error::Location(e) => e.fmt(f),
Error::LineIndexOutOfBounds(e) => e.fmt(f),
Error::SpanOutOfBounds(e) => e.fmt(f),
}
}
}
impl From<LocationError> for Error {
fn from(e: LocationError) -> Error {
Error::Location(e)
}
}
impl From<LineIndexOutOfBoundsError> for Error {
fn from(e: LineIndexOutOfBoundsError) -> Error {
Error::LineIndexOutOfBounds(e)
}
}
impl From<SpanOutOfBoundsError> for Error {
fn from(e: SpanOutOfBoundsError) -> Error {
Error::SpanOutOfBounds(e)
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Error::UnableToCorrelateFilename(_) | Error::ColumnOutOfBounds { .. } => None,
Error::Location(error) => Some(error),
Error::LineIndexOutOfBounds(error) => Some(error),
Error::SpanOutOfBounds(error) => Some(error),
}
}
}
fn location_to_position(
line_str: &str,
line: LineIndex,
column: ColumnIndex,
byte_index: ByteIndex,
) -> Result<lsp::Position, Error> {
if column.to_usize() > line_str.len() {
let max = ColumnIndex(line_str.len() as RawIndex);
let given = column;
Err(Error::ColumnOutOfBounds { given, max })
} else if !line_str.is_char_boundary(column.to_usize()) {
let given = byte_index;
Err(LocationError::InvalidCharBoundary { given }.into())
} else {
let line_utf16 = line_str[..column.to_usize()].encode_utf16();
let character = line_utf16.count() as u64;
let line = line.to_usize() as u64;
Ok(lsp::Position { line, character })
}
}
pub fn byte_index_to_position(
files: &Files,
file_id: FileId,
byte_index: ByteIndex,
) -> Result<lsp::Position, Error> {
let location = files.location(file_id, byte_index)?;
let line_span = files.line_span(file_id, location.line)?;
let line_str = files.source_slice(file_id, line_span)?;
let column = ColumnIndex::from((byte_index - line_span.start()).0 as RawIndex);
location_to_position(line_str, location.line, column, byte_index)
}
pub fn byte_span_to_range(files: &Files, file_id: FileId, span: Span) -> Result<lsp::Range, Error> {
Ok(lsp::Range {
start: byte_index_to_position(files, file_id, span.start())?,
end: byte_index_to_position(files, file_id, span.end())?,
})
}
pub fn character_to_line_offset(line: &str, character: u64) -> Result<ByteOffset, Error> {
let line_len = ByteOffset::from(line.len() as RawOffset);
let mut character_offset = 0;
let mut chars = line.chars();
while let Some(ch) = chars.next() {
if character_offset == character {
let chars_off = ByteOffset::from_str_len(chars.as_str());
let ch_off = ByteOffset::from_char_len(ch);
return Ok(line_len - chars_off - ch_off);
}
character_offset += ch.len_utf16() as u64;
}
if character_offset == character {
Ok(line_len)
} else {
Err(Error::ColumnOutOfBounds {
given: ColumnIndex(character_offset as RawIndex),
max: ColumnIndex(line.len() as RawIndex),
})
}
}
pub fn position_to_byte_index(
files: &Files,
file_id: FileId,
position: &lsp::Position,
) -> Result<ByteIndex, Error> {
let line_span = files.line_span(file_id, position.line as RawIndex)?;
let source = files.source_slice(file_id, line_span)?;
let byte_offset = character_to_line_offset(source, position.character)?;
Ok(line_span.start() + byte_offset)
}
pub fn range_to_byte_span(
files: &Files,
file_id: FileId,
range: &lsp::Range,
) -> Result<Span, Error> {
Ok(Span::new(
position_to_byte_index(files, file_id, &range.start)?,
position_to_byte_index(files, file_id, &range.end)?,
))
}
pub fn make_lsp_severity(severity: Severity) -> lsp::DiagnosticSeverity {
match severity {
Severity::Error | Severity::Bug => lsp::DiagnosticSeverity::Error,
Severity::Warning => lsp::DiagnosticSeverity::Warning,
Severity::Note => lsp::DiagnosticSeverity::Information,
Severity::Help => lsp::DiagnosticSeverity::Hint,
}
}
pub fn make_lsp_diagnostic(
files: &Files,
source: impl Into<Option<String>>,
diagnostic: Diagnostic,
mut correlate_file_url: impl FnMut(FileId) -> Result<Url, ()>,
) -> Result<lsp::Diagnostic, Error> {
let primary_file_id = diagnostic.primary_label.file_id;
let primary_span = diagnostic.primary_label.span;
let primary_label_range = byte_span_to_range(files, primary_file_id, primary_span)?;
let primary_message = {
let mut message = diagnostic.message;
if !diagnostic.notes.is_empty() {
message.push_str("\n\n");
for note in diagnostic.notes {
for (i, line) in note.lines().enumerate() {
message.push_str(" ");
match i {
0 => message.push_str("•"),
_ => message.push_str(" "),
}
message.push_str(" ");
message.push_str(line.trim_end());
message.push_str("\n");
}
}
}
message
};
let related_information = diagnostic
.secondary_labels
.into_iter()
.map(|label| {
let file_id = label.file_id;
let range = byte_span_to_range(files, file_id, label.span)?;
let uri = correlate_file_url(file_id)
.map_err(|()| Error::UnableToCorrelateFilename(files.name(file_id).to_owned()))?;
Ok(lsp::DiagnosticRelatedInformation {
location: lsp::Location { uri, range },
message: label.message,
})
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(lsp::Diagnostic {
range: primary_label_range,
code: diagnostic.code.map(lsp::NumberOrString::String),
source: source.into(),
severity: Some(make_lsp_severity(diagnostic.severity)),
message: primary_message,
related_information: if related_information.is_empty() {
None
} else {
Some(related_information)
},
tags: None,
})
}
#[cfg(test)]
mod tests {
use codespan::Location;
use super::*;
#[test]
fn position() {
let text = r#"
let test = 2
let test1 = ""
test
"#;
let mut files = Files::new();
let file_id = files.add("test", text);
let pos = position_to_byte_index(
&files,
file_id,
&lsp::Position {
line: 3,
character: 2,
},
)
.unwrap();
assert_eq!(Location::new(3, 2), files.location(file_id, pos).unwrap());
}
const UNICODE: &str = "åä t𐐀b";
#[test]
fn unicode_get_byte_index() {
let mut files = Files::new();
let file_id = files.add("unicode", UNICODE);
let result = position_to_byte_index(
&files,
file_id,
&lsp::Position {
line: 0,
character: 3,
},
);
assert_eq!(result, Ok(ByteIndex::from(5)));
let result = position_to_byte_index(
&files,
file_id,
&lsp::Position {
line: 0,
character: 6,
},
);
assert_eq!(result, Ok(ByteIndex::from(10)));
}
#[test]
fn unicode_get_position() {
let mut files = Files::new();
let file_id = files.add("unicode", UNICODE);
let result = byte_index_to_position(&files, file_id, ByteIndex::from(5));
assert_eq!(
result,
Ok(lsp::Position {
line: 0,
character: 3,
})
);
let result = byte_index_to_position(&files, file_id, ByteIndex::from(10));
assert_eq!(
result,
Ok(lsp::Position {
line: 0,
character: 6,
})
);
}
}