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: 5 additions & 0 deletions .github/workflows/ci_vscode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
paths:
- "pnpm-lock.yaml"
- "crates/oxc_language_server/**"
- "apps/oxlint/src/lsp.rs"
- "editors/vscode/**"
- ".github/workflows/ci_vscode.yml"

Expand Down Expand Up @@ -45,6 +46,10 @@ jobs:
working-directory: editors/vscode
run: pnpm run server:build:debug

- name: Build oxlint (with napi)
working-directory: editors/vscode
run: pnpm run oxlint:build:debug

- name: Compile VSCode
working-directory: editors/vscode
run: pnpm run compile
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ oxc_traverse = { version = "0.97.0", path = "crates/oxc_traverse" } # AST traver

# publish = false
oxc_formatter = { path = "crates/oxc_formatter" } # Code formatting
oxc_language_server = { path = "crates/oxc_language_server" } # Language server
oxc_linter = { path = "crates/oxc_linter" } # Linting engine
oxc_macros = { path = "crates/oxc_macros" } # Proc macros
oxc_tasks_common = { path = "tasks/common" } # Task utilities
Expand Down
3 changes: 2 additions & 1 deletion apps/oxlint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ doctest = false
[dependencies]
oxc_allocator = { workspace = true, features = ["fixed_size"] }
oxc_diagnostics = { workspace = true }
oxc_language_server = { workspace = true }
oxc_linter = { workspace = true }
oxc_span = { workspace = true }

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

[target.'cfg(not(any(target_os = "linux", target_os = "freebsd", target_arch = "arm", target_family = "wasm")))'.dependencies]
Expand Down
4 changes: 4 additions & 0 deletions apps/oxlint/src/command/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub struct LintCommand {
#[bpaf(long("rules"), switch, hide_usage)]
pub list_rules: bool,

/// Start the language server
#[bpaf(long("lsp"), switch, hide_usage)]
pub lsp: bool,

#[bpaf(external)]
pub misc_options: MiscOptions,

Expand Down
3 changes: 2 additions & 1 deletion apps/oxlint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
mod command;
mod init;
mod lint;
mod lsp;
mod output_formatter;
mod result;
mod walk;
Expand All @@ -13,7 +14,7 @@ mod tester;

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

// Only include code to run linter when the `napi` feature is enabled.
Expand Down
4 changes: 4 additions & 0 deletions apps/oxlint/src/lsp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// Run the language server
pub async fn run_lsp() {
oxc_language_server::run_server(vec![Box::new(oxc_language_server::ServerLinterBuilder)]).await;
}
17 changes: 12 additions & 5 deletions apps/oxlint/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
use std::io::BufWriter;

use oxlint::cli::{CliRunResult, CliRunner, init_miette, init_tracing, lint_command};

fn main() -> CliRunResult {
init_tracing();
init_miette();
use oxlint::cli::{CliRunResult, CliRunner, init_miette, init_tracing, lint_command, run_lsp};

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

// If --lsp flag is set, run the language server
if command.lsp {
run_lsp().await;
return CliRunResult::LintSucceeded;
}

init_tracing();
init_miette();

command.handle_threads();

// stdio is blocked by LineWriter, use a BufWriter to reduce syscalls.
Expand Down
38 changes: 23 additions & 15 deletions apps/oxlint/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,34 +64,42 @@ pub type JsLintFileCb = ThreadsafeFunction<
#[allow(clippy::trailing_empty_array, clippy::unused_async)] // https://github.com/napi-rs/napi-rs/issues/2758
#[napi]
pub async fn lint(args: Vec<String>, load_plugin: JsLoadPluginCb, lint_file: JsLintFileCb) -> bool {
lint_impl(args, load_plugin, lint_file).report() == ExitCode::SUCCESS
lint_impl(args, load_plugin, lint_file).await.report() == ExitCode::SUCCESS
}

/// Run the linter.
fn lint_impl(
async fn lint_impl(
args: Vec<String>,
load_plugin: JsLoadPluginCb,
lint_file: JsLintFileCb,
) -> CliRunResult {
init_tracing();
init_miette();

// Convert String args to OsString for compatibility with bpaf
let args: Vec<std::ffi::OsString> = args.into_iter().map(std::ffi::OsString::from).collect();

let cmd = crate::cli::lint_command();
let command = match cmd.run_inner(&*args) {
Ok(cmd) => cmd,
Err(e) => {
e.print_message(100);
return if e.exit_code() == 0 {
CliRunResult::LintSucceeded
} else {
CliRunResult::InvalidOptionConfig
};
let command = {
let cmd = crate::cli::lint_command();
match cmd.run_inner(&*args) {
Ok(cmd) => cmd,
Err(e) => {
e.print_message(100);
return if e.exit_code() == 0 {
CliRunResult::LintSucceeded
} else {
CliRunResult::InvalidOptionConfig
};
}
}
};

// If --lsp flag is set, run the language server
if command.lsp {
crate::lsp::run_lsp().await;
return CliRunResult::LintSucceeded;
}

init_tracing();
init_miette();

command.handle_threads();

// JS plugins are only supported on 64-bit little-endian platforms at present
Expand Down
24 changes: 22 additions & 2 deletions editors/vscode/.vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ const ext = process.platform === 'win32' ? '.exe' : '';

export default defineConfig({
tests: [
// Single-folder workspace tests
{
files: 'out/**/*.spec.js',
workspaceFolder: './test_workspace',
launchArgs: [
// This disables all extensions except the one being testing
// This disables all extensions except the one being tested
'--disable-extensions',
],
env: {
Expand All @@ -31,11 +32,12 @@ export default defineConfig({
timeout: 10_000,
},
},
// Multi-root workspace tests
{
files: 'out/**/*.spec.js',
workspaceFolder: multiRootWorkspaceFile,
launchArgs: [
// This disables all extensions except the one being testing
// This disables all extensions except the one being tested
'--disable-extensions',
],
env: {
Expand All @@ -46,5 +48,23 @@ export default defineConfig({
timeout: 10_000,
},
},
// Oxlint --lsp tests
{
files: 'out/**/*.spec.js',
workspaceFolder: './test_workspace',
launchArgs: [
// This disables all extensions except the one being tested
'--disable-extensions',
],
env: {
SINGLE_FOLDER_WORKSPACE: 'true',
OXLINT_LSP_TEST: 'true',
SERVER_PATH_DEV: path.resolve(import.meta.dirname, `../../apps/oxlint/dist/cli.js`),
SKIP_FORMATTER_TEST: 'true',
},
mocha: {
timeout: 10_000,
},
},
],
});
27 changes: 19 additions & 8 deletions editors/vscode/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,6 @@ export async function activate(context: ExtensionContext) {
return process.env.SERVER_PATH_DEV ?? join(context.extensionPath, `./target/release/oxc_language_server${ext}`);
}

const command = await findBinary();

const nodePath = configService.vsCodeConfig.nodePath;
const serverEnv: Record<string, string> = {
...process.env,
Expand All @@ -154,12 +152,25 @@ export async function activate(context: ExtensionContext) {
if (nodePath) {
serverEnv.PATH = `${nodePath}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`;
}
const run: Executable = {
command: command!,
options: {
env: serverEnv,
},
};

const path = await findBinary();

const run: Executable =
process.env.OXLINT_LSP_TEST === 'true'
? {
command: 'node',
args: [path!, '--lsp'],
options: {
env: serverEnv,
},
}
: {
command: path!,
options: {
env: serverEnv,
},
};

const serverOptions: ServerOptions = {
run,
debug: run,
Expand Down
2 changes: 2 additions & 0 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@
"watch": "pnpm run compile --watch",
"package": "vsce package --no-dependencies -o oxc_language_server.vsix",
"install-extension": "code --install-extension oxc_language_server.vsix --force",
"oxlint:build:debug": "pnpm -C ../../apps/oxlint build-napi-test && pnpm -C ../../apps/oxlint build-js",
"oxlint:build:release": "pnpm -C ../../apps/oxlint build-napi && pnpm -C ../../apps/oxlint build-js",
"server:build:debug": "cross-env CARGO_TARGET_DIR=./target cargo build -p oxc_language_server",
"server:build:release": "cross-env CARGO_TARGET_DIR=./target cargo build -p oxc_language_server --release",
"test": "cross-env TEST=true pnpm run compile && vscode-test",
Expand Down
69 changes: 69 additions & 0 deletions editors/vscode/tests/e2e_server_formatter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { strictEqual } from 'assert';
import {
commands,
Uri,
window,
workspace,
} from 'vscode';
import {
activateExtension,
fixturesWorkspaceUri,
loadFixture,
sleep,
} from './test-helpers';

suiteSetup(async () => {
await activateExtension();
});

teardown(async () => {
await workspace.getConfiguration('oxc').update('fmt.experimental', undefined);
await workspace.getConfiguration('oxc').update('fmt.configPath', undefined);
await workspace.getConfiguration('editor').update('defaultFormatter', undefined);
await workspace.saveAll();
});

suite('E2E Server Formatter', () => {
// Skip tests if formatter tests are disabled
if (process.env.SKIP_FORMATTER_TEST === 'true') {
return;
}

test('formats code with `oxc.fmt.experimental`', async () => {
await workspace.getConfiguration('oxc').update('fmt.experimental', true);
await workspace.getConfiguration('editor').update('defaultFormatter', 'oxc.oxc-vscode');
await loadFixture('formatting');

await sleep(500);

const fileUri = Uri.joinPath(fixturesWorkspaceUri(), 'fixtures', 'formatting.ts');

const document = await workspace.openTextDocument(fileUri);
await window.showTextDocument(document);
await commands.executeCommand('editor.action.formatDocument');
await workspace.saveAll();
const content = await workspace.fs.readFile(fileUri);

strictEqual(content.toString(), "class X {\n foo() {\n return 42;\n }\n}\n");
});

test('formats code with `oxc.fmt.configPath`', async () => {
await loadFixture('formatting_with_config');

await workspace.getConfiguration('oxc').update('fmt.experimental', true);
await workspace.getConfiguration('oxc').update('fmt.configPath', './fixtures/formatter.json');
await workspace.getConfiguration('editor').update('defaultFormatter', 'oxc.oxc-vscode');

await sleep(500); // wait for the server to pick up the new config
const fileUri = Uri.joinPath(fixturesWorkspaceUri(), 'fixtures', 'formatting.ts');

const document = await workspace.openTextDocument(fileUri);
await window.showTextDocument(document);
await commands.executeCommand('editor.action.formatDocument');
await workspace.saveAll();
const content = await workspace.fs.readFile(fileUri);

strictEqual(content.toString(), "class X {\n foo() {\n return 42\n }\n}\n");
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ teardown(async () => {
});


suite('E2E Diagnostics', () => {
suite('E2E Server Linter', () => {
test('simple debugger statement', async () => {
await loadFixture('debugger');
const diagnostics = await getDiagnostics('debugger.js');
Expand Down Expand Up @@ -277,41 +277,4 @@ suite('E2E Diagnostics', () => {
const secondDiagnostics = await getDiagnostics('index.ts');
strictEqual(secondDiagnostics.length, 1);
});

test('formats code with `oxc.fmt.experimental`', async () => {
await workspace.getConfiguration('oxc').update('fmt.experimental', true);
await workspace.getConfiguration('editor').update('defaultFormatter', 'oxc.oxc-vscode');
await loadFixture('formatting');

await sleep(500);

const fileUri = Uri.joinPath(fixturesWorkspaceUri(), 'fixtures', 'formatting.ts');

const document = await workspace.openTextDocument(fileUri);
await window.showTextDocument(document);
await commands.executeCommand('editor.action.formatDocument');
await workspace.saveAll();
const content = await workspace.fs.readFile(fileUri);

strictEqual(content.toString(), "class X {\n foo() {\n return 42;\n }\n}\n");
});

test('formats code with `oxc.fmt.configPath`', async () => {
await loadFixture('formatting_with_config');

await workspace.getConfiguration('oxc').update('fmt.experimental', true);
await workspace.getConfiguration('oxc').update('fmt.configPath', './fixtures/formatter.json');
await workspace.getConfiguration('editor').update('defaultFormatter', 'oxc.oxc-vscode');

await sleep(500); // wait for the server to pick up the new config
const fileUri = Uri.joinPath(fixturesWorkspaceUri(), 'fixtures', 'formatting.ts');

const document = await workspace.openTextDocument(fileUri);
await window.showTextDocument(document);
await commands.executeCommand('editor.action.formatDocument');
await workspace.saveAll();
const content = await workspace.fs.readFile(fileUri);

strictEqual(content.toString(), "class X {\n foo() {\n return 42\n }\n}\n");
});
});
Loading
Loading