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] 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.
|
||||
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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 mut vfs = self.vfs.write().unwrap();
|
||||
vfs.set_file_content(path, text);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
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 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))
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user