1
1
mirror of https://github.com/oxalica/nil.git synced 2024-10-27 12:30:52 +03:00

Impl diagnostics publishing

This commit is contained in:
oxalica 2022-08-05 18:39:16 +08:00
parent 6613e56095
commit 7c555755fc
7 changed files with 141 additions and 24 deletions

View File

@ -14,6 +14,11 @@ Super fast incremental analysis! Scans `all-packages.nix` in less than 0.1s and
- [x] Builtin names.
- [x] Local bindings.
- [ ] Attrset fields.
- [x] Diagnostics. `textDocument/publishDiagnostics`
- Syntax errors.
- Incomplete syntax errors are currently suppressed to avoid noisy outputs during typing.
- [x] Hard semantic errors reported as parse errors by Nix, like duplicated keys in attrsets.
- [ ] Client pulled diagnostics.
- [ ] Cross-file analysis.
- [ ] Multi-threaded.

View File

@ -1,6 +1,8 @@
use crate::{LineMap, StateSnapshot, Vfs, VfsPath};
use lsp_types::{Location, Position, Range, TextDocumentPositionParams};
use nil::{FilePos, InFile};
use lsp_types::{
self as lsp, DiagnosticSeverity, Location, Position, Range, TextDocumentPositionParams,
};
use nil::{Diagnostic, FilePos, InFile, Severity};
use text_size::TextRange;
pub(crate) fn from_file_pos(
@ -25,3 +27,20 @@ pub(crate) fn to_range(line_map: &LineMap, range: TextRange) -> Range {
let (line2, col2) = line_map.line_col(range.end());
Range::new(Position::new(line1, col1), Position::new(line2, col2))
}
pub(crate) fn to_diagnostic(line_map: &LineMap, diag: Diagnostic) -> Option<lsp::Diagnostic> {
Some(lsp::Diagnostic {
severity: match diag.severity() {
Severity::Error => Some(DiagnosticSeverity::ERROR),
Severity::IncompleteSyntax => return None,
},
range: to_range(line_map, diag.range),
code: None,
code_description: None,
source: None,
message: diag.message(),
related_information: None,
tags: None,
data: None,
})
}

View File

@ -1,16 +1,16 @@
use crate::{handler, Vfs, VfsPath};
use crate::{convert, handler, Vfs, VfsPath};
use anyhow::{bail, Result};
use crossbeam_channel::{Receiver, Sender};
use lsp_server::{ErrorCode, Message, Notification, Request, Response};
use lsp_types::notification::Notification as _;
use lsp_types::{notification as notif, request as req, Url};
use nil::{Analysis, AnalysisHost, Change};
use lsp_types::{notification as notif, request as req, PublishDiagnosticsParams, Url};
use nil::{Analysis, AnalysisHost};
use std::sync::{Arc, RwLock};
pub struct State {
host: AnalysisHost,
vfs: Arc<RwLock<Vfs>>,
responder: Sender<Message>,
sender: Sender<Message>,
is_shutdown: bool,
}
@ -19,7 +19,7 @@ impl State {
Self {
host: Default::default(),
vfs: Default::default(),
responder,
sender: responder,
is_shutdown: false,
}
}
@ -47,7 +47,7 @@ impl State {
ErrorCode::InvalidRequest as i32,
"Shutdown already requested.".into(),
);
self.responder.send(resp.into()).unwrap();
self.sender.send(resp.into()).unwrap();
return;
}
@ -77,6 +77,12 @@ impl State {
.finish();
}
fn send_notification<N: notif::Notification>(&self, params: N::Params) {
self.sender
.send(Notification::new(N::METHOD.into(), params).into())
.unwrap();
}
fn snapshot(&self) -> StateSnapshot {
StateSnapshot {
analysis: self.host.snapshot(),
@ -84,19 +90,33 @@ impl State {
}
}
fn apply_change(&mut self, change: Change) {
if !change.is_empty() {
log::debug!("Files changed: {:?}", change);
self.host.apply_change(change);
}
}
fn set_vfs_file_content(&mut self, uri: &Url, text: Option<String>) {
if let Ok(path) = VfsPath::try_from(uri) {
self.apply_change({
let mut vfs = self.vfs.write().unwrap();
vfs.set_file_content(path, text);
vfs.take_change()
let change = vfs.take_change();
log::debug!("Files changed: {:?}", change);
self.host.apply_change(change.clone());
// Currently we push down changes immediately.
assert_eq!(change.file_changes.len(), 1);
let (file, text) = &change.file_changes[0];
let line_map = vfs.file_line_map(*file).unwrap();
let diagnostics = text
.as_ref()
.and_then(|_| self.host.snapshot().diagnostics(*file).ok())
.map(|diags| {
diags
.into_iter()
.filter_map(|diag| convert::to_diagnostic(line_map, diag))
.collect::<Vec<_>>()
})
.unwrap_or_default();
self.send_notification::<notif::PublishDiagnostics>(PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics,
version: None,
});
}
}
@ -112,7 +132,7 @@ impl<'s> RequestDispatcher<'s> {
let params = serde_json::from_value::<R::Params>(req.params).unwrap();
let resp = f(self.0, params);
let resp = Response::new_ok(req.id, serde_json::to_value(resp).unwrap());
self.0.responder.send(resp.into()).unwrap();
self.0.sender.send(resp.into()).unwrap();
}
self
}
@ -124,7 +144,7 @@ impl<'s> RequestDispatcher<'s> {
let params = serde_json::from_value::<R::Params>(req.params).unwrap();
let resp = f(self.0.snapshot(), params);
let resp = Response::new_ok(req.id, serde_json::to_value(resp).unwrap());
self.0.responder.send(resp.into()).unwrap();
self.0.sender.send(resp.into()).unwrap();
}
self
}
@ -132,7 +152,7 @@ impl<'s> RequestDispatcher<'s> {
fn finish(self) {
if let Some(req) = self.1 {
let resp = Response::new_err(req.id, ErrorCode::MethodNotFound as _, String::new());
self.0.responder.send(resp.into()).unwrap();
self.0.sender.send(resp.into()).unwrap();
}
}
}

View File

@ -1,3 +1,4 @@
use std::fmt;
use syntax::{ErrorKind as SynErrorKind, TextRange};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -53,3 +54,15 @@ impl From<syntax::Error> for Diagnostic {
}
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} at {}..{}",
self.message(),
u32::from(self.range.start()),
u32::from(self.range.end()),
)
}
}

55
src/ide/diagnostics.rs Normal file
View File

@ -0,0 +1,55 @@
use crate::def::DefDatabase;
use crate::{Diagnostic, FileId};
const MAX_DIAGNOSTIC_CNT: usize = 128;
pub(crate) fn diagnostics(db: &dyn DefDatabase, file: FileId) -> Vec<Diagnostic> {
let parse = db.parse(file).value;
let module = db.module(file);
parse
.errors()
.iter()
.map(|&err| Diagnostic::from(err))
.chain(module.diagnostics().iter().cloned())
.take(MAX_DIAGNOSTIC_CNT)
.collect()
}
#[cfg(test)]
mod tests {
use crate::tests::TestDB;
use expect_test::{expect, Expect};
fn check(fixture: &str, expect: Expect) {
let (db, file_id, []) = TestDB::single_file(fixture).unwrap();
let diags = super::diagnostics(&db, file_id);
assert!(!diags.is_empty());
let got = diags
.iter()
.map(|d| d.to_string() + "\n")
.collect::<Vec<_>>()
.join("");
expect.assert_eq(&got);
}
#[test]
fn syntax_error() {
check(
"1 == 2 == 3",
expect![[r#"
Invalid usage of no-associative operators at 7..9
"#]],
);
}
#[test]
fn lower_error() {
check(
"{ a = 1; a = 2; }",
expect![[r#"
Duplicated name definition at 2..3
Duplicated name definition at 9..10
"#]],
);
}
}

View File

@ -1,10 +1,11 @@
mod completion;
mod diagnostics;
mod goto_definition;
mod references;
use crate::base::SourceDatabaseStorage;
use crate::def::DefDatabaseStorage;
use crate::{Change, FileId, FilePos, FileRange};
use crate::{Change, Diagnostic, FileId, FilePos, FileRange};
use rowan::TextRange;
use salsa::{Cancelled, Database, Durability, ParallelDatabase};
use std::fmt;
@ -81,6 +82,10 @@ impl Analysis {
Cancelled::catch(|| f(&self.db))
}
pub fn diagnostics(&self, file: FileId) -> Cancellable<Vec<Diagnostic>> {
self.with_db(|db| diagnostics::diagnostics(db, file))
}
pub fn goto_definition(&self, pos: FilePos) -> Cancellable<Option<Vec<NavigationTarget>>> {
self.with_db(|db| goto_definition::goto_definition(db, pos.file_id, pos.value))
}

View File

@ -8,7 +8,7 @@ mod ide;
mod tests;
pub use base::{Change, FileId, FilePos, FileRange, InFile};
pub use diagnostic::{Diagnostic, DiagnosticKind};
pub use diagnostic::{Diagnostic, DiagnosticKind, Severity};
pub use ide::{
Analysis, AnalysisHost, CompletionItem, CompletionItemKind, NavigationTarget, RootDatabase,
};