Skip to content

Commit 8440072

Browse files
committed
feat(language_server): support textDocument/diagnostic
1 parent f729734 commit 8440072

File tree

9 files changed

+184
-56
lines changed

9 files changed

+184
-56
lines changed

crates/oxc_language_server/src/capabilities.rs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
use tower_lsp_server::lsp_types::{
22
ClientCapabilities, CodeActionKind, CodeActionOptions, CodeActionProviderCapability,
3-
ExecuteCommandOptions, OneOf, SaveOptions, ServerCapabilities, TextDocumentSyncCapability,
4-
TextDocumentSyncKind, TextDocumentSyncOptions, TextDocumentSyncSaveOptions,
5-
WorkDoneProgressOptions, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
3+
DiagnosticOptions, DiagnosticServerCapabilities, ExecuteCommandOptions, OneOf, SaveOptions,
4+
ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
5+
TextDocumentSyncSaveOptions, WorkDoneProgressOptions, WorkspaceFoldersServerCapabilities,
6+
WorkspaceServerCapabilities,
67
};
78

89
use crate::{code_actions::CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC, commands::FIX_ALL_COMMAND_ID};
910

11+
/// Represents the capabilities of the client that the server can use to determine
12+
/// which features to enable or disable.
1013
#[derive(Clone, Default)]
1114
pub struct Capabilities {
1215
pub code_action_provider: bool,
1316
pub workspace_apply_edit: bool,
1417
pub workspace_execute_command: bool,
1518
pub workspace_configuration: bool,
19+
/// Whether the client supports dynamic registration of file watchers.
1620
pub dynamic_watchers: bool,
21+
/// Whether the client supports pull diagnostics.
22+
pub pull_diagnostics: bool,
23+
/// Whether the client supports the `workspace/diagnostic/refresh` request.
24+
pub refresh_diagnostics: bool,
25+
}
26+
27+
impl Capabilities {
28+
/// The server supports pull and push diagnostics.
29+
/// Only use push diagnostics if the client does not support pull diagnostics,
30+
/// or we can not the client to refresh diagnostics.
31+
pub fn use_push_diagnostics(&self) -> bool {
32+
!self.pull_diagnostics || !self.refresh_diagnostics
33+
}
1734
}
1835

1936
impl From<ClientCapabilities> for Capabilities {
@@ -34,18 +51,32 @@ impl From<ClientCapabilities> for Capabilities {
3451
.workspace
3552
.as_ref()
3653
.is_some_and(|workspace| workspace.configuration.is_some_and(|config| config));
37-
let dynamic_watchers = value.workspace.is_some_and(|workspace| {
38-
workspace.did_change_watched_files.is_some_and(|watched_files| {
54+
let dynamic_watchers = value.workspace.as_ref().is_some_and(|workspace| {
55+
workspace.did_change_watched_files.as_ref().is_some_and(|watched_files| {
3956
watched_files.dynamic_registration.is_some_and(|dynamic| dynamic)
4057
})
4158
});
4259

60+
let pull_diagnostics = value
61+
.text_document
62+
.as_ref()
63+
.is_some_and(|text_document| text_document.diagnostic.is_some());
64+
65+
let refresh_diagnostics = value.workspace.as_ref().is_some_and(|workspace| {
66+
workspace
67+
.diagnostic
68+
.as_ref()
69+
.is_some_and(|diagnostic| diagnostic.refresh_support.is_some_and(|refresh| refresh))
70+
});
71+
4372
Self {
4473
code_action_provider,
4574
workspace_apply_edit,
4675
workspace_execute_command,
4776
workspace_configuration,
4877
dynamic_watchers,
78+
pull_diagnostics,
79+
refresh_diagnostics,
4980
}
5081
}
5182
}
@@ -92,6 +123,11 @@ impl From<Capabilities> for ServerCapabilities {
92123
} else {
93124
None
94125
},
126+
diagnostic_provider: if value.pull_diagnostics {
127+
Some(DiagnosticServerCapabilities::Options(DiagnosticOptions::default()))
128+
} else {
129+
None
130+
},
95131
..ServerCapabilities::default()
96132
}
97133
}

crates/oxc_language_server/src/main.rs

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ use tower_lsp_server::{
1313
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
1414
DidChangeWatchedFilesRegistrationOptions, DidChangeWorkspaceFoldersParams,
1515
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
16-
ExecuteCommandParams, InitializeParams, InitializeResult, InitializedParams, Registration,
17-
ServerInfo, Unregistration, Uri, WorkspaceEdit,
16+
DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult,
17+
ExecuteCommandParams, FullDocumentDiagnosticReport, InitializeParams, InitializeResult,
18+
InitializedParams, Registration, RelatedFullDocumentDiagnosticReport, ServerInfo,
19+
Unregistration, Uri, WorkspaceEdit,
1820
},
1921
};
2022
// #
@@ -464,14 +466,22 @@ impl LanguageServer for Backend {
464466
if !worker.should_lint_on_run_type(Run::OnSave).await {
465467
return;
466468
}
467-
if let Some(diagnostics) = worker.lint_file(uri, None).await {
468-
self.client
469-
.publish_diagnostics(
470-
uri.clone(),
471-
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
472-
None,
473-
)
474-
.await;
469+
470+
// we no longer need the cached document, as it is saved to disk
471+
worker.remove_cached_document(uri);
472+
473+
// if the client supports pull diagnostics, we should not lint the file immediately,
474+
// instead wait for the client to request diagnostics
475+
if self.capabilities.get().is_some_and(Capabilities::use_push_diagnostics) {
476+
if let Some(diagnostics) = worker.lint_file(uri).await {
477+
self.client
478+
.publish_diagnostics(
479+
uri.clone(),
480+
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
481+
None,
482+
)
483+
.await;
484+
}
475485
}
476486
}
477487

@@ -487,14 +497,24 @@ impl LanguageServer for Backend {
487497
return;
488498
}
489499
let content = params.content_changes.first().map(|c| c.text.clone());
490-
if let Some(diagnostics) = worker.lint_file(uri, content).await {
491-
self.client
492-
.publish_diagnostics(
493-
uri.clone(),
494-
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
495-
Some(params.text_document.version),
496-
)
497-
.await;
500+
501+
if let Some(content) = content {
502+
// cache the document content for later use
503+
worker.cache_document(uri, content);
504+
}
505+
506+
// if the client supports pull diagnostics, we should not lint the file immediately,
507+
// instead wait for the client to request diagnostics
508+
if self.capabilities.get().is_some_and(Capabilities::use_push_diagnostics) {
509+
if let Some(diagnostics) = worker.lint_file(uri).await {
510+
self.client
511+
.publish_diagnostics(
512+
uri.clone(),
513+
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
514+
Some(params.text_document.version),
515+
)
516+
.await;
517+
}
498518
}
499519
}
500520

@@ -505,15 +525,21 @@ impl LanguageServer for Backend {
505525
return;
506526
};
507527

508-
let content = params.text_document.text;
509-
if let Some(diagnostics) = worker.lint_file(uri, Some(content)).await {
510-
self.client
511-
.publish_diagnostics(
512-
uri.clone(),
513-
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
514-
Some(params.text_document.version),
515-
)
516-
.await;
528+
// cache the document content for later use
529+
worker.cache_document(uri, params.text_document.text);
530+
531+
// if the client supports pull diagnostics, we should not lint the file immediately,
532+
// instead wait for the client to request diagnostics
533+
if self.capabilities.get().is_some_and(Capabilities::use_push_diagnostics) {
534+
if let Some(diagnostics) = worker.lint_file(uri).await {
535+
self.client
536+
.publish_diagnostics(
537+
uri.clone(),
538+
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
539+
Some(params.text_document.version),
540+
)
541+
.await;
542+
}
517543
}
518544
}
519545

@@ -523,7 +549,38 @@ impl LanguageServer for Backend {
523549
let Some(worker) = workers.iter().find(|worker| worker.is_responsible_for_uri(uri)) else {
524550
return;
525551
};
526-
worker.remove_diagnostics(&params.text_document.uri);
552+
worker.remove_cached_document(uri);
553+
worker.remove_diagnostics(uri);
554+
}
555+
556+
async fn diagnostic(
557+
&self,
558+
params: DocumentDiagnosticParams,
559+
) -> Result<DocumentDiagnosticReportResult> {
560+
let uri = &params.text_document.uri;
561+
let workers = self.workspace_workers.read().await;
562+
let Some(worker) = workers.iter().find(|worker| worker.is_responsible_for_uri(uri)) else {
563+
return Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
564+
RelatedFullDocumentDiagnosticReport::default(),
565+
)));
566+
};
567+
let diagnostics = worker.lint_file(uri).await;
568+
569+
if let Some(diagnostics) = diagnostics {
570+
Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
571+
RelatedFullDocumentDiagnosticReport {
572+
full_document_diagnostic_report: FullDocumentDiagnosticReport {
573+
items: diagnostics.into_iter().map(|d| d.diagnostic).collect(),
574+
..Default::default()
575+
},
576+
..Default::default()
577+
},
578+
)))
579+
} else {
580+
Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
581+
RelatedFullDocumentDiagnosticReport::default(),
582+
)))
583+
}
527584
}
528585

529586
async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {

crates/oxc_language_server/src/tester.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ impl Tester<'_> {
115115
let reports = tokio::runtime::Runtime::new().unwrap().block_on(async {
116116
self.create_workspace_worker()
117117
.await
118-
.lint_file(&uri, None)
118+
.lint_file(&uri)
119119
.await
120120
.expect("lint file is ignored")
121121
});

crates/oxc_language_server/src/worker.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub struct WorkspaceWorker {
2727
root_uri: Uri,
2828
server_linter: RwLock<Option<ServerLinter>>,
2929
diagnostics_report_map: Arc<ConcurrentHashMap<String, Vec<DiagnosticReport>>>,
30+
document_cache: Arc<ConcurrentHashMap<String, String>>,
3031
options: Mutex<Options>,
3132
}
3233

@@ -36,6 +37,7 @@ impl WorkspaceWorker {
3637
root_uri,
3738
server_linter: RwLock::new(None),
3839
diagnostics_report_map: Arc::new(ConcurrentHashMap::default()),
40+
document_cache: Arc::new(ConcurrentHashMap::default()),
3941
options: Mutex::new(Options::default()),
4042
}
4143
}
@@ -56,6 +58,14 @@ impl WorkspaceWorker {
5658
*self.server_linter.write().await = Some(ServerLinter::new(&self.root_uri, options));
5759
}
5860

61+
pub fn cache_document(&self, uri: &Uri, content: String) {
62+
self.document_cache.pin().insert(uri.to_string(), content);
63+
}
64+
65+
pub fn remove_cached_document(&self, uri: &Uri) {
66+
self.document_cache.pin().remove(&uri.to_string());
67+
}
68+
5969
// WARNING: start all programs (linter, formatter) before calling this function
6070
// each program can tell us customized file watcher patterns
6171
pub async fn init_watchers(&self) -> Vec<FileSystemWatcher> {
@@ -135,12 +145,8 @@ impl WorkspaceWorker {
135145
run_level == current_run
136146
}
137147

138-
pub async fn lint_file(
139-
&self,
140-
uri: &Uri,
141-
content: Option<String>,
142-
) -> Option<Vec<DiagnosticReport>> {
143-
let diagnostics = self.lint_file_internal(uri, content).await;
148+
pub async fn lint_file(&self, uri: &Uri) -> Option<Vec<DiagnosticReport>> {
149+
let diagnostics = self.lint_file_internal(uri).await;
144150

145151
if let Some(diagnostics) = &diagnostics {
146152
self.update_diagnostics(uri, diagnostics);
@@ -149,16 +155,15 @@ impl WorkspaceWorker {
149155
diagnostics
150156
}
151157

152-
async fn lint_file_internal(
153-
&self,
154-
uri: &Uri,
155-
content: Option<String>,
156-
) -> Option<Vec<DiagnosticReport>> {
158+
async fn lint_file_internal(&self, uri: &Uri) -> Option<Vec<DiagnosticReport>> {
157159
let Some(server_linter) = &*self.server_linter.read().await else {
158160
return None;
159161
};
160162

161-
server_linter.run_single(uri, content)
163+
let cache = self.document_cache.pin();
164+
let content = cache.get(&uri.to_string());
165+
166+
server_linter.run_single(uri, content.cloned())
162167
}
163168

164169
fn update_diagnostics(&self, uri: &Uri, diagnostics: &[DiagnosticReport]) {
@@ -208,7 +213,7 @@ impl WorkspaceWorker {
208213
Some(value) => value,
209214
// code actions / commands can be requested without opening the file
210215
// we just internally lint and provide the code actions / commands without refreshing the diagnostic map.
211-
None => &self.lint_file_internal(uri, None).await.unwrap_or_default(),
216+
None => &self.lint_file_internal(uri).await.unwrap_or_default(),
212217
};
213218

214219
if value.is_empty() {
@@ -250,7 +255,7 @@ impl WorkspaceWorker {
250255
Some(value) => value,
251256
// code actions / commands can be requested without opening the file
252257
// we just internally lint and provide the code actions / commands without refreshing the diagnostic map.
253-
None => &self.lint_file_internal(uri, None).await.unwrap_or_default(),
258+
None => &self.lint_file_internal(uri).await.unwrap_or_default(),
254259
};
255260

256261
if value.is_empty() {

editors/vscode/client/ConfigService.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ConfigurationChangeEvent, Uri, workspace, WorkspaceFolder } from 'vscode';
2+
import { DiagnosticPullMode } from 'vscode-languageclient';
23
import { IDisposable } from './types';
34
import { VSCodeConfig } from './VSCodeConfig';
45
import { WorkspaceConfig, WorkspaceConfigInterface } from './WorkspaceConfig';
@@ -79,6 +80,24 @@ export class ConfigService implements IDisposable {
7980
}
8081
}
8182

83+
public shouldRequestDiagnostics(
84+
textDocumentUri: Uri,
85+
diagnosticPullMode: DiagnosticPullMode,
86+
): boolean {
87+
if (!this.vsCodeConfig.enable) {
88+
return false;
89+
}
90+
91+
const textDocumentPath = textDocumentUri.path;
92+
93+
for (const [workspaceUri, workspaceConfig] of this.workspaceConfigs.entries()) {
94+
if (textDocumentPath.startsWith(workspaceUri)) {
95+
return workspaceConfig.shouldRequestDiagnostics(diagnosticPullMode);
96+
}
97+
}
98+
return false;
99+
}
100+
82101
dispose() {
83102
for (const disposable of this._disposables) {
84103
disposable.dispose();

0 commit comments

Comments
 (0)