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:
parent
6613e56095
commit
7c555755fc
@ -14,6 +14,11 @@ Super fast incremental analysis! Scans `all-packages.nix` in less than 0.1s and
|
|||||||
- [x] Builtin names.
|
- [x] Builtin names.
|
||||||
- [x] Local bindings.
|
- [x] Local bindings.
|
||||||
- [ ] Attrset fields.
|
- [ ] 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.
|
- [ ] Cross-file analysis.
|
||||||
- [ ] Multi-threaded.
|
- [ ] Multi-threaded.
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use crate::{LineMap, StateSnapshot, Vfs, VfsPath};
|
use crate::{LineMap, StateSnapshot, Vfs, VfsPath};
|
||||||
use lsp_types::{Location, Position, Range, TextDocumentPositionParams};
|
use lsp_types::{
|
||||||
use nil::{FilePos, InFile};
|
self as lsp, DiagnosticSeverity, Location, Position, Range, TextDocumentPositionParams,
|
||||||
|
};
|
||||||
|
use nil::{Diagnostic, FilePos, InFile, Severity};
|
||||||
use text_size::TextRange;
|
use text_size::TextRange;
|
||||||
|
|
||||||
pub(crate) fn from_file_pos(
|
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());
|
let (line2, col2) = line_map.line_col(range.end());
|
||||||
Range::new(Position::new(line1, col1), Position::new(line2, col2))
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
use crate::{handler, Vfs, VfsPath};
|
use crate::{convert, handler, Vfs, VfsPath};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use lsp_server::{ErrorCode, Message, Notification, Request, Response};
|
use lsp_server::{ErrorCode, Message, Notification, Request, Response};
|
||||||
use lsp_types::notification::Notification as _;
|
use lsp_types::notification::Notification as _;
|
||||||
use lsp_types::{notification as notif, request as req, Url};
|
use lsp_types::{notification as notif, request as req, PublishDiagnosticsParams, Url};
|
||||||
use nil::{Analysis, AnalysisHost, Change};
|
use nil::{Analysis, AnalysisHost};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
host: AnalysisHost,
|
host: AnalysisHost,
|
||||||
vfs: Arc<RwLock<Vfs>>,
|
vfs: Arc<RwLock<Vfs>>,
|
||||||
responder: Sender<Message>,
|
sender: Sender<Message>,
|
||||||
is_shutdown: bool,
|
is_shutdown: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ impl State {
|
|||||||
Self {
|
Self {
|
||||||
host: Default::default(),
|
host: Default::default(),
|
||||||
vfs: Default::default(),
|
vfs: Default::default(),
|
||||||
responder,
|
sender: responder,
|
||||||
is_shutdown: false,
|
is_shutdown: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,7 +47,7 @@ impl State {
|
|||||||
ErrorCode::InvalidRequest as i32,
|
ErrorCode::InvalidRequest as i32,
|
||||||
"Shutdown already requested.".into(),
|
"Shutdown already requested.".into(),
|
||||||
);
|
);
|
||||||
self.responder.send(resp.into()).unwrap();
|
self.sender.send(resp.into()).unwrap();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +77,12 @@ impl State {
|
|||||||
.finish();
|
.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 {
|
fn snapshot(&self) -> StateSnapshot {
|
||||||
StateSnapshot {
|
StateSnapshot {
|
||||||
analysis: self.host.snapshot(),
|
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>) {
|
fn set_vfs_file_content(&mut self, uri: &Url, text: Option<String>) {
|
||||||
if let Ok(path) = VfsPath::try_from(uri) {
|
if let Ok(path) = VfsPath::try_from(uri) {
|
||||||
self.apply_change({
|
let mut vfs = self.vfs.write().unwrap();
|
||||||
let mut vfs = self.vfs.write().unwrap();
|
vfs.set_file_content(path, text);
|
||||||
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 params = serde_json::from_value::<R::Params>(req.params).unwrap();
|
||||||
let resp = f(self.0, params);
|
let resp = f(self.0, params);
|
||||||
let resp = Response::new_ok(req.id, serde_json::to_value(resp).unwrap());
|
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
|
self
|
||||||
}
|
}
|
||||||
@ -124,7 +144,7 @@ impl<'s> RequestDispatcher<'s> {
|
|||||||
let params = serde_json::from_value::<R::Params>(req.params).unwrap();
|
let params = serde_json::from_value::<R::Params>(req.params).unwrap();
|
||||||
let resp = f(self.0.snapshot(), params);
|
let resp = f(self.0.snapshot(), params);
|
||||||
let resp = Response::new_ok(req.id, serde_json::to_value(resp).unwrap());
|
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
|
self
|
||||||
}
|
}
|
||||||
@ -132,7 +152,7 @@ impl<'s> RequestDispatcher<'s> {
|
|||||||
fn finish(self) {
|
fn finish(self) {
|
||||||
if let Some(req) = self.1 {
|
if let Some(req) = self.1 {
|
||||||
let resp = Response::new_err(req.id, ErrorCode::MethodNotFound as _, String::new());
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use std::fmt;
|
||||||
use syntax::{ErrorKind as SynErrorKind, TextRange};
|
use syntax::{ErrorKind as SynErrorKind, TextRange};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[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
55
src/ide/diagnostics.rs
Normal 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
|
||||||
|
"#]],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
mod completion;
|
mod completion;
|
||||||
|
mod diagnostics;
|
||||||
mod goto_definition;
|
mod goto_definition;
|
||||||
mod references;
|
mod references;
|
||||||
|
|
||||||
use crate::base::SourceDatabaseStorage;
|
use crate::base::SourceDatabaseStorage;
|
||||||
use crate::def::DefDatabaseStorage;
|
use crate::def::DefDatabaseStorage;
|
||||||
use crate::{Change, FileId, FilePos, FileRange};
|
use crate::{Change, Diagnostic, FileId, FilePos, FileRange};
|
||||||
use rowan::TextRange;
|
use rowan::TextRange;
|
||||||
use salsa::{Cancelled, Database, Durability, ParallelDatabase};
|
use salsa::{Cancelled, Database, Durability, ParallelDatabase};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@ -81,6 +82,10 @@ impl Analysis {
|
|||||||
Cancelled::catch(|| f(&self.db))
|
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>>> {
|
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))
|
self.with_db(|db| goto_definition::goto_definition(db, pos.file_id, pos.value))
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ mod ide;
|
|||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub use base::{Change, FileId, FilePos, FileRange, InFile};
|
pub use base::{Change, FileId, FilePos, FileRange, InFile};
|
||||||
pub use diagnostic::{Diagnostic, DiagnosticKind};
|
pub use diagnostic::{Diagnostic, DiagnosticKind, Severity};
|
||||||
pub use ide::{
|
pub use ide::{
|
||||||
Analysis, AnalysisHost, CompletionItem, CompletionItemKind, NavigationTarget, RootDatabase,
|
Analysis, AnalysisHost, CompletionItem, CompletionItemKind, NavigationTarget, RootDatabase,
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user