pub mod code_actions;
pub mod completion;
mod content_changes;
pub mod definition;
pub mod document_link;
pub mod document_symbol;
pub mod hover;
pub mod inlay_hints;
pub mod references;
pub mod rename;
pub mod semantic_tokens;
pub mod workspace_symbol;
pub use code_actions::compute_code_actions;
pub use completion::{
compute_completion, resolve_completion_item, DATA_KEY_NAME, DATA_KEY_TYPE, DATA_KEY_URI,
};
pub use content_changes::apply_content_changes;
pub use definition::compute_definition;
pub use document_link::compute_document_links;
pub use document_symbol::compute_document_symbols;
pub use hover::compute_hover;
pub use inlay_hints::{compute_inlay_hints, InlayHintOptions};
pub use references::find_references;
pub use rename::{compute_rename, prepare_rename, RenameError};
pub use semantic_tokens::{
compute_semantic_tokens, compute_semantic_tokens_fallback, compute_semantic_tokens_range,
semantic_tokens_legend, semantic_tokens_provider,
};
pub use workspace_symbol::compute_workspace_symbols;
use sipha::error::{SemanticDiagnostic, Severity};
use sipha::line_index::LineIndex;
use sipha::red::SyntaxNode;
use tower_lsp::lsp_types::{
Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location, NumberOrString,
Position, Range, TextDocumentContentChangeEvent,
};
pub trait DocumentAnalysisLspExt {
fn lsp_diagnostics(&self, document_uri: Option<&str>) -> Vec<Diagnostic>;
fn apply_changes(
&self,
content_changes: Vec<TextDocumentContentChangeEvent>,
) -> (String, Option<SyntaxNode>);
}
impl DocumentAnalysisLspExt for crate::DocumentAnalysis {
fn lsp_diagnostics(&self, document_uri: Option<&str>) -> Vec<Diagnostic> {
self.diagnostics
.iter()
.map(|d| to_lsp_diagnostic(d, &self.source, &self.line_index, document_uri))
.collect()
}
fn apply_changes(
&self,
content_changes: Vec<TextDocumentContentChangeEvent>,
) -> (String, Option<SyntaxNode>) {
let (new_source, reparse_edit) = apply_content_changes(self, content_changes);
let root = if let Some(edit) = reparse_edit {
crate::reparse_or_parse(&self.source, self.root.as_ref(), &edit)
} else {
crate::parse(&new_source)
.ok()
.and_then(std::convert::identity)
};
(new_source, root)
}
}
#[must_use]
pub fn to_lsp_diagnostic(
d: &SemanticDiagnostic,
source: &str,
line_index: &LineIndex,
document_uri: Option<&str>,
) -> Diagnostic {
let (line_start, col_start) = line_index.line_col_utf16(source, d.span.start);
let (line_end, col_end) = line_index.line_col_utf16(source, d.span.end);
let severity = match d.severity {
Severity::Error => Some(DiagnosticSeverity::ERROR),
Severity::Warning => Some(DiagnosticSeverity::WARNING),
Severity::Deprecation => Some(DiagnosticSeverity::WARNING),
Severity::Note => Some(DiagnosticSeverity::INFORMATION),
};
let message = d.message.clone();
let related_information = build_related_information(d, source, line_index, document_uri);
Diagnostic {
range: Range {
start: Position {
line: line_start,
character: col_start,
},
end: Position {
line: line_end,
character: col_end,
},
},
severity,
code: d.code.clone().map(NumberOrString::String),
code_description: code_description_for(d.code.as_deref()),
source: Some("leekscript".to_string()),
message,
related_information,
tags: None,
data: None,
}
}
fn build_related_information(
d: &SemanticDiagnostic,
source: &str,
line_index: &LineIndex,
document_uri: Option<&str>,
) -> Option<Vec<DiagnosticRelatedInformation>> {
if d.related.is_empty() {
return None;
}
let uri = document_uri.and_then(|s| tower_lsp::lsp_types::Url::parse(s).ok())?;
let mut out = Vec::with_capacity(d.related.len());
for rel in &d.related {
let (line_start, col_start) = line_index.line_col_utf16(source, rel.span.start);
let (line_end, col_end) = line_index.line_col_utf16(source, rel.span.end);
out.push(DiagnosticRelatedInformation {
location: Location::new(
uri.clone(),
Range {
start: Position {
line: line_start,
character: col_start,
},
end: Position {
line: line_end,
character: col_end,
},
},
),
message: rel.message.clone(),
});
}
Some(out)
}
fn code_description_for(code: Option<&str>) -> Option<tower_lsp::lsp_types::CodeDescription> {
let code = code?;
let href = tower_lsp::lsp_types::Url::parse(
format!("https://leek-wars.github.io/leek-wars-wiki/en/errors#{code}").as_str(),
)
.ok()?;
Some(tower_lsp::lsp_types::CodeDescription { href })
}
#[cfg(test)]
#[cfg(feature = "lsp")]
mod tests {
use sipha::error::{SemanticDiagnostic, Severity};
use sipha::line_index::LineIndex;
use sipha::types::Span;
use tower_lsp::lsp_types::DiagnosticSeverity;
use super::to_lsp_diagnostic;
use crate::AnalysisError;
#[test]
fn to_lsp_diagnostic_error_single_line() {
let source = "var x = 1";
let line_index = LineIndex::new(source.as_bytes());
let span = Span::new(4, 5);
let diag = AnalysisError::UnknownVariableOrFunction.at(span);
let lsp = to_lsp_diagnostic(&diag, source, &line_index, None);
assert_eq!(lsp.range.start.line, 0);
assert_eq!(lsp.range.start.character, 4);
assert_eq!(lsp.range.end.line, 0);
assert_eq!(lsp.range.end.character, 5);
assert_eq!(lsp.severity, Some(DiagnosticSeverity::ERROR));
assert_eq!(
lsp.code,
Some(tower_lsp::lsp_types::NumberOrString::String(
"E033".to_string()
))
);
assert!(lsp.message.contains("unknown variable or function"));
assert_eq!(lsp.source.as_deref(), Some("leekscript"));
}
#[test]
fn to_lsp_diagnostic_multiline_span() {
let source = "var a = 1;\nreturn z;\n";
let line_index = LineIndex::new(source.as_bytes());
let span = Span::new(11, 19);
let diag = AnalysisError::UnknownVariableOrFunction.at(span);
let lsp = to_lsp_diagnostic(&diag, source, &line_index, None);
assert_eq!(lsp.range.start.line, 1);
assert_eq!(lsp.range.start.character, 0);
assert_eq!(lsp.range.end.line, 1);
assert_eq!(lsp.range.end.character, 8); assert_eq!(lsp.severity, Some(DiagnosticSeverity::ERROR));
}
#[test]
fn to_lsp_diagnostic_severity_mapping() {
let source = "x";
let line_index = LineIndex::new(source.as_bytes());
let span = Span::new(0, 1);
let err = SemanticDiagnostic::error(span, "error");
let lsp_err = to_lsp_diagnostic(&err, source, &line_index, None);
assert_eq!(lsp_err.severity, Some(DiagnosticSeverity::ERROR));
let warn = SemanticDiagnostic::warning(span, "warning");
let lsp_warn = to_lsp_diagnostic(&warn, source, &line_index, None);
assert_eq!(lsp_warn.severity, Some(DiagnosticSeverity::WARNING));
let dep = SemanticDiagnostic::deprecation(span, "deprecated");
let lsp_dep = to_lsp_diagnostic(&dep, source, &line_index, None);
assert_eq!(lsp_dep.severity, Some(DiagnosticSeverity::WARNING));
let note = SemanticDiagnostic {
span,
message: "note".to_string(),
severity: Severity::Note,
code: None,
file_id: None,
related: vec![],
};
let lsp_note = to_lsp_diagnostic(¬e, source, &line_index, None);
assert_eq!(lsp_note.severity, Some(DiagnosticSeverity::INFORMATION));
}
#[test]
fn to_lsp_diagnostic_utf16_column() {
let source = "éx";
let line_index = LineIndex::new(source.as_bytes());
let span = Span::new(2, 3);
let diag = SemanticDiagnostic::error(span, "bad");
let lsp = to_lsp_diagnostic(&diag, source, &line_index, None);
assert_eq!(lsp.range.start.line, 0);
assert_eq!(lsp.range.start.character, 1);
assert_eq!(lsp.range.end.line, 0);
assert_eq!(lsp.range.end.character, 2);
}
#[test]
fn to_lsp_diagnostic_from_analysis_pipeline() {
let source = "return z;";
let root = crate::parse(source).unwrap().expect("parse");
let result = crate::analyze(&root);
assert!(result.has_errors());
let diag = &result.diagnostics[0];
let line_index = LineIndex::new(source.as_bytes());
let lsp = to_lsp_diagnostic(diag, source, &line_index, None);
assert_eq!(lsp.severity, Some(DiagnosticSeverity::ERROR));
assert!(lsp.message.contains("unknown variable or function") || lsp.message.contains("z"));
assert_eq!(lsp.source.as_deref(), Some("leekscript"));
}
#[test]
fn to_lsp_diagnostic_related_information() {
let source = "var a = 1;\nvar a;";
let line_index = LineIndex::new(source.as_bytes());
let first_span = Span::new(4, 5); let second_span = Span::new(13, 14); let diag = AnalysisError::VariableNameUnavailable
.at_with_related(second_span, vec![(first_span, "first declared here")]);
let lsp_no_uri = to_lsp_diagnostic(&diag, source, &line_index, None);
let lsp_with_uri =
to_lsp_diagnostic(&diag, source, &line_index, Some("file:///tmp/main.leek"));
assert!(lsp_no_uri.related_information.is_none());
let related = lsp_with_uri.related_information.as_ref().unwrap();
assert_eq!(related.len(), 1);
assert_eq!(related[0].message, "first declared here");
assert_eq!(related[0].location.range.start.line, 0);
assert_eq!(related[0].location.range.start.character, 4);
}
}