Skip to content

Commit c53bfc8

Browse files
authored
feat(lsp): Publish diagnostics on file save (#1676)
* feat(lsp): Publish diagnostics on file save * chore: Construct the LSP frontend inside the tokio runtime * chore(errors): Expose properties on CustomDiagnostic and CustomLabel for use by lsp * update for jakes changes
1 parent cd1acdb commit c53bfc8

File tree

6 files changed

+168
-40
lines changed

6 files changed

+168
-40
lines changed

Cargo.lock

+32-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ noir_wasm = { path = "crates/wasm" }
4141
cfg-if = "1.0.0"
4242
clap = { version = "4.1.4", features = ["derive"] }
4343
codespan = "0.11.1"
44+
codespan-lsp = "0.11.1"
4445
codespan-reporting = "0.11.1"
4546
chumsky = { git = "https://github.com/jfecher/chumsky", rev = "ad9d312" }
4647
dirs = "4"
48+
lsp-types = "0.94"
4749
serde = { version = "1.0.136", features = ["derive"] }
4850
serde_json = "1.0"
4951
smol_str = "0.1.17"
@@ -52,4 +54,7 @@ toml = "0.7.2"
5254
tower = "0.4"
5355
url = "2.2.0"
5456
wasm-bindgen = { version = "0.2.83", features = ["serde-serialize"] }
55-
wasm-bindgen-test = "0.3.33"
57+
wasm-bindgen-test = "0.3.33"
58+
59+
[patch.crates-io]
60+
async-lsp = { git = "https://github.com/oxalica/async-lsp", rev = "9fc2db84ddcda291a864f044657f68d4377557f7" }

crates/lsp/Cargo.toml

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ edition.workspace = true
88
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
99

1010
[dependencies]
11+
acvm.workspace = true
12+
codespan-lsp.workspace = true
13+
lsp-types.workspace = true
14+
noirc_driver.workspace = true
15+
noirc_errors.workspace = true
16+
noirc_frontend.workspace = true
1117
serde_json.workspace = true
1218
tower.workspace = true
1319
async-lsp = { version = "0.0.4", default-features = false, features = ["omni-trait"] }
14-
lsp-types = "0.94"
1520

1621
[dev-dependencies]
1722
tokio = { version = "1.0", features = ["macros"] }

crates/lsp/src/lib.rs

+108-16
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,42 @@ use std::{
55
task::{Context, Poll},
66
};
77

8+
use acvm::Language;
89
use async_lsp::{
9-
router::Router, AnyEvent, AnyNotification, AnyRequest, Error, LspService, ResponseError,
10+
router::Router, AnyEvent, AnyNotification, AnyRequest, ClientSocket, Error, LanguageClient,
11+
LspService, ResponseError,
1012
};
1113
use lsp_types::{
12-
notification, request, DidChangeConfigurationParams, DidChangeTextDocumentParams,
13-
DidCloseTextDocumentParams, DidOpenTextDocumentParams, InitializeParams, InitializeResult,
14-
InitializedParams, ServerCapabilities,
14+
notification, request, Diagnostic, DiagnosticSeverity, DidChangeConfigurationParams,
15+
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
16+
DidSaveTextDocumentParams, InitializeParams, InitializeResult, InitializedParams,
17+
PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncOptions,
1518
};
19+
use noirc_driver::Driver;
20+
use noirc_errors::{DiagnosticKind, FileDiagnostic};
21+
use noirc_frontend::graph::CrateType;
1622
use serde_json::Value as JsonValue;
1723
use tower::Service;
1824

1925
// State for the LSP gets implemented on this struct and is internal to the implementation
20-
#[derive(Debug, Default)]
21-
struct LspState;
26+
#[derive(Debug)]
27+
struct LspState {
28+
client: ClientSocket,
29+
}
30+
31+
impl LspState {
32+
fn new(client: &ClientSocket) -> Self {
33+
Self { client: client.clone() }
34+
}
35+
}
2236

2337
pub struct NargoLspService {
2438
router: Router<LspState>,
2539
}
2640

2741
impl NargoLspService {
28-
pub fn new() -> Self {
29-
let state = LspState::default();
42+
pub fn new(client: &ClientSocket) -> Self {
43+
let state = LspState::new(client);
3044
let mut router = Router::new(state);
3145
router
3246
.request::<request::Initialize, _>(on_initialize)
@@ -36,17 +50,12 @@ impl NargoLspService {
3650
.notification::<notification::DidOpenTextDocument>(on_did_open_text_document)
3751
.notification::<notification::DidChangeTextDocument>(on_did_change_text_document)
3852
.notification::<notification::DidCloseTextDocument>(on_did_close_text_document)
53+
.notification::<notification::DidSaveTextDocument>(on_did_save_text_document)
3954
.notification::<notification::Exit>(on_exit);
4055
Self { router }
4156
}
4257
}
4358

44-
impl Default for NargoLspService {
45-
fn default() -> Self {
46-
Self::new()
47-
}
48-
}
49-
5059
// This trait implemented as a passthrough to the router, which makes
5160
// our `NargoLspService` a normal Service as far as Tower is concerned.
5261
impl Service<AnyRequest> for NargoLspService {
@@ -90,8 +99,14 @@ fn on_initialize(
9099
_params: InitializeParams,
91100
) -> impl Future<Output = Result<InitializeResult, ResponseError>> {
92101
async {
102+
let text_document_sync = TextDocumentSyncOptions {
103+
save: Some(true.into()),
104+
..TextDocumentSyncOptions::default()
105+
};
106+
93107
Ok(InitializeResult {
94108
capabilities: ServerCapabilities {
109+
text_document_sync: Some(text_document_sync.into()),
95110
// Add capabilities before this spread when adding support for one
96111
..ServerCapabilities::default()
97112
},
@@ -142,22 +157,99 @@ fn on_did_close_text_document(
142157
ControlFlow::Continue(())
143158
}
144159

160+
fn on_did_save_text_document(
161+
state: &mut LspState,
162+
params: DidSaveTextDocumentParams,
163+
) -> ControlFlow<Result<(), async_lsp::Error>> {
164+
// TODO: Requiring `Language` and `is_opcode_supported` to construct a driver makes for some real stinky code
165+
// The driver should not require knowledge of the backend; instead should be implemented as an independent pass (in nargo?)
166+
let mut driver = Driver::new(&Language::R1CS, Box::new(|_op| false));
167+
168+
let file_path = &params.text_document.uri.to_file_path().unwrap();
169+
170+
driver.create_local_crate(file_path, CrateType::Binary);
171+
172+
let mut diagnostics = Vec::new();
173+
174+
let file_diagnostics = match driver.check_crate(false) {
175+
Ok(warnings) => warnings,
176+
Err(errors_and_warnings) => errors_and_warnings,
177+
};
178+
179+
if !file_diagnostics.is_empty() {
180+
let fm = driver.file_manager();
181+
let files = fm.as_simple_files();
182+
183+
for FileDiagnostic { file_id, diagnostic } in file_diagnostics {
184+
// TODO: This file_id never be 0 because the "path" where it maps is the directory, not a file
185+
if file_id.as_usize() != 0 {
186+
continue;
187+
}
188+
189+
let mut range = Range::default();
190+
191+
// TODO: Should this be the first item in secondaries? Should we bail when we find a range?
192+
for sec in diagnostic.secondaries {
193+
// TODO: Codespan ranges are often (always?) off by some amount of characters
194+
if let Ok(codespan_range) =
195+
codespan_lsp::byte_span_to_range(files, file_id.as_usize(), sec.span.into())
196+
{
197+
// We have to manually attach each because the codespan_lsp restricts lsp-types to the wrong version range
198+
range.start.line = codespan_range.start.line;
199+
range.start.character = codespan_range.start.character;
200+
range.end.line = codespan_range.end.line;
201+
range.end.character = codespan_range.end.character;
202+
}
203+
}
204+
let severity = match diagnostic.kind {
205+
DiagnosticKind::Error => Some(DiagnosticSeverity::ERROR),
206+
DiagnosticKind::Warning => Some(DiagnosticSeverity::WARNING),
207+
};
208+
diagnostics.push(Diagnostic {
209+
range,
210+
severity,
211+
message: diagnostic.message,
212+
..Diagnostic::default()
213+
})
214+
}
215+
}
216+
217+
let _ = state.client.publish_diagnostics(PublishDiagnosticsParams {
218+
uri: params.text_document.uri,
219+
version: None,
220+
diagnostics,
221+
});
222+
223+
ControlFlow::Continue(())
224+
}
225+
145226
fn on_exit(_state: &mut LspState, _params: ()) -> ControlFlow<Result<(), async_lsp::Error>> {
146227
ControlFlow::Continue(())
147228
}
148229

149230
#[cfg(test)]
150231
mod lsp_tests {
232+
use lsp_types::TextDocumentSyncCapability;
151233
use tokio::test;
152234

153235
use super::*;
154236

155237
#[test]
156238
async fn test_on_initialize() {
157-
let mut state = LspState::default();
239+
// Not available in published release yet
240+
let client = ClientSocket::new_closed();
241+
let mut state = LspState::new(&client);
158242
let params = InitializeParams::default();
159243
let response = on_initialize(&mut state, params).await.unwrap();
160-
assert_eq!(response.capabilities, ServerCapabilities::default());
244+
assert!(matches!(
245+
response.capabilities,
246+
ServerCapabilities {
247+
text_document_sync: Some(TextDocumentSyncCapability::Options(
248+
TextDocumentSyncOptions { save: Some(_), .. }
249+
)),
250+
..
251+
}
252+
));
161253
assert!(response.server_info.is_none());
162254
}
163255
}

crates/nargo_cli/src/cli/lsp_cmd.rs

+11-12
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,18 @@ pub(crate) fn run<B: Backend>(
2828

2929
let runtime = Builder::new_current_thread().enable_all().build().unwrap();
3030

31-
let (server, _) = async_lsp::Frontend::new_server(|client| {
32-
let router = NargoLspService::new();
33-
34-
ServiceBuilder::new()
35-
.layer(TracingLayer::default())
36-
.layer(LifecycleLayer::default())
37-
.layer(CatchUnwindLayer::default())
38-
.layer(ConcurrencyLayer::default())
39-
.layer(ClientProcessMonitorLayer::new(client))
40-
.service(router)
41-
});
42-
4331
runtime.block_on(async {
32+
let (server, _) = async_lsp::Frontend::new_server(|client| {
33+
let router = NargoLspService::new(&client);
34+
35+
ServiceBuilder::new()
36+
.layer(TracingLayer::default())
37+
.layer(LifecycleLayer::default())
38+
.layer(CatchUnwindLayer::default())
39+
.layer(ConcurrencyLayer::default())
40+
.layer(ClientProcessMonitorLayer::new(client))
41+
.service(router)
42+
});
4443
let stdin = BufReader::new(PipeStdin::lock().unwrap());
4544
let stdout = async_lsp::stdio::PipeStdout::lock().unwrap();
4645
server.run(stdin, stdout).await.map_err(CliError::LspError)

crates/noirc_errors/src/reporter.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
55

66
#[derive(Debug, Clone, PartialEq, Eq)]
77
pub struct CustomDiagnostic {
8-
message: String,
9-
secondaries: Vec<CustomLabel>,
8+
pub message: String,
9+
pub secondaries: Vec<CustomLabel>,
1010
notes: Vec<String>,
11-
kind: DiagnosticKind,
11+
pub kind: DiagnosticKind,
1212
}
1313

1414
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -93,9 +93,9 @@ impl std::fmt::Display for CustomDiagnostic {
9393
}
9494

9595
#[derive(Debug, Clone, PartialEq, Eq)]
96-
struct CustomLabel {
96+
pub struct CustomLabel {
9797
message: String,
98-
span: Span,
98+
pub span: Span,
9999
}
100100

101101
impl CustomLabel {

0 commit comments

Comments
 (0)