Skip to content

Commit ac73099

Browse files
committed
feat(lsp): support textDocument/diagnostic specification
Implementation of pull diagnostics introduced in LSP 3.17.
1 parent 7702e13 commit ac73099

File tree

8 files changed

+306
-136
lines changed

8 files changed

+306
-136
lines changed

helix-core/src/syntax.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -235,22 +235,26 @@ impl<'de> Deserialize<'de> for FileType {
235235
#[serde(rename_all = "kebab-case")]
236236
pub enum LanguageServerFeature {
237237
Format,
238+
// Goto, use bitflags, combining previous Goto members?
238239
GotoDeclaration,
239240
GotoDefinition,
240241
GotoTypeDefinition,
241242
GotoReference,
242243
GotoImplementation,
243-
// Goto, use bitflags, combining previous Goto members?
244+
244245
SignatureHelp,
245246
Hover,
246247
DocumentHighlight,
247248
Completion,
248249
CodeAction,
249250
WorkspaceCommand,
251+
// Symbols, use bitflags, see above?
250252
DocumentSymbols,
251253
WorkspaceSymbols,
252-
// Symbols, use bitflags, see above?
254+
255+
// Diagnostic in any form that is displayed on the gutter and screen
253256
Diagnostics,
257+
PullDiagnostics,
254258
RenameSymbol,
255259
InlayHints,
256260
}
@@ -274,6 +278,7 @@ impl Display for LanguageServerFeature {
274278
DocumentSymbols => "document-symbols",
275279
WorkspaceSymbols => "workspace-symbols",
276280
Diagnostics => "diagnostics",
281+
PullDiagnostics => "pull-diagnostics",
277282
RenameSymbol => "rename-symbol",
278283
InlayHints => "inlay-hints",
279284
};

helix-lsp/src/client.rs

+53-3
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ use lsp_types as lsp;
1515
use parking_lot::Mutex;
1616
use serde::Deserialize;
1717
use serde_json::Value;
18-
use std::future::Future;
19-
use std::process::Stdio;
2018
use std::sync::{
2119
atomic::{AtomicU64, Ordering},
2220
Arc,
2321
};
2422
use std::{collections::HashMap, path::PathBuf};
23+
use std::{future::Future, sync::atomic::AtomicBool};
24+
use std::{process::Stdio, sync::atomic};
2525
use tokio::{
2626
io::{BufReader, BufWriter},
2727
process::{Child, Command},
@@ -57,6 +57,9 @@ pub struct Client {
5757
initialize_notify: Arc<Notify>,
5858
/// workspace folders added while the server is still initializing
5959
req_timeout: u64,
60+
// there is no server capability to know if server supports it
61+
// set to true on first PublishDiagnostic notification
62+
supports_publish_diagnostic: AtomicBool,
6063
}
6164

6265
impl Client {
@@ -238,6 +241,7 @@ impl Client {
238241
root_uri,
239242
workspace_folders: Mutex::new(workspace_folders),
240243
initialize_notify: initialize_notify.clone(),
244+
supports_publish_diagnostic: AtomicBool::new(false),
241245
};
242246

243247
Ok((client, server_rx, initialize_notify))
@@ -277,6 +281,17 @@ impl Client {
277281
.expect("language server not yet initialized!")
278282
}
279283

284+
pub fn set_publish_diagnostic(&self, val: bool) {
285+
self.supports_publish_diagnostic
286+
.fetch_or(val, atomic::Ordering::Relaxed);
287+
}
288+
289+
/// Whether the server supports Publish Diagnostic
290+
pub fn publish_diagnostic(&self) -> bool {
291+
self.supports_publish_diagnostic
292+
.load(atomic::Ordering::Relaxed)
293+
}
294+
280295
/// Client has to be initialized otherwise this function panics
281296
#[inline]
282297
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
@@ -346,7 +361,12 @@ impl Client {
346361
capabilities.workspace_symbol_provider,
347362
Some(OneOf::Left(true) | OneOf::Right(_))
348363
),
349-
LanguageServerFeature::Diagnostics => true, // there's no extra server capability
364+
LanguageServerFeature::Diagnostics => {
365+
self.publish_diagnostic() || matches!(capabilities.diagnostic_provider, Some(_))
366+
}
367+
LanguageServerFeature::PullDiagnostics => {
368+
matches!(capabilities.diagnostic_provider, Some(_))
369+
}
350370
LanguageServerFeature::RenameSymbol => matches!(
351371
capabilities.rename_provider,
352372
Some(OneOf::Left(true)) | Some(OneOf::Right(_))
@@ -630,6 +650,10 @@ impl Client {
630650
dynamic_registration: Some(false),
631651
resolve_support: None,
632652
}),
653+
diagnostic: Some(lsp::DiagnosticClientCapabilities {
654+
dynamic_registration: Some(false),
655+
related_document_support: Some(true),
656+
}),
633657
..Default::default()
634658
}),
635659
window: Some(lsp::WindowClientCapabilities {
@@ -1141,6 +1165,32 @@ impl Client {
11411165
})
11421166
}
11431167

1168+
pub fn text_document_diagnostic(
1169+
&self,
1170+
text_document: lsp::TextDocumentIdentifier,
1171+
previous_result_id: Option<String>,
1172+
) -> Option<impl Future<Output = Result<Value>>> {
1173+
let capabilities = self.capabilities.get().unwrap();
1174+
1175+
// Return early if the server does not support pull diagnostic.
1176+
let identifier = match capabilities.diagnostic_provider.as_ref()? {
1177+
lsp::DiagnosticServerCapabilities::Options(cap) => cap.identifier.clone(),
1178+
lsp::DiagnosticServerCapabilities::RegistrationOptions(cap) => {
1179+
cap.diagnostic_options.identifier.clone()
1180+
}
1181+
};
1182+
1183+
let params = lsp::DocumentDiagnosticParams {
1184+
text_document,
1185+
identifier,
1186+
previous_result_id,
1187+
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
1188+
partial_result_params: lsp::PartialResultParams::default(),
1189+
};
1190+
1191+
Some(self.call::<lsp::request::DocumentDiagnosticRequest>(params))
1192+
}
1193+
11441194
pub fn text_document_document_highlight(
11451195
&self,
11461196
text_document: lsp::TextDocumentIdentifier,

helix-lsp/src/lib.rs

+91
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,97 @@ pub mod util {
120120
}
121121
}
122122

123+
/// Converts a [`lsp::Diagnostic`] to [`helix_core::Diagnostic`].
124+
pub fn lsp_diagnostic_to_diagnostic(
125+
params: &[lsp::Diagnostic],
126+
text: &helix_core::Rope,
127+
offset_encoding: OffsetEncoding,
128+
lang_conf: Option<&helix_core::syntax::LanguageConfiguration>,
129+
server_id: usize,
130+
) -> Vec<helix_core::Diagnostic> {
131+
params
132+
.iter()
133+
.filter_map(|diagnostic| {
134+
use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
135+
use lsp::DiagnosticSeverity;
136+
137+
// TODO: convert inside server
138+
let start = if let Some(start) =
139+
lsp_pos_to_pos(text, diagnostic.range.start, offset_encoding)
140+
{
141+
start
142+
} else {
143+
log::warn!("lsp position out of bounds - {:?}", diagnostic);
144+
return None;
145+
};
146+
147+
let end = if let Some(end) =
148+
lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
149+
{
150+
end
151+
} else {
152+
log::warn!("lsp position out of bounds - {:?}", diagnostic);
153+
return None;
154+
};
155+
156+
let severity = diagnostic.severity.map(|severity| match severity {
157+
DiagnosticSeverity::ERROR => Error,
158+
DiagnosticSeverity::WARNING => Warning,
159+
DiagnosticSeverity::INFORMATION => Info,
160+
DiagnosticSeverity::HINT => Hint,
161+
severity => unreachable!("unrecognized diagnostic severity: {:?}", severity),
162+
});
163+
164+
if let Some(lang_conf) = lang_conf {
165+
if let Some(severity) = severity {
166+
if severity < lang_conf.diagnostic_severity {
167+
return None;
168+
}
169+
}
170+
};
171+
172+
let code = match diagnostic.code.clone() {
173+
Some(x) => match x {
174+
lsp::NumberOrString::Number(x) => Some(NumberOrString::Number(x)),
175+
lsp::NumberOrString::String(x) => Some(NumberOrString::String(x)),
176+
},
177+
None => None,
178+
};
179+
180+
let tags = if let Some(tags) = &diagnostic.tags {
181+
let new_tags = tags
182+
.iter()
183+
.filter_map(|tag| match *tag {
184+
lsp::DiagnosticTag::DEPRECATED => {
185+
Some(helix_core::diagnostic::DiagnosticTag::Deprecated)
186+
}
187+
lsp::DiagnosticTag::UNNECESSARY => {
188+
Some(helix_core::diagnostic::DiagnosticTag::Unnecessary)
189+
}
190+
_ => None,
191+
})
192+
.collect();
193+
194+
new_tags
195+
} else {
196+
Vec::new()
197+
};
198+
199+
Some(Diagnostic {
200+
range: Range { start, end },
201+
line: diagnostic.range.start.line as usize,
202+
message: diagnostic.message.clone(),
203+
severity,
204+
code,
205+
tags,
206+
source: diagnostic.source.clone(),
207+
data: diagnostic.data.clone(),
208+
language_server_id: server_id,
209+
})
210+
})
211+
.collect()
212+
}
213+
123214
/// Converts [`lsp::Position`] to a position in the document.
124215
///
125216
/// Returns `None` if position.line is out of bounds or an overflow occurs

0 commit comments

Comments
 (0)