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

refactor(language_server): add own ServerCapabilities struct #7458

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
211 changes: 95 additions & 116 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod linter;
mod server_capabilities;

use std::{fmt::Debug, path::PathBuf, str::FromStr};

Expand All @@ -13,61 +14,43 @@ use tokio::sync::{Mutex, OnceCell, RwLock, SetError};
use tower_lsp::{
jsonrpc::{Error, ErrorCode, Result},
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,
WorkspaceServerCapabilities,
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
ConfigurationItem, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams,
DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult,
FullDocumentDiagnosticReport, InitializeParams, InitializeResult, InitializedParams,
RelatedFullDocumentDiagnosticReport, ServerInfo, TextEdit, Url, WorkspaceEdit,
},
Client, LanguageServer, LspService, Server,
};

use crate::linter::{DiagnosticReport, ServerLinter};
use crate::server_capabilities::ServerCapabilities;

struct Backend {
client: Client,
root_uri: OnceCell<Option<Url>>,
server_linter: RwLock<ServerLinter>,
document_content_cache: DashMap<String, String>,
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 +60,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 All @@ -100,31 +76,11 @@ impl LanguageServer for Backend {
*self.options.lock().await = value;
}
self.init_linter_config().await;

Ok(InitializeResult {
server_info: Some(ServerInfo { name: "oxc".into(), version: None }),
offset_encoding: None,
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::FULL,
)),
workspace: Some(WorkspaceServerCapabilities {
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
file_operations: None,
}),
code_action_provider: Some(CodeActionProviderCapability::Options(
CodeActionOptions {
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
resolve_provider: None,
},
)),
..ServerCapabilities::default()
},
capabilities: ServerCapabilities::from(params.capabilities).into(),
})
}

Expand All @@ -151,10 +107,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 +125,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 +144,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 +308,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 +344,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 +412,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
Loading
Loading