Skip to content

Commit

Permalink
feat(lsp): support textDocument/diagnostic specification
Browse files Browse the repository at this point in the history
Implementation of pull diagnostics introduced in LSP 3.17.
  • Loading branch information
woojiq committed Aug 12, 2023
1 parent 1bd0410 commit 8d64610
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 137 deletions.
9 changes: 7 additions & 2 deletions helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,22 +235,26 @@ impl<'de> Deserialize<'de> for FileType {
#[serde(rename_all = "kebab-case")]
pub enum LanguageServerFeature {
Format,
// Goto, use bitflags, combining previous Goto members?
GotoDeclaration,
GotoDefinition,
GotoTypeDefinition,
GotoReference,
GotoImplementation,
// Goto, use bitflags, combining previous Goto members?

SignatureHelp,
Hover,
DocumentHighlight,
Completion,
CodeAction,
WorkspaceCommand,
// Symbols, use bitflags, see above?
DocumentSymbols,
WorkspaceSymbols,
// Symbols, use bitflags, see above?

// Diagnostic in any form that is displayed on the gutter and screen
Diagnostics,
PullDiagnostics,
RenameSymbol,
InlayHints,
}
Expand All @@ -274,6 +278,7 @@ impl Display for LanguageServerFeature {
DocumentSymbols => "document-symbols",
WorkspaceSymbols => "workspace-symbols",
Diagnostics => "diagnostics",
PullDiagnostics => "pull-diagnostics",
RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints",
};
Expand Down
56 changes: 53 additions & 3 deletions helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ use lsp_types as lsp;
use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Value;
use std::future::Future;
use std::process::Stdio;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use std::{collections::HashMap, path::PathBuf};
use std::{future::Future, sync::atomic::AtomicBool};
use std::{process::Stdio, sync::atomic};
use tokio::{
io::{BufReader, BufWriter},
process::{Child, Command},
Expand Down Expand Up @@ -57,6 +57,9 @@ pub struct Client {
initialize_notify: Arc<Notify>,
/// workspace folders added while the server is still initializing
req_timeout: u64,
// there is no server capability to know if server supports it
// set to true on first PublishDiagnostic notification
supports_publish_diagnostic: AtomicBool,
}

impl Client {
Expand Down Expand Up @@ -238,6 +241,7 @@ impl Client {
root_uri,
workspace_folders: Mutex::new(workspace_folders),
initialize_notify: initialize_notify.clone(),
supports_publish_diagnostic: AtomicBool::new(false),
};

Ok((client, server_rx, initialize_notify))
Expand Down Expand Up @@ -277,6 +281,17 @@ impl Client {
.expect("language server not yet initialized!")
}

pub fn set_publish_diagnostic(&self, val: bool) {
self.supports_publish_diagnostic
.fetch_or(val, atomic::Ordering::Relaxed);
}

/// Whether the server supports Publish Diagnostic
pub fn publish_diagnostic(&self) -> bool {
self.supports_publish_diagnostic
.load(atomic::Ordering::Relaxed)
}

/// Client has to be initialized otherwise this function panics
#[inline]
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
Expand Down Expand Up @@ -346,7 +361,12 @@ impl Client {
capabilities.workspace_symbol_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::Diagnostics => true, // there's no extra server capability
LanguageServerFeature::Diagnostics => {
self.publish_diagnostic() || matches!(capabilities.diagnostic_provider, Some(_))
}
LanguageServerFeature::PullDiagnostics => {
matches!(capabilities.diagnostic_provider, Some(_))
}
LanguageServerFeature::RenameSymbol => matches!(
capabilities.rename_provider,
Some(OneOf::Left(true)) | Some(OneOf::Right(_))
Expand Down Expand Up @@ -630,6 +650,10 @@ impl Client {
dynamic_registration: Some(false),
resolve_support: None,
}),
diagnostic: Some(lsp::DiagnosticClientCapabilities {
dynamic_registration: Some(false),
related_document_support: Some(true),
}),
..Default::default()
}),
window: Some(lsp::WindowClientCapabilities {
Expand Down Expand Up @@ -1141,6 +1165,32 @@ impl Client {
})
}

pub fn text_document_diagnostic(
&self,
text_document: lsp::TextDocumentIdentifier,
previous_result_id: Option<String>,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();

// Return early if the server does not support pull diagnostic.
let identifier = match capabilities.diagnostic_provider.as_ref()? {
lsp::DiagnosticServerCapabilities::Options(cap) => cap.identifier.clone(),
lsp::DiagnosticServerCapabilities::RegistrationOptions(cap) => {
cap.diagnostic_options.identifier.clone()
}
};

let params = lsp::DocumentDiagnosticParams {
text_document,
identifier,
previous_result_id,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};

Some(self.call::<lsp::request::DocumentDiagnosticRequest>(params))
}

pub fn text_document_document_highlight(
&self,
text_document: lsp::TextDocumentIdentifier,
Expand Down
91 changes: 91 additions & 0 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,97 @@ pub mod util {
}
}

/// Converts a [`lsp::Diagnostic`] to [`helix_core::Diagnostic`].
pub fn lsp_diagnostic_to_diagnostic(
params: &[lsp::Diagnostic],
text: &helix_core::Rope,
offset_encoding: OffsetEncoding,
lang_conf: Option<&helix_core::syntax::LanguageConfiguration>,
server_id: usize,
) -> Vec<helix_core::Diagnostic> {
params
.iter()
.filter_map(|diagnostic| {
use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
use lsp::DiagnosticSeverity;

// TODO: convert inside server
let start = if let Some(start) =
lsp_pos_to_pos(text, diagnostic.range.start, offset_encoding)
{
start
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};

let end = if let Some(end) =
lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
{
end
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};

let severity = diagnostic.severity.map(|severity| match severity {
DiagnosticSeverity::ERROR => Error,
DiagnosticSeverity::WARNING => Warning,
DiagnosticSeverity::INFORMATION => Info,
DiagnosticSeverity::HINT => Hint,
severity => unreachable!("unrecognized diagnostic severity: {:?}", severity),
});

if let Some(lang_conf) = lang_conf {
if let Some(severity) = severity {
if severity < lang_conf.diagnostic_severity {
return None;
}
}
};

let code = match diagnostic.code.clone() {
Some(x) => match x {
lsp::NumberOrString::Number(x) => Some(NumberOrString::Number(x)),
lsp::NumberOrString::String(x) => Some(NumberOrString::String(x)),
},
None => None,
};

let tags = if let Some(tags) = &diagnostic.tags {
let new_tags = tags
.iter()
.filter_map(|tag| match *tag {
lsp::DiagnosticTag::DEPRECATED => {
Some(helix_core::diagnostic::DiagnosticTag::Deprecated)
}
lsp::DiagnosticTag::UNNECESSARY => {
Some(helix_core::diagnostic::DiagnosticTag::Unnecessary)
}
_ => None,
})
.collect();

new_tags
} else {
Vec::new()
};

Some(Diagnostic {
range: Range { start, end },
line: diagnostic.range.start.line as usize,
message: diagnostic.message.clone(),
severity,
code,
tags,
source: diagnostic.source.clone(),
data: diagnostic.data.clone(),
language_server_id: server_id,
})
})
.collect()
}

/// Converts [`lsp::Position`] to a position in the document.
///
/// Returns `None` if position.line is out of bounds or an overflow occurs
Expand Down
Loading

0 comments on commit 8d64610

Please sign in to comment.