Skip to content

Commit e01c551

Browse files
committed
feat(oxlint): add --lsp flag to run the language server (#15611)
closes #15038 and #12251 > This PR adds a --lsp flag to the oxlint command-line tool that starts the language server directly, allowing for a unified binary that can function as both a linter and a language server. This addresses the need for a simpler deployment model where a single oxlint binary can be used in different modes.
1 parent e2a0997 commit e01c551

File tree

16 files changed

+170
-70
lines changed

16 files changed

+170
-70
lines changed

.github/workflows/ci_vscode.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ on:
1717
paths:
1818
- "pnpm-lock.yaml"
1919
- "crates/oxc_language_server/**"
20+
- "apps/oxlint/src/lsp.rs"
2021
- "editors/vscode/**"
2122
- ".github/workflows/ci_vscode.yml"
2223

@@ -45,6 +46,10 @@ jobs:
4546
working-directory: editors/vscode
4647
run: pnpm run server:build:debug
4748

49+
- name: Build oxlint (with napi)
50+
working-directory: editors/vscode
51+
run: pnpm run oxlint:build:debug
52+
4853
- name: Compile VSCode
4954
working-directory: editors/vscode
5055
run: pnpm run compile

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ oxc_traverse = { version = "0.97.0", path = "crates/oxc_traverse" } # AST traver
133133

134134
# publish = false
135135
oxc_formatter = { path = "crates/oxc_formatter" } # Code formatting
136+
oxc_language_server = { path = "crates/oxc_language_server" } # Language server
136137
oxc_linter = { path = "crates/oxc_linter" } # Linting engine
137138
oxc_macros = { path = "crates/oxc_macros" } # Proc macros
138139
oxc_tasks_common = { path = "tasks/common" } # Task utilities

apps/oxlint/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ doctest = false
2929
[dependencies]
3030
oxc_allocator = { workspace = true, features = ["fixed_size"] }
3131
oxc_diagnostics = { workspace = true }
32+
oxc_language_server = { workspace = true }
3233
oxc_linter = { workspace = true }
3334
oxc_span = { workspace = true }
3435

@@ -44,7 +45,7 @@ serde = { workspace = true, features = ["derive"] }
4445
serde_json = { workspace = true }
4546
simdutf8 = { workspace = true }
4647
tempfile = { workspace = true }
47-
tokio = { workspace = true, features = ["rt-multi-thread"] }
48+
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros"] }
4849
tracing-subscriber = { workspace = true, features = [] } # Omit the `regex` feature
4950

5051
[target.'cfg(not(any(target_os = "linux", target_os = "freebsd", target_arch = "arm", target_family = "wasm")))'.dependencies]

apps/oxlint/src/command/lint.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ pub struct LintCommand {
3939
#[bpaf(long("rules"), switch, hide_usage)]
4040
pub list_rules: bool,
4141

42+
/// Start the language server
43+
#[bpaf(long("lsp"), switch, hide_usage)]
44+
pub lsp: bool,
45+
4246
#[bpaf(external)]
4347
pub misc_options: MiscOptions,
4448

apps/oxlint/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
mod command;
55
mod init;
66
mod lint;
7+
mod lsp;
78
mod output_formatter;
89
mod result;
910
mod walk;
@@ -13,7 +14,7 @@ mod tester;
1314

1415
/// Re-exported CLI-related items for use in `tasks/website`.
1516
pub mod cli {
16-
pub use super::{command::*, init::*, lint::CliRunner, result::CliRunResult};
17+
pub use super::{command::*, init::*, lint::CliRunner, lsp::run_lsp, result::CliRunResult};
1718
}
1819

1920
// Only include code to run linter when the `napi` feature is enabled.

apps/oxlint/src/lsp.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/// Run the language server
2+
pub async fn run_lsp() {
3+
oxc_language_server::run_server(vec![Box::new(oxc_language_server::ServerLinterBuilder)]).await;
4+
}

apps/oxlint/src/main.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
use std::io::BufWriter;
22

3-
use oxlint::cli::{CliRunResult, CliRunner, init_miette, init_tracing, lint_command};
4-
5-
fn main() -> CliRunResult {
6-
init_tracing();
7-
init_miette();
3+
use oxlint::cli::{CliRunResult, CliRunner, init_miette, init_tracing, lint_command, run_lsp};
84

5+
#[tokio::main]
6+
async fn main() -> CliRunResult {
97
// Parse command line arguments from std::env::args()
108
let command = lint_command().run();
119

10+
// If --lsp flag is set, run the language server
11+
if command.lsp {
12+
run_lsp().await;
13+
return CliRunResult::LintSucceeded;
14+
}
15+
16+
init_tracing();
17+
init_miette();
18+
1219
command.handle_threads();
1320

1421
// stdio is blocked by LineWriter, use a BufWriter to reduce syscalls.

apps/oxlint/src/run.rs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,34 +64,42 @@ pub type JsLintFileCb = ThreadsafeFunction<
6464
#[allow(clippy::trailing_empty_array, clippy::unused_async)] // https://github.com/napi-rs/napi-rs/issues/2758
6565
#[napi]
6666
pub async fn lint(args: Vec<String>, load_plugin: JsLoadPluginCb, lint_file: JsLintFileCb) -> bool {
67-
lint_impl(args, load_plugin, lint_file).report() == ExitCode::SUCCESS
67+
lint_impl(args, load_plugin, lint_file).await.report() == ExitCode::SUCCESS
6868
}
6969

7070
/// Run the linter.
71-
fn lint_impl(
71+
async fn lint_impl(
7272
args: Vec<String>,
7373
load_plugin: JsLoadPluginCb,
7474
lint_file: JsLintFileCb,
7575
) -> CliRunResult {
76-
init_tracing();
77-
init_miette();
78-
7976
// Convert String args to OsString for compatibility with bpaf
8077
let args: Vec<std::ffi::OsString> = args.into_iter().map(std::ffi::OsString::from).collect();
8178

82-
let cmd = crate::cli::lint_command();
83-
let command = match cmd.run_inner(&*args) {
84-
Ok(cmd) => cmd,
85-
Err(e) => {
86-
e.print_message(100);
87-
return if e.exit_code() == 0 {
88-
CliRunResult::LintSucceeded
89-
} else {
90-
CliRunResult::InvalidOptionConfig
91-
};
79+
let command = {
80+
let cmd = crate::cli::lint_command();
81+
match cmd.run_inner(&*args) {
82+
Ok(cmd) => cmd,
83+
Err(e) => {
84+
e.print_message(100);
85+
return if e.exit_code() == 0 {
86+
CliRunResult::LintSucceeded
87+
} else {
88+
CliRunResult::InvalidOptionConfig
89+
};
90+
}
9291
}
9392
};
9493

94+
// If --lsp flag is set, run the language server
95+
if command.lsp {
96+
crate::lsp::run_lsp().await;
97+
return CliRunResult::LintSucceeded;
98+
}
99+
100+
init_tracing();
101+
init_miette();
102+
95103
command.handle_threads();
96104

97105
// JS plugins are only supported on 64-bit little-endian platforms at present

editors/vscode/.vscode-test.mjs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ const ext = process.platform === 'win32' ? '.exe' : '';
1616

1717
export default defineConfig({
1818
tests: [
19+
// Single-folder workspace tests
1920
{
2021
files: 'out/**/*.spec.js',
2122
workspaceFolder: './test_workspace',
2223
launchArgs: [
23-
// This disables all extensions except the one being testing
24+
// This disables all extensions except the one being tested
2425
'--disable-extensions',
2526
],
2627
env: {
@@ -31,11 +32,12 @@ export default defineConfig({
3132
timeout: 10_000,
3233
},
3334
},
35+
// Multi-root workspace tests
3436
{
3537
files: 'out/**/*.spec.js',
3638
workspaceFolder: multiRootWorkspaceFile,
3739
launchArgs: [
38-
// This disables all extensions except the one being testing
40+
// This disables all extensions except the one being tested
3941
'--disable-extensions',
4042
],
4143
env: {
@@ -46,5 +48,23 @@ export default defineConfig({
4648
timeout: 10_000,
4749
},
4850
},
51+
// Oxlint --lsp tests
52+
{
53+
files: 'out/**/*.spec.js',
54+
workspaceFolder: './test_workspace',
55+
launchArgs: [
56+
// This disables all extensions except the one being tested
57+
'--disable-extensions',
58+
],
59+
env: {
60+
SINGLE_FOLDER_WORKSPACE: 'true',
61+
OXLINT_LSP_TEST: 'true',
62+
SERVER_PATH_DEV: path.resolve(import.meta.dirname, `../../apps/oxlint/dist/cli.js`),
63+
SKIP_FORMATTER_TEST: 'true',
64+
},
65+
mocha: {
66+
timeout: 10_000,
67+
},
68+
},
4969
],
5070
});

0 commit comments

Comments
 (0)