Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(language_server)!: add capability diagnosticProvider #7444

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions crates/oxc_language_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ This crate provides an [LSP](https://microsoft.github.io/language-server-protoco
- Workspace
- [Workspace Folders](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspaceFoldersServerCapabilities): `true`
- File Operations: `false`
- [Diagnostic Provider](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics)
- `interFileDependencies`: `false`
- `workspaceDiagnostics`: `false`
- [Code Actions Provider](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionKind):
- `quickfix`

Expand All @@ -34,20 +37,20 @@ The server will revalidate the diagnostics for all open files and send one or mo

#### [textDocument/didOpen](https://microsoft.github.io/language-server-protocol/specification#textDocument_didOpen)

The server will validate the file content and send a [textDocument/publishDiagnostics](#textdocumentpublishdiagnostics) request to the client.

#### [textDocument/didSave](https://microsoft.github.io/language-server-protocol/specification#textDocument_didSave)

When the configuration `run` is set to `onSave`, the server will validate the file content and send a [textDocument/publishDiagnostics](#textdocumentpublishdiagnostics) request to the client.
It will save the reference internal.

#### [textDocument/didChange](https://microsoft.github.io/language-server-protocol/specification#textDocument_didChange)

When the configuration `run` is set to `onType`, the server will validate the file content and send a [textDocument/publishDiagnostics](#textdocumentpublishdiagnostics) request to the client.
It will update the reference internal.

#### [textDocument/didClose](https://microsoft.github.io/language-server-protocol/specification#textDocument_didClose)

It will remove the reference internal.

#### [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specification#textDocument_diagnostic)

Returns all Diagnostics for the requested file

#### [textDocument/codeAction](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction)

Returns a list of [CodeAction](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_codeAction)
Expand Down
191 changes: 100 additions & 91 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ use tower_lsp::{
lsp_types::{
CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams,
CodeActionProviderCapability, CodeActionResponse, ConfigurationItem, Diagnostic,
DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
InitializeParams, InitializeResult, InitializedParams, OneOf, ServerCapabilities,
ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url,
WorkDoneProgressOptions, WorkspaceEdit, WorkspaceFoldersServerCapabilities,
DiagnosticOptions, DiagnosticServerCapabilities, DidChangeConfigurationParams,
DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DocumentDiagnosticParams, DocumentDiagnosticReport,
DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, InitializeParams,
InitializeResult, InitializedParams, OneOf, RelatedFullDocumentDiagnosticReport,
ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit,
Url, WorkDoneProgressOptions, WorkspaceEdit, WorkspaceFoldersServerCapabilities,
WorkspaceServerCapabilities,
},
Client, LanguageServer, LspService, Server,
Expand All @@ -31,43 +33,26 @@ struct Backend {
client: Client,
root_uri: OnceCell<Option<Url>>,
server_linter: RwLock<ServerLinter>,
document_content_cache: DashMap<String, String>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do know now if this could lead to performance problems. We need to keep all content of any open file in the memory.

diagnostics_report_map: DashMap<String, Vec<DiagnosticReport>>,
options: Mutex<Options>,
gitignore_glob: Mutex<Vec<Gitignore>>,
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, PartialOrd, Clone, Copy)]
#[serde(rename_all = "camelCase")]
enum Run {
OnSave,
#[default]
OnType,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct Options {
run: Run,
enable: bool,
config_path: String,
}

impl Default for Options {
fn default() -> Self {
Self { enable: true, run: Run::default(), config_path: ".eslintrc".into() }
Self { enable: true, config_path: ".eslintrc".into() }
}
}

impl Options {
fn get_lint_level(&self) -> SyntheticRunLevel {
if self.enable {
match self.run {
Run::OnSave => SyntheticRunLevel::OnSave,
Run::OnType => SyntheticRunLevel::OnType,
}
} else {
SyntheticRunLevel::Disable
}
}

fn get_config_path(&self) -> Option<PathBuf> {
if self.config_path.is_empty() {
None
Expand All @@ -77,13 +62,6 @@ impl Options {
}
}

#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
enum SyntheticRunLevel {
Disable,
OnSave,
OnType,
}

#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
Expand Down Expand Up @@ -114,6 +92,14 @@ impl LanguageServer for Backend {
}),
file_operations: None,
}),
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(
DiagnosticOptions {
identifier: Some("oxc".into()),
inter_file_dependencies: false,
workspace_diagnostics: false,
work_done_progress_options: WorkDoneProgressOptions::default(),
},
)),
code_action_provider: Some(CodeActionProviderCapability::Options(
CodeActionOptions {
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
Expand Down Expand Up @@ -151,10 +137,10 @@ impl LanguageServer for Backend {
options
};

debug!("{:?}", &changed_options.get_lint_level());
if changed_options.get_lint_level() == SyntheticRunLevel::Disable {
debug!("{:?}", &changed_options.enable);
if !changed_options.enable {
// clear all exists diagnostics when linter is disabled
let opened_files = self.diagnostics_report_map.iter().map(|k| k.key().to_string());
let opened_files = self.document_content_cache.iter().map(|k| k.key().to_string());
let cleared_diagnostics = opened_files
.into_iter()
.map(|uri| {
Expand All @@ -169,6 +155,7 @@ impl LanguageServer for Backend {
})
.collect::<Vec<_>>();
self.publish_all_diagnostics(&cleared_diagnostics).await;
self.diagnostics_report_map.clear();
}
*self.options.lock().await = changed_options;
}
Expand All @@ -187,56 +174,60 @@ impl LanguageServer for Backend {
Ok(())
}

async fn did_save(&self, params: DidSaveTextDocumentParams) {
debug!("oxc server did save");
// drop as fast as possible
let run_level = { self.options.lock().await.get_lint_level() };
if run_level < SyntheticRunLevel::OnSave {
return;
}
let uri = params.text_document.uri;
if self.is_ignored(&uri).await {
return;
}
self.handle_file_update(uri, None, None).await;
async fn did_open(&self, params: DidOpenTextDocumentParams) {
self.document_content_cache
.insert(params.text_document.uri.to_string(), params.text_document.text);
}

async fn did_close(&self, params: DidCloseTextDocumentParams) {
self.document_content_cache.remove(&params.text_document.uri.to_string());
}

/// When the document changed, it may not be written to disk, so we should
/// get the file context from the language client
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let run_level = { self.options.lock().await.get_lint_level() };
if run_level < SyntheticRunLevel::OnType {
return;
}
let content = params.content_changes.first().map(|c| c.text.clone());

let uri = &params.text_document.uri;
if self.is_ignored(uri).await {
return;
if let Some(content) = content {
self.document_content_cache.insert(params.text_document.uri.to_string(), content);
}
let content = params.content_changes.first().map(|c| c.text.clone());
self.handle_file_update(
params.text_document.uri,
content,
Some(params.text_document.version),
)
.await;
}

async fn did_open(&self, params: DidOpenTextDocumentParams) {
let run_level = { self.options.lock().await.get_lint_level() };
if run_level <= SyntheticRunLevel::Disable {
return;
}
async fn diagnostic(
&self,
params: DocumentDiagnosticParams,
) -> Result<DocumentDiagnosticReportResult> {
// the file is ignored, return empty result
if self.is_ignored(&params.text_document.uri).await {
return;
return Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport::default(),
},
)));
}
self.handle_file_update(params.text_document.uri, None, Some(params.text_document.version))
.await;
}

async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri.to_string();
self.diagnostics_report_map.remove(&uri);
let content = self
.document_content_cache
.get(&params.text_document.uri.to_string())
.map(|entry| entry.value().to_owned());

let Some(result) = self.lint_uri(&params.text_document.uri, content).await else {
return Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport::default(),
},
)));
};

Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
items: result.into_iter().map(|report| report.diagnostic).collect(),
..FullDocumentDiagnosticReport::default()
},
},
)))
}

async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
Expand Down Expand Up @@ -347,10 +338,12 @@ impl Backend {
}

async fn revalidate_open_files(&self) {
join_all(self.diagnostics_report_map.iter().map(|map| {
let url = Url::from_str(map.key()).expect("should convert to path");

self.handle_file_update(url, None, None)
join_all(self.document_content_cache.iter().map(|map| {
self.lint_file_and_publish_diagnostic(
Url::from_str(map.key()).unwrap(),
Some(map.value().clone()),
None,
)
}))
.await;
}
Expand Down Expand Up @@ -381,19 +374,33 @@ impl Backend {
}
}

async fn handle_file_update(&self, uri: Url, content: Option<String>, version: Option<i32>) {
if let Some(Some(_root_uri)) = self.root_uri.get() {
if let Some(diagnostics) = self.server_linter.read().await.run_single(&uri, content) {
self.client
.publish_diagnostics(
uri.clone(),
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
version,
)
.await;
async fn lint_uri(&self, uri: &Url, content: Option<String>) -> Option<Vec<DiagnosticReport>> {
let Some(Some(_root_uri)) = self.root_uri.get() else {
return None;
};

self.diagnostics_report_map.insert(uri.to_string(), diagnostics);
}
let result = self.server_linter.read().await.run_single(uri, content);

if result.is_some() {
self.diagnostics_report_map.insert(uri.to_string(), result.clone().unwrap());
}

result
}
async fn lint_file_and_publish_diagnostic(
&self,
uri: Url,
content: Option<String>,
version: Option<i32>,
) {
if let Some(diagnostics) = self.lint_uri(&uri, content).await {
self.client
.publish_diagnostics(
uri.clone(),
diagnostics.clone().into_iter().map(|d| d.diagnostic).collect(),
version,
)
.await;
}
}

Expand Down Expand Up @@ -435,11 +442,13 @@ async fn main() {

let server_linter = ServerLinter::new();
let diagnostics_report_map = DashMap::new();
let document_content_cache = DashMap::new();

let (service, socket) = LspService::build(|client| Backend {
client,
root_uri: OnceCell::new(),
server_linter: RwLock::new(server_linter),
document_content_cache,
diagnostics_report_map,
options: Mutex::new(Options::default()),
gitignore_glob: Mutex::new(vec![]),
Expand Down
14 changes: 14 additions & 0 deletions editors/vscode/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ export async function activate(context: ExtensionContext) {
},
outputChannel,
traceOutputChannel: outputChannel,
diagnosticPullOptions: {
onChange: true,
onSave: true,
onTabs: false,
filter: (_, mode) => {
if (mode === 'onType' && configService.config.runTrigger !== 'onType') {
return true;
} else if (mode === 'onSave' && configService.config.runTrigger !== 'onSave') {
return true;
}

return !configService.config.enable;
},
},
};

// Create the language client and start the client.
Expand Down
Loading