Skip to content
Merged
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
5 changes: 3 additions & 2 deletions crates/oxc_language_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ Initialization Options:
| Option Key | Value(s) | Default | Description |
| ------------ | ---------------------- | ---------------- | ---------------------------------------------------------------------------------------------------- |
| `run` | `"onSave" \| "onType"` | `"onType"` | Should the server lint the files when the user is typing or saving |
| `enable` | `true \| false` | `true` | Should the server lint files |
| `configPath` | `<string>` | `.oxlintrc.json` | Path to a oxlint configuration file, pass '' to enable nested configuration |
| `flags` | `Map<string, string>` | `<empty>` | Special oxc language server flags, currently only one flag key is supported: `disable_nested_config` |

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

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

The server will reset the diagnostics for all open files and send one or more [textDocument/publishDiagnostics](#textdocumentpublishdiagnostics) requests to the client.

### Workspace

#### [workspace/didChangeConfiguration](https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeConfiguration)

The server expects this request when settings like `run`, `enable`, `flags` or `configPath` are changed.
The server expects this request when settings like `run`, `flags` or `configPath` are changed.
The server will revalidate or reset the diagnostics for all open files and send one or more [textDocument/publishDiagnostics](#textdocumentpublishdiagnostics) requests to the client.

#### [workspace/didChangeWatchedFiles](https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWatchedFiles)
Expand Down
84 changes: 28 additions & 56 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,13 @@ enum Run {
#[serde(rename_all = "camelCase")]
struct Options {
run: Run,
enable: bool,
config_path: String,
flags: FxHashMap<String, String>,
}

impl Default for Options {
fn default() -> Self {
Self {
enable: true,
run: Run::default(),
config_path: OXC_CONFIG_FILE.into(),
flags: FxHashMap::default(),
Expand All @@ -71,17 +69,6 @@ impl Default for Options {
}

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 } else { Some(PathBuf::from(&self.config_path)) }
}
Expand All @@ -91,13 +78,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 @@ -155,40 +135,16 @@ impl LanguageServer for Backend {
"
);

if current_option.get_lint_level() != changed_options.get_lint_level()
&& changed_options.get_lint_level() == SyntheticRunLevel::Disable
{
debug!("lint level change detected {:?}", &changed_options.get_lint_level());
// clear all exists diagnostics when linter is disabled
let cleared_diagnostics = self
.diagnostics_report_map
.pin()
.keys()
.map(|uri| {
(
// should convert successfully, case the key is from `params.document.uri`
Url::from_str(uri)
.ok()
.and_then(|url| url.to_file_path().ok())
.expect("should convert to path"),
vec![],
)
})
.collect::<Vec<_>>();
self.publish_all_diagnostics(&cleared_diagnostics).await;
}

if changed_options.disable_nested_configs() {
self.nested_configs.pin().clear();
}

*self.options.lock().await = changed_options.clone();

// revalidate the config and all open files, when lint level is not disabled and the config path is changed
if changed_options.get_lint_level() != SyntheticRunLevel::Disable
&& changed_options
.get_config_path()
.is_some_and(|path| path.to_str().unwrap() != current_option.config_path)
// revalidate the config and all open files
if changed_options
.get_config_path()
.is_some_and(|path| path.to_str().unwrap() != current_option.config_path)
{
info!("config path change detected {:?}", &changed_options.get_config_path());
self.init_linter_config().await;
Expand Down Expand Up @@ -247,14 +203,15 @@ impl LanguageServer for Backend {
}

async fn shutdown(&self) -> Result<()> {
self.clear_all_diagnostics().await;
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 {
let run_level = { self.options.lock().await.run };
if run_level != Run::OnSave {
return;
}
let uri = params.text_document.uri;
Expand All @@ -267,8 +224,8 @@ impl LanguageServer for Backend {
/// 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 {
let run_level = { self.options.lock().await.run };
if run_level != Run::OnType {
return;
}

Expand All @@ -286,10 +243,6 @@ impl LanguageServer for Backend {
}

async fn did_open(&self, params: DidOpenTextDocumentParams) {
let run_level = { self.options.lock().await.get_lint_level() };
if run_level <= SyntheticRunLevel::Disable {
return;
}
if self.is_ignored(&params.text_document.uri).await {
return;
}
Expand Down Expand Up @@ -515,6 +468,25 @@ impl Backend {
}
}

async fn clear_all_diagnostics(&self) {
let cleared_diagnostics = self
.diagnostics_report_map
.pin()
.keys()
.map(|uri| {
(
// should convert successfully, case the key is from `params.document.uri`
Url::from_str(uri)
.ok()
.and_then(|url| url.to_file_path().ok())
.expect("should convert to path"),
vec![],
)
})
.collect::<Vec<_>>();
self.publish_all_diagnostics(&cleared_diagnostics).await;
}

#[expect(clippy::ptr_arg)]
async fn publish_all_diagnostics(&self, result: &Vec<(PathBuf, Vec<Diagnostic>)>) {
join_all(result.iter().map(|(path, diagnostics)| {
Expand Down
2 changes: 0 additions & 2 deletions editors/vscode/client/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ export class Config implements ConfigInterface {
public toLanguageServerConfig(): LanguageServerConfig {
return {
run: this.runTrigger,
enable: this.enable,
configPath: this.configPath,
flags: this.flags,
};
Expand All @@ -107,7 +106,6 @@ export class Config implements ConfigInterface {

interface LanguageServerConfig {
configPath: string;
enable: boolean;
run: Trigger;
flags: Record<string, string>;
}
Expand Down
33 changes: 28 additions & 5 deletions editors/vscode/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,14 @@ export async function activate(context: ExtensionContext) {
try {
if (client.isRunning()) {
await client.restart();
// ToDo: refactor it on the server side.
// Do not touch watchers on client side, just simplify the restart of the server.
const configFiles = await findOxlintrcConfigFiles();
await sendDidChangeWatchedFilesNotificationWith(client, configFiles);

window.showInformationMessage('oxc server restarted.');
} else {
await client.start();
await startClient();
}
} catch (err) {
client.error('Restarting client failed', err, 'force');
Expand All @@ -78,8 +82,18 @@ export async function activate(context: ExtensionContext) {

const toggleEnable = commands.registerCommand(
OxcCommands.ToggleEnable,
() => {
configService.config.updateEnable(!configService.config.enable);
async () => {
await configService.config.updateEnable(!configService.config.enable);

if (client.isRunning()) {
if (!configService.config.enable) {
await client.stop();
}
} else {
if (configService.config.enable) {
await startClient();
}
}
},
);

Expand Down Expand Up @@ -232,7 +246,7 @@ export async function activate(context: ExtensionContext) {

configService.onConfigChange = function onConfigChange(event) {
let settings = this.config.toLanguageServerConfig();
updateStatsBar(settings.enable);
updateStatsBar(this.config.enable);
client.sendNotification('workspace/didChangeConfiguration', { settings });

if (event.affectsConfiguration('oxc.configPath')) {
Expand Down Expand Up @@ -265,8 +279,17 @@ export async function activate(context: ExtensionContext) {
myStatusBarItem.backgroundColor = bgColor;
}
updateStatsBar(configService.config.enable);
await client.start();

if (configService.config.enable) {
await startClient();
}
}

// Starts the client, it does not check if it is already started
async function startClient() {
await client.start();
// ToDo: refactor it on the server side.
// Do not touch watchers on client side, just simplify the start of the server.
const configFiles = await findOxlintrcConfigFiles();
await sendDidChangeWatchedFilesNotificationWith(client, configFiles);
}
Expand Down