Support basic diagnostic reporting

This commit is contained in:
Ayaz Hafiz 2022-08-20 15:14:59 -05:00
parent c50925240d
commit 9d365a8a57
No known key found for this signature in database
GPG key ID: 0E2A37416A25EF58
11 changed files with 572 additions and 3 deletions

View file

@ -0,0 +1,176 @@
use roc_region::all::{LineColumnRegion, LineInfo, Region};
use tower_lsp::lsp_types::{Position, Range};
fn range_of_region(line_info: &LineInfo, region: Region) -> Range {
let LineColumnRegion { start, end } = line_info.convert_region(region);
Range {
start: Position {
line: start.line,
character: start.column,
},
end: Position {
line: end.line,
character: end.column,
},
}
}
pub(crate) mod diag {
use std::path::Path;
use roc_load::LoadingProblem;
use roc_region::all::LineInfo;
use roc_solve_problem::TypeError;
use roc_reporting::report::{RocDocAllocator, Severity};
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
use super::range_of_region;
pub trait IntoLspSeverity {
fn into_lsp_severity(self) -> DiagnosticSeverity;
}
impl IntoLspSeverity for Severity {
fn into_lsp_severity(self) -> DiagnosticSeverity {
match self {
Severity::RuntimeError => DiagnosticSeverity::ERROR,
Severity::Warning => DiagnosticSeverity::WARNING,
}
}
}
pub trait IntoLspDiagnostic<'a> {
type Feed;
fn into_lsp_diagnostic(self, feed: &'a Self::Feed) -> Option<Diagnostic>;
}
impl IntoLspDiagnostic<'_> for LoadingProblem<'_> {
type Feed = ();
fn into_lsp_diagnostic(self, _feed: &()) -> Option<Diagnostic> {
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 1,
},
};
let msg;
match self {
LoadingProblem::FileProblem { filename, error } => {
msg = format!(
"Failed to load {} due to an I/O error: {}",
filename.display(),
error
);
}
LoadingProblem::ParsingFailed(_) => {
unreachable!("should be formatted before sent back")
}
LoadingProblem::UnexpectedHeader(header) => {
msg = format!("Unexpected header: {}", header);
}
LoadingProblem::MsgChannelDied => {
msg = format!("Internal error: message channel died");
}
LoadingProblem::ErrJoiningWorkerThreads => {
msg = format!("Internal error: analysis worker threads died");
}
LoadingProblem::TriedToImportAppModule => {
msg = format!("Attempted to import app module");
}
LoadingProblem::FormattedReport(report) => {
msg = report;
}
};
Some(Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
code: None,
code_description: None,
source: Some("load".to_owned()),
message: msg,
related_information: None,
tags: None,
data: None,
})
}
}
pub struct ProblemFmt<'a> {
pub alloc: &'a RocDocAllocator<'a>,
pub line_info: &'a LineInfo,
pub path: &'a Path,
}
impl<'a> IntoLspDiagnostic<'a> for roc_problem::can::Problem {
type Feed = ProblemFmt<'a>;
fn into_lsp_diagnostic(self, fmt: &'a ProblemFmt<'a>) -> Option<Diagnostic> {
let range = range_of_region(fmt.line_info, self.region());
let report = roc_reporting::report::can_problem(
&fmt.alloc,
&fmt.line_info,
fmt.path.to_path_buf(),
self,
);
let severity = report.severity.into_lsp_severity();
let mut msg = String::new();
report.render_ci(&mut msg, fmt.alloc);
Some(Diagnostic {
range,
severity: Some(severity),
code: None,
code_description: None,
source: None,
message: msg,
related_information: None,
tags: None,
data: None,
})
}
}
impl<'a> IntoLspDiagnostic<'a> for TypeError {
type Feed = ProblemFmt<'a>;
fn into_lsp_diagnostic(self, fmt: &'a ProblemFmt<'a>) -> Option<Diagnostic> {
let range = range_of_region(fmt.line_info, self.region());
let report = roc_reporting::report::type_problem(
&fmt.alloc,
&fmt.line_info,
fmt.path.to_path_buf(),
self,
)?;
let severity = report.severity.into_lsp_severity();
let mut msg = String::new();
report.render_ci(&mut msg, fmt.alloc);
Some(Diagnostic {
range,
severity: Some(severity),
code: None,
code_description: None,
source: None,
message: msg,
related_information: None,
tags: None,
data: None,
})
}
}
}

View file

@ -0,0 +1,158 @@
use std::collections::HashMap;
use bumpalo::Bump;
use roc_load::{LoadedModule, LoadingProblem};
use roc_region::all::LineInfo;
use roc_reporting::report::RocDocAllocator;
use tower_lsp::lsp_types::{Diagnostic, Url};
use crate::convert::diag::{IntoLspDiagnostic, ProblemFmt};
pub(crate) enum DocumentChange {
Modified(Url, String),
Closed(Url),
}
#[derive(Debug)]
struct Document {
url: Url,
source: String,
arena: Bump,
// Incrementally updated module, diagnostis, etc.
module: Option<Result<LoadedModule, ()>>,
diagnostics: Option<Vec<Diagnostic>>,
}
impl Document {
fn new(url: Url, source: String) -> Self {
Self {
url,
source,
arena: Bump::new(),
module: None,
diagnostics: None,
}
}
fn prime(&mut self, source: String) {
self.source = source;
self.module = None;
self.diagnostics = None;
}
fn module(&mut self) -> Result<&mut LoadedModule, LoadingProblem<'_>> {
if let Some(Ok(module)) = &mut self.module {
// Safety: returning for time self is alive
return Ok(unsafe { std::mem::transmute(module) });
}
let fi = self.url.to_file_path().unwrap();
let src_dir = fi.parent().unwrap().to_path_buf();
let loaded = roc_load::load_and_typecheck_str(
&self.arena,
fi,
&self.source,
src_dir,
Default::default(),
roc_target::TargetInfo::default_x86_64(),
roc_reporting::report::RenderTarget::Generic,
);
match loaded {
Ok(module) => {
self.module = Some(Ok(module));
Ok(self.module.as_mut().unwrap().as_mut().unwrap())
}
Err(problem) => {
self.module = Some(Err(()));
Err(problem)
}
}
}
fn diagnostics(&mut self) -> Vec<Diagnostic> {
if let Some(diagnostics) = &self.diagnostics {
return diagnostics.clone();
}
let loaded: Result<&'static mut LoadedModule, LoadingProblem> =
unsafe { std::mem::transmute(self.module()) };
let diagnostics = match loaded {
Ok(module) => {
let lines: Vec<_> = self.source.lines().collect();
let line_info = LineInfo::new(&self.source);
let alloc = RocDocAllocator::new(&lines, module.module_id, &module.interns);
let mut all_problems = Vec::new();
let module_path = self.url.to_file_path().unwrap();
let fmt = ProblemFmt {
alloc: &alloc,
line_info: &line_info,
path: &module_path,
};
for can_problem in module
.can_problems
.remove(&module.module_id)
.unwrap_or_default()
{
if let Some(diag) = can_problem.into_lsp_diagnostic(&fmt) {
all_problems.push(diag);
}
}
for type_problem in module
.type_problems
.remove(&module.module_id)
.unwrap_or_default()
{
if let Some(diag) = type_problem.into_lsp_diagnostic(&fmt) {
all_problems.push(diag);
}
}
all_problems
}
Err(problem) => {
let mut all_problems = vec![];
all_problems.extend(problem.into_lsp_diagnostic(&()));
all_problems
}
};
self.diagnostics = Some(diagnostics);
self.diagnostics.as_ref().unwrap().clone()
}
}
#[derive(Debug, Default)]
pub(crate) struct Registry {
documents: HashMap<Url, Document>,
}
impl Registry {
pub fn apply_change(&mut self, change: DocumentChange) {
match change {
DocumentChange::Modified(url, source) => match self.documents.get_mut(&url) {
Some(document) => document.prime(source),
None => {
self.documents
.insert(url.clone(), Document::new(url, source));
}
},
DocumentChange::Closed(url) => {
self.documents.remove(&url);
}
}
}
pub fn diagnostics(&mut self, document: &Url) -> Vec<Diagnostic> {
self.documents.get_mut(document).unwrap().diagnostics()
}
}

View file

@ -0,0 +1,112 @@
use parking_lot::{Mutex, MutexGuard};
use registry::{DocumentChange, Registry};
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server};
mod convert;
mod registry;
#[derive(Debug)]
struct RocLs {
client: Client,
registry: Mutex<Registry>,
}
impl RocLs {
pub fn new(client: Client) -> Self {
Self {
client,
registry: Mutex::new(Registry::default()),
}
}
fn registry(&self) -> MutexGuard<Registry> {
self.registry.lock()
}
pub fn capabilities() -> ServerCapabilities {
let text_document_sync = Some(TextDocumentSyncCapability::Options(
// TODO: later on make this incremental
TextDocumentSyncOptions {
open_close: Some(true),
change: Some(TextDocumentSyncKind::FULL),
..TextDocumentSyncOptions::default()
},
));
let hover_provider = Some(HoverProviderCapability::Simple(true));
ServerCapabilities {
text_document_sync,
hover_provider,
..ServerCapabilities::default()
}
}
/// Records a document content change.
async fn change(&self, fi: Url, text: String, version: i32) {
self.registry()
.apply_change(DocumentChange::Modified(fi.clone(), text));
let diagnostics = self.registry().diagnostics(&fi);
self.client
.publish_diagnostics(fi, diagnostics, Some(version))
.await;
}
async fn close(&self, fi: Url) {
self.registry().apply_change(DocumentChange::Closed(fi));
}
}
#[tower_lsp::async_trait]
impl LanguageServer for RocLs {
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
Ok(InitializeResult {
capabilities: Self::capabilities(),
..InitializeResult::default()
})
}
async fn initialized(&self, _: InitializedParams) {
self.client
.log_message(MessageType::INFO, "Roc language server initialized.")
.await;
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let TextDocumentItem {
uri, text, version, ..
} = params.text_document;
self.change(uri, text, version).await;
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let VersionedTextDocumentIdentifier { uri, version, .. } = params.text_document;
// NOTE: We specify that we expect full-content syncs in the server capabilities,
// so here we assume the only change passed is a change of the entire document's content.
let TextDocumentContentChangeEvent { text, .. } =
params.content_changes.into_iter().next().unwrap();
self.change(uri, text, version).await;
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let TextDocumentIdentifier { uri } = params.text_document;
self.close(uri).await;
}
async fn shutdown(&self) -> Result<()> {
Ok(())
}
}
#[tokio::main]
async fn main() {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let (service, socket) = LspService::new(RocLs::new);
Server::new(stdin, stdout, socket).serve(service).await;
}