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
114 changes: 92 additions & 22 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use futures::future::join_all;
use globset::Glob;
use ignore::gitignore::Gitignore;
use log::{debug, error, info};
use rustc_hash::FxBuildHasher;
use oxc_linter::{ConfigStore, ConfigStoreBuilder, FixKind, LintOptions, Linter, Oxlintrc};
use rustc_hash::{FxBuildHasher, FxHashMap};
use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, OnceCell, RwLock, SetError};
use tower_lsp::{
Expand All @@ -15,14 +16,12 @@ use tower_lsp::{
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
ConfigurationItem, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams,
DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DidSaveTextDocumentParams, ExecuteCommandParams, InitializeParams, InitializeResult,
InitializedParams, NumberOrString, Position, Range, ServerInfo, TextEdit, Url,
WorkspaceEdit,
DidSaveTextDocumentParams, ExecuteCommandParams, FileChangeType, InitializeParams,
InitializeResult, InitializedParams, NumberOrString, Position, Range, ServerInfo, TextEdit,
Url, WorkspaceEdit,
},
};

use oxc_linter::{ConfigStoreBuilder, FixKind, LintOptions, Linter, Oxlintrc};

use crate::capabilities::{CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC, Capabilities};
use crate::linter::error_with_position::DiagnosticReport;
use crate::linter::server_linter::ServerLinter;
Expand All @@ -33,13 +32,16 @@ mod linter;

type ConcurrentHashMap<K, V> = papaya::HashMap<K, V, FxBuildHasher>;

const OXC_CONFIG_FILE: &str = ".oxlintrc.json";

struct Backend {
client: Client,
root_uri: OnceCell<Option<Url>>,
server_linter: RwLock<ServerLinter>,
diagnostics_report_map: ConcurrentHashMap<String, Vec<DiagnosticReport>>,
options: Mutex<Options>,
gitignore_glob: Mutex<Vec<Gitignore>>,
nested_configs: ConcurrentHashMap<PathBuf, ConfigStore>,
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, PartialOrd, Clone, Copy)]
#[serde(rename_all = "camelCase")]
Expand All @@ -54,11 +56,17 @@ 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: ".oxlintrc.json".into() }
Self {
enable: true,
run: Run::default(),
config_path: OXC_CONFIG_FILE.into(),
flags: FxHashMap::default(),
}
}
}

Expand All @@ -77,6 +85,10 @@ impl Options {
fn get_config_path(&self) -> Option<PathBuf> {
if self.config_path.is_empty() { None } else { Some(PathBuf::from(&self.config_path)) }
}

fn disable_nested_configs(&self) -> bool {
self.flags.contains_key("disable_nested_config")
}
}

#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
Expand Down Expand Up @@ -166,6 +178,10 @@ impl LanguageServer for Backend {
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
Expand All @@ -180,8 +196,48 @@ impl LanguageServer for Backend {
}
}

async fn did_change_watched_files(&self, _params: DidChangeWatchedFilesParams) {
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
debug!("watched file did change");
if !self.options.lock().await.disable_nested_configs() {
let nested_configs = self.nested_configs.pin();

params.changes.iter().for_each(|x| {
let Ok(file_path) = x.uri.to_file_path() else {
info!("Unable to convert {:?} to a file path", x.uri);
return;
};
let Some(file_name) = file_path.file_name() else {
info!("Unable to retrieve file name from {:?}", file_path);
return;
};

if file_name != OXC_CONFIG_FILE {
return;
}

let Some(dir_path) = file_path.parent() else {
info!("Unable to retrieve parent from {:?}", file_path);
return;
};

// spellchecker:off -- "typ" is accurate
if x.typ == FileChangeType::CREATED || x.typ == FileChangeType::CHANGED {
// spellchecker:on
let oxlintrc =
Oxlintrc::from_file(&file_path).expect("Failed to parse config file");
let config_store_builder = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc)
.expect("Failed to create config store builder");
let config_store =
config_store_builder.build().expect("Failed to build config store");
nested_configs.insert(dir_path.to_path_buf(), config_store);
// spellchecker:off -- "typ" is accurate
} else if x.typ == FileChangeType::DELETED {
// spellchecker:on
nested_configs.remove(&dir_path.to_path_buf());
}
});
}

self.init_linter_config().await;
self.revalidate_open_files().await;
}
Expand Down Expand Up @@ -492,21 +548,34 @@ impl Backend {
if config.exists() {
config_path = Some(config);
}
if let Some(config_path) = config_path {
let mut linter = self.server_linter.write().await;
let config = Oxlintrc::from_file(&config_path)
.expect("should have initialized linter with new options");
let config_store = ConfigStoreBuilder::from_oxlintrc(true, config.clone())
.expect("failed to build config")
.build()
.expect("failed to build config");
*linter = ServerLinter::new_with_linter(
Linter::new(LintOptions::default(), config_store).with_fix(FixKind::SafeFix),
);
return Some(config);
}

None
let config_path = config_path?;
let oxlintrc = Oxlintrc::from_file(&config_path)
.expect("should have initialized linter with new options");
let config_store = ConfigStoreBuilder::from_oxlintrc(true, oxlintrc.clone())
.expect("failed to build config")
.build()
.expect("failed to build config");

let linter = if self.options.lock().await.disable_nested_configs() {
Linter::new(LintOptions::default(), config_store).with_fix(FixKind::SafeFix)
} else {
let nested_configs = self.nested_configs.pin();
let nested_configs_copy: FxHashMap<PathBuf, ConfigStore> = nested_configs
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<FxHashMap<_, _>>();

Linter::new_with_nested_configs(
LintOptions::default(),
config_store,
nested_configs_copy,
)
};

*self.server_linter.write().await = ServerLinter::new_with_linter(linter);

Some(oxlintrc.clone())
}

async fn handle_file_update(&self, uri: Url, content: Option<String>, version: Option<i32>) {
Expand Down Expand Up @@ -568,6 +637,7 @@ async fn main() {
diagnostics_report_map,
options: Mutex::new(Options::default()),
gitignore_glob: Mutex::new(vec![]),
nested_configs: ConcurrentHashMap::default(),
})
.finish();

Expand Down
4 changes: 2 additions & 2 deletions crates/oxc_linter/src/config/config_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl Clone for ResolvedLinterState {
}
}

#[derive(Debug)]
#[derive(Debug, Clone)]
struct Config {
/// The basic linter state for this configuration.
base: ResolvedLinterState,
Expand All @@ -29,7 +29,7 @@ struct Config {
}

/// Resolves a lint configuration for a given file, by applying overrides based on the file's path.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ConfigStore {
base: Config,
}
Expand Down
Loading