Skip to content
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
20 changes: 10 additions & 10 deletions crates/oxc_language_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ This crate provides an [LSP](https://microsoft.github.io/language-server-protoco

These options can be passed with [initialize](#initialize), [workspace/didChangeConfiguration](#workspace/didChangeConfiguration) and [workspace/configuration](#workspace/configuration).

| Option Key | Value(s) | Default | Description |
| ------------------------- | ------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `run` | `"onSave" \| "onType"` | `"onType"` | Should the server lint the files when the user is typing or saving |
| `configPath` | `<string>` \| `null` | `null` | Path to a oxlint configuration file, passing a string will disable nested configuration |
| `tsConfigPath` | `<string>` \| `null` | `null` | Path to a TypeScript configuration file. If your `tsconfig.json` is not at the root, alias paths will not be resolve correctly for the `import` plugin |
| `unusedDisableDirectives` | `"allow" \| "warn"` \| "deny"` | `"allow"` | Define how directive comments like `// oxlint-disable-line` should be reported, when no errors would have been reported on that line anyway |
| `typeAware` | `true` \| `false` | `false` | Enables type-aware linting |
| `flags` | `Map<string, string>` | `<empty>` | Special oxc language server flags, currently only one flag key is supported: `disable_nested_config` |
| `fmt.experimental` | `true` \| `false` | `false` | Enables experimental formatting with `oxc_formatter` |
| `fmt.configPath` | `<string>` \| `null` | `null` | Path to a oxfmt configuration file, when `null` is passed, the server will use `.oxfmtrc.json` and the workspace root |
| Option Key | Value(s) | Default | Description |
| ------------------------- | ------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `run` | `"onSave" \| "onType" \| "off"` | `"onType"` | Should the server lint the files when the user is typing or saving |
| `configPath` | `<string>` \| `null` | `null` | Path to a oxlint configuration file, passing a string will disable nested configuration |
| `tsConfigPath` | `<string>` \| `null` | `null` | Path to a TypeScript configuration file. If your `tsconfig.json` is not at the root, alias paths will not be resolve correctly for the `import` plugin |
| `unusedDisableDirectives` | `"allow" \| "warn"` \| "deny"` | `"allow"` | Define how directive comments like `// oxlint-disable-line` should be reported, when no errors would have been reported on that line anyway |
| `typeAware` | `true` \| `false` | `false` | Enables type-aware linting |
| `flags` | `Map<string, string>` | `<empty>` | Special oxc language server flags, currently only one flag key is supported: `disable_nested_config` |
| `fmt.experimental` | `true` \| `false` | `false` | Enables experimental formatting with `oxc_formatter` |
| `fmt.configPath` | `<string>` \| `null` | `null` | Path to a oxfmt configuration file, when `null` is passed, the server will use `.oxfmtrc.json` and the workspace root |

## Supported LSP Specifications from Server

Expand Down
1 change: 1 addition & 0 deletions crates/oxc_language_server/src/linter/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub enum Run {
OnSave,
#[default]
OnType,
Off,
}

#[derive(Debug, Default, Serialize, Clone)]
Expand Down
9 changes: 7 additions & 2 deletions crates/oxc_language_server/src/linter/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,8 @@ impl ServerLinter {
// run only oxlint on type
// tsgolint does not support memory source_text
(ServerLinterRun::OnType, Run::OnType) => (true, false),
// it does not match, run nothing
(ServerLinterRun::OnType, Run::OnSave) => (false, false),
// it is disabled, or it does not match, run nothing
(_, Run::Off) | (ServerLinterRun::OnType, Run::OnSave) => (false, false),
// In onType mode, only TypeScript type checking runs on save
// If type_aware is disabled (tsgo_linter is None), skip everything to preserve diagnostics
(ServerLinterRun::OnSave, Run::OnType) => {
Expand Down Expand Up @@ -403,6 +403,11 @@ impl ServerLinter {
new_options: &LSPLintOptions,
root_path: &Path,
) -> Option<Vec<Pattern>> {
// from off to on, need to watch all patterns
if old_options.run == Run::Off && new_options.run != Run::Off {
return Some(self.get_watch_patterns(new_options, root_path));
}

if old_options.config_path == new_options.config_path
&& old_options.use_nested_configs() == new_options.use_nested_configs()
{
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_language_server/src/tester.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ impl Tester<'_> {
match run_type {
Run::OnSave => ServerLinterRun::OnSave,
Run::OnType => ServerLinterRun::OnType,
Run::Off => panic!("Linter is disabled"),
},
)
.await
Expand Down
113 changes: 103 additions & 10 deletions crates/oxc_language_server/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
formatter::{options::FormatOptions, server_formatter::ServerFormatter},
linter::{
error_with_position::DiagnosticReport,
options::LintOptions,
options::{LintOptions, Run},
server_linter::{ServerLinter, ServerLinterRun},
},
options::Options,
Expand Down Expand Up @@ -68,7 +68,12 @@ impl WorkspaceWorker {
pub async fn start_worker(&self, options: &Options) {
*self.options.lock().await = Some(options.clone());

*self.server_linter.write().await = Some(ServerLinter::new(&self.root_uri, &options.lint));
if options.lint.run != Run::Off {
debug!("linter enabled");
*self.server_linter.write().await =
Some(ServerLinter::new(&self.root_uri, &options.lint));
}

if options.format.experimental {
debug!("experimental formatter enabled");
*self.server_formatter.write().await =
Expand Down Expand Up @@ -320,13 +325,15 @@ impl WorkspaceWorker {
let format_options = options.as_ref().map(|o| o.format.clone()).unwrap_or_default();
let lint_options = options.as_ref().map(|o| o.lint.clone()).unwrap_or_default();

if format_options.experimental {
if format_options.experimental && lint_options.run != Run::Off {
tokio::join!(
self.refresh_server_formatter(&format_options),
self.refresh_server_linter(&lint_options)
);
} else {
} else if lint_options.run != Run::Off {
self.refresh_server_linter(&lint_options).await;
} else if format_options.experimental {
self.refresh_server_formatter(&format_options).await;
}

Some(self.revalidate_diagnostics(files).await)
Expand Down Expand Up @@ -421,7 +428,12 @@ impl WorkspaceWorker {
}
}

if ServerLinter::needs_restart(&current_option.lint, &changed_options.lint) {
let is_linter_enabled =
current_option.lint.run == Run::Off && changed_options.lint.run != Run::Off;

if is_linter_enabled
|| ServerLinter::needs_restart(&current_option.lint, &changed_options.lint)
{
// get the cached files before refreshing the linter
let linter_files = {
let linter_guard = self.server_linter.read().await;
Expand Down Expand Up @@ -450,10 +462,13 @@ impl WorkspaceWorker {
}

if let Some(patterns) = patterns {
unregistrations.push(Unregistration {
id: format!("watcher-linter-{}", self.root_uri.as_str()),
method: "workspace/didChangeWatchedFiles".to_string(),
});
// when linter was disabled before, no need to unregister
if !is_linter_enabled {
unregistrations.push(Unregistration {
id: format!("watcher-linter-{}", self.root_uri.as_str()),
method: "workspace/didChangeWatchedFiles".to_string(),
});
}

registrations.push(Registration {
id: format!("watcher-linter-{}", self.root_uri.as_str()),
Expand All @@ -472,6 +487,17 @@ impl WorkspaceWorker {
})),
});
}
} else if current_option.lint.run != Run::Off && changed_options.lint.run == Run::Off {
// the current files needs to be cleared
diagnostics = Some(self.get_clear_diagnostics().await);

// linter is turned off
*self.server_linter.write().await = None;

unregistrations.push(Unregistration {
id: format!("watcher-linter-{}", self.root_uri.as_str()),
method: "workspace/didChangeWatchedFiles".to_string(),
});
}

(diagnostics, registrations, unregistrations, formatting)
Expand Down Expand Up @@ -599,7 +625,9 @@ mod test_watchers {

mod init_watchers {
use crate::{
formatter::options::FormatOptions, linter::options::LintOptions, options::Options,
formatter::options::FormatOptions,
linter::options::{LintOptions, Run},
options::Options,
worker::test_watchers::Tester,
};

Expand All @@ -612,6 +640,20 @@ mod test_watchers {
tester.assert_eq_registration(&registrations[0], "linter", &["**/.oxlintrc.json"]);
}

#[test]
fn test_disabling_linter() {
let tester = Tester::new(
"fixtures/watcher/default",
&Options {
lint: LintOptions { run: Run::Off, ..Default::default() },
..Default::default()
},
);
let registrations = tester.init_watchers();

assert_eq!(registrations.len(), 0);
}

#[test]
fn test_custom_config_path() {
let tester = Tester::new(
Expand Down Expand Up @@ -682,6 +724,21 @@ mod test_watchers {
tester.assert_eq_registration(&watchers[1], "formatter", &[".oxfmtrc.json"]);
}

#[test]
fn test_formatter_with_disabled_linter() {
let tester = Tester::new(
"fixtures/watcher/default",
&Options {
lint: LintOptions { run: Run::Off, ..Default::default() },
format: FormatOptions { experimental: true, ..Default::default() },
},
);
let watchers = tester.init_watchers();

assert_eq!(watchers.len(), 1);
tester.assert_eq_registration(&watchers[0], "formatter", &[".oxfmtrc.json"]);
}

#[test]
fn test_formatter_custom_config_path() {
let tester = Tester::new(
Expand Down Expand Up @@ -896,5 +953,41 @@ mod test_watchers {
}
);
}

#[test]
fn test_linter_disabling() {
let tester = Tester::new("fixtures/watcher/default", &Options::default());
let (registration, unregistrations) = tester.did_change_configuration(&Options {
lint: LintOptions { run: Run::Off, ..Default::default() },
..Default::default()
});

assert_eq!(unregistrations.len(), 1);
assert_eq!(registration.len(), 0);
assert_eq!(
unregistrations[0],
Unregistration {
id: format!("watcher-linter-{}", tester.worker.get_root_uri().as_str()),
method: "workspace/didChangeWatchedFiles".to_string(),
}
);
}

#[test]
fn test_linter_enabling() {
let tester = Tester::new(
"fixtures/watcher/default",
&Options {
lint: LintOptions { run: Run::Off, ..Default::default() },
..Default::default()
},
);
let (registration, unregistrations) =
tester.did_change_configuration(&Options::default());

assert_eq!(unregistrations.len(), 0);
assert_eq!(registration.len(), 1);
tester.assert_eq_registration(&registration[0], "linter", &["**/.oxlintrc.json"]);
}
}
}
Loading