diff --git a/Cargo.lock b/Cargo.lock index 46ecb435c5fa..30e01dab321f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -931,6 +931,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "slotmap", "tests_macros", "tracing", ] @@ -2902,6 +2903,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "smallvec" version = "1.10.0" diff --git a/Cargo.toml b/Cargo.toml index 43e8f003b226..69d9021c36d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,7 @@ schemars = { version = "0.8.12" } serde = { version = "1.0.198", features = ["derive"] } serde_json = "1.0.116" similar = "2.5.0" +slotmap = "1.0.7" smallvec = { version = "1.10.0", features = ["union", "const_new"] } syn = "1.0.109" tokio = { version = "1.36.0" } diff --git a/crates/biome_cli/src/commands/check.rs b/crates/biome_cli/src/commands/check.rs index 4aaab8826a15..25899e9e6076 100644 --- a/crates/biome_cli/src/commands/check.rs +++ b/crates/biome_cli/src/commands/check.rs @@ -11,6 +11,7 @@ use biome_configuration::{ }; use biome_deserialize::Merge; use biome_service::configuration::PartialConfigurationExt; +use biome_service::workspace::RegisterProjectFolderParams; use biome_service::{ configuration::{load_configuration, LoadedConfiguration}, workspace::{FixFileMode, UpdateSettingsParams}, @@ -129,11 +130,19 @@ pub(crate) fn check( paths = _paths; } + session + .app + .workspace + .register_project_folder(RegisterProjectFolderParams { + path: session.app.fs.working_directory(), + set_as_current_workspace: true, + })?; + session .app .workspace .update_settings(UpdateSettingsParams { - working_directory: session.app.fs.working_directory(), + workspace_directory: session.app.fs.working_directory(), configuration: fs_configuration, vcs_base_path, gitignore_matches, diff --git a/crates/biome_cli/src/commands/ci.rs b/crates/biome_cli/src/commands/ci.rs index cab1b0764ee9..fd7540748447 100644 --- a/crates/biome_cli/src/commands/ci.rs +++ b/crates/biome_cli/src/commands/ci.rs @@ -8,7 +8,7 @@ use biome_deserialize::Merge; use biome_service::configuration::{ load_configuration, LoadedConfiguration, PartialConfigurationExt, }; -use biome_service::workspace::UpdateSettingsParams; +use biome_service::workspace::{RegisterProjectFolderParams, UpdateSettingsParams}; use std::ffi::OsString; pub(crate) struct CiCommandPayload { @@ -106,12 +106,20 @@ pub(crate) fn ci(session: CliSession, payload: CiCommandPayload) -> Result<(), C paths = get_changed_files(&session.app.fs, &fs_configuration, since)?; } + session + .app + .workspace + .register_project_folder(RegisterProjectFolderParams { + path: session.app.fs.working_directory(), + set_as_current_workspace: true, + })?; + session .app .workspace .update_settings(UpdateSettingsParams { configuration: fs_configuration, - working_directory: session.app.fs.working_directory(), + workspace_directory: session.app.fs.working_directory(), vcs_base_path, gitignore_matches, })?; diff --git a/crates/biome_cli/src/commands/format.rs b/crates/biome_cli/src/commands/format.rs index 57fc758856cf..7efae9ddaf6d 100644 --- a/crates/biome_cli/src/commands/format.rs +++ b/crates/biome_cli/src/commands/format.rs @@ -17,7 +17,7 @@ use biome_diagnostics::PrintDiagnostic; use biome_service::configuration::{ load_configuration, LoadedConfiguration, PartialConfigurationExt, }; -use biome_service::workspace::UpdateSettingsParams; +use biome_service::workspace::{RegisterProjectFolderParams, UpdateSettingsParams}; use std::ffi::OsString; pub(crate) struct FormatCommandPayload { @@ -165,11 +165,19 @@ pub(crate) fn format( paths = _paths; } + session + .app + .workspace + .register_project_folder(RegisterProjectFolderParams { + path: session.app.fs.working_directory(), + set_as_current_workspace: true, + })?; + session .app .workspace .update_settings(UpdateSettingsParams { - working_directory: session.app.fs.working_directory(), + workspace_directory: session.app.fs.working_directory(), configuration, vcs_base_path, gitignore_matches, diff --git a/crates/biome_cli/src/commands/lint.rs b/crates/biome_cli/src/commands/lint.rs index 79eb84145968..e010f2962ced 100644 --- a/crates/biome_cli/src/commands/lint.rs +++ b/crates/biome_cli/src/commands/lint.rs @@ -13,7 +13,7 @@ use biome_deserialize::Merge; use biome_service::configuration::{ load_configuration, LoadedConfiguration, PartialConfigurationExt, }; -use biome_service::workspace::{FixFileMode, UpdateSettingsParams}; +use biome_service::workspace::{FixFileMode, RegisterProjectFolderParams, UpdateSettingsParams}; use std::ffi::OsString; pub(crate) struct LintCommandPayload { @@ -106,11 +106,19 @@ pub(crate) fn lint(session: CliSession, payload: LintCommandPayload) -> Result<( let stdin = get_stdin(stdin_file_path, &mut *session.app.console, "lint")?; + session + .app + .workspace + .register_project_folder(RegisterProjectFolderParams { + path: session.app.fs.working_directory(), + set_as_current_workspace: true, + })?; + session .app .workspace .update_settings(UpdateSettingsParams { - working_directory: session.app.fs.working_directory(), + workspace_directory: session.app.fs.working_directory(), configuration: fs_configuration, vcs_base_path, gitignore_matches, diff --git a/crates/biome_cli/src/commands/migrate.rs b/crates/biome_cli/src/commands/migrate.rs index 11cbb2dafa61..3a58f3e17e0c 100644 --- a/crates/biome_cli/src/commands/migrate.rs +++ b/crates/biome_cli/src/commands/migrate.rs @@ -4,6 +4,7 @@ use crate::execute::{execute_mode, Execution, TraversalMode}; use crate::{setup_cli_subscriber, CliDiagnostic, CliSession}; use biome_console::{markup, ConsoleExt}; use biome_service::configuration::{load_configuration, LoadedConfiguration}; +use biome_service::workspace::RegisterProjectFolderParams; use super::MigrateSubCommand; @@ -23,6 +24,14 @@ pub(crate) fn migrate( } = load_configuration(&session.app.fs, base_path)?; setup_cli_subscriber(cli_options.log_level, cli_options.log_kind); + session + .app + .workspace + .register_project_folder(RegisterProjectFolderParams { + path: session.app.fs.working_directory(), + set_as_current_workspace: true, + })?; + if let (Some(path), Some(directory_path)) = (file_path, directory_path) { execute_mode( Execution::new(TraversalMode::Migrate { diff --git a/crates/biome_cli/src/commands/search.rs b/crates/biome_cli/src/commands/search.rs index b829e003cfaa..c599962cd2d1 100644 --- a/crates/biome_cli/src/commands/search.rs +++ b/crates/biome_cli/src/commands/search.rs @@ -8,7 +8,9 @@ use biome_deserialize::Merge; use biome_service::configuration::{ load_configuration, LoadedConfiguration, PartialConfigurationExt, }; -use biome_service::workspace::{ParsePatternParams, UpdateSettingsParams}; +use biome_service::workspace::{ + ParsePatternParams, RegisterProjectFolderParams, UpdateSettingsParams, +}; use std::ffi::OsString; pub(crate) struct SearchCommandPayload { @@ -59,8 +61,15 @@ pub(crate) fn search( configuration.retrieve_gitignore_matches(&session.app.fs, vcs_base_path.as_deref())?; let workspace = &session.app.workspace; + session + .app + .workspace + .register_project_folder(RegisterProjectFolderParams { + path: session.app.fs.working_directory(), + set_as_current_workspace: true, + })?; workspace.update_settings(UpdateSettingsParams { - working_directory: session.app.fs.working_directory(), + workspace_directory: session.app.fs.working_directory(), configuration, vcs_base_path, gitignore_matches, diff --git a/crates/biome_css_formatter/tests/language.rs b/crates/biome_css_formatter/tests/language.rs index 856421294a5f..4a03098df42f 100644 --- a/crates/biome_css_formatter/tests/language.rs +++ b/crates/biome_css_formatter/tests/language.rs @@ -6,7 +6,7 @@ use biome_formatter::{FormatResult, Formatted, Printed}; use biome_formatter_test::TestFormatLanguage; use biome_parser::AnyParse; use biome_rowan::{SyntaxNode, TextRange}; -use biome_service::settings::{ServiceLanguage, WorkspaceSettings}; +use biome_service::settings::{ServiceLanguage, Settings}; #[derive(Default)] pub struct CssTestFormatLanguage { @@ -26,7 +26,7 @@ impl TestFormatLanguage for CssTestFormatLanguage { fn to_language_settings<'a>( &self, - settings: &'a WorkspaceSettings, + settings: &'a Settings, ) -> &'a ::FormatterSettings { &settings.languages.css.formatter } diff --git a/crates/biome_formatter_test/src/lib.rs b/crates/biome_formatter_test/src/lib.rs index b79f54c9016d..ddd4e6e5cef3 100644 --- a/crates/biome_formatter_test/src/lib.rs +++ b/crates/biome_formatter_test/src/lib.rs @@ -4,7 +4,7 @@ use biome_parser::AnyParse; use biome_rowan::{SyntaxNode, TextRange}; use biome_service::file_handlers::DocumentFileSource; use biome_service::settings::ServiceLanguage; -use biome_service::settings::WorkspaceSettings; +use biome_service::settings::Settings; pub mod check_reformat; pub mod diff_report; @@ -25,7 +25,7 @@ pub trait TestFormatLanguage { fn to_language_settings<'a>( &self, - settings: &'a WorkspaceSettings, + settings: &'a Settings, ) -> &'a ::FormatterSettings; fn format_node( @@ -45,7 +45,7 @@ pub trait TestFormatLanguage { fn to_options( &self, - settings: &WorkspaceSettings, + settings: &Settings, file_source: &DocumentFileSource, ) -> ::FormatOptions { let language_settings = self.to_language_settings(settings); diff --git a/crates/biome_formatter_test/src/spec.rs b/crates/biome_formatter_test/src/spec.rs index 799287c55749..edf0c32c6676 100644 --- a/crates/biome_formatter_test/src/spec.rs +++ b/crates/biome_formatter_test/src/spec.rs @@ -10,7 +10,7 @@ use biome_formatter::{FormatOptions, Printed}; use biome_fs::BiomePath; use biome_parser::AnyParse; use biome_rowan::{TextRange, TextSize}; -use biome_service::settings::{ServiceLanguage, WorkspaceSettings}; +use biome_service::settings::{ServiceLanguage, Settings}; use biome_service::workspace::{DocumentFileSource, FeaturesBuilder, SupportsFeatureParams}; use biome_service::App; use std::ops::Range; @@ -222,7 +222,7 @@ where if options_path.exists() { let mut options_path = BiomePath::new(&options_path); - let mut settings = WorkspaceSettings::default(); + let mut settings = Settings::default(); // SAFETY: we checked its existence already, we assume we have rights to read it let (test_options, diagnostics) = deserialize_from_str::( options_path.get_buffer_from_file().as_str(), diff --git a/crates/biome_fs/src/path.rs b/crates/biome_fs/src/path.rs index 3b1c812de411..bf34107bfa45 100644 --- a/crates/biome_fs/src/path.rs +++ b/crates/biome_fs/src/path.rs @@ -6,7 +6,7 @@ use std::fs::{self, read_to_string}; use std::{fs::File, io, io::Write, ops::Deref, path::PathBuf}; -#[derive(Debug, Clone, Eq, Hash, PartialEq)] +#[derive(Debug, Clone, Eq, Hash, PartialEq, Default)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema) diff --git a/crates/biome_js_formatter/tests/language.rs b/crates/biome_js_formatter/tests/language.rs index 6add0c0d0548..4ad692e77939 100644 --- a/crates/biome_js_formatter/tests/language.rs +++ b/crates/biome_js_formatter/tests/language.rs @@ -6,7 +6,7 @@ use biome_js_parser::{parse, JsParserOptions}; use biome_js_syntax::{JsFileSource, JsLanguage}; use biome_parser::AnyParse; use biome_rowan::SyntaxNode; -use biome_service::settings::{ServiceLanguage, WorkspaceSettings}; +use biome_service::settings::{ServiceLanguage, Settings}; use biome_text_size::TextRange; pub struct JsTestFormatLanguage { @@ -36,7 +36,7 @@ impl TestFormatLanguage for JsTestFormatLanguage { fn to_language_settings<'a>( &self, - settings: &'a WorkspaceSettings, + settings: &'a Settings, ) -> &'a ::FormatterSettings { &settings.languages.javascript.formatter } diff --git a/crates/biome_json_formatter/tests/language.rs b/crates/biome_json_formatter/tests/language.rs index a6a7373984d9..b8878e3ab5b4 100644 --- a/crates/biome_json_formatter/tests/language.rs +++ b/crates/biome_json_formatter/tests/language.rs @@ -6,7 +6,7 @@ use biome_json_parser::{parse_json, JsonParserOptions}; use biome_json_syntax::{JsonFileSource, JsonLanguage}; use biome_parser::AnyParse; use biome_rowan::{SyntaxNode, TextRange}; -use biome_service::settings::{ServiceLanguage, WorkspaceSettings}; +use biome_service::settings::{ServiceLanguage, Settings}; use serde::{Deserialize, Serialize}; #[derive(Default)] @@ -27,7 +27,7 @@ impl TestFormatLanguage for JsonTestFormatLanguage { fn to_language_settings<'a>( &self, - settings: &'a WorkspaceSettings, + settings: &'a Settings, ) -> &'a ::FormatterSettings { &settings.languages.json.formatter } diff --git a/crates/biome_lsp/src/server.rs b/crates/biome_lsp/src/server.rs index ec99f3837a51..c8197e277aa2 100644 --- a/crates/biome_lsp/src/server.rs +++ b/crates/biome_lsp/src/server.rs @@ -248,6 +248,14 @@ impl LanguageServer for LSPServer { self.is_initialized.store(true, Ordering::Relaxed); let server_capabilities = server_capabilities(¶ms.capabilities); + if params.root_path.is_some() { + warn!("The Biome Server was initialized with the deprecated `root_path` parameter: this is not supported, use `root_uri` instead"); + } + + if let Some(_folders) = ¶ms.workspace_folders { + warn!("The Biome Server was initialized with the `workspace_folders` parameter: this is unsupported at the moment, use `root_uri` instead"); + } + self.session.initialize( params.capabilities, params.client_info.map(|client_info| ClientInformation { @@ -255,16 +263,9 @@ impl LanguageServer for LSPServer { version: client_info.version, }), params.root_uri, + params.workspace_folders, ); - if params.root_path.is_some() { - warn!("The Biome Server was initialized with the deprecated `root_path` parameter: this is not supported, use `root_uri` instead"); - } - - if params.workspace_folders.is_some() { - warn!("The Biome Server was initialized with the `workspace_folders` parameter: this is unsupported at the moment, use `root_uri` instead"); - } - // let init = InitializeResult { capabilities: server_capabilities, @@ -574,6 +575,7 @@ impl ServerFactory { workspace_method!(builder, file_features); workspace_method!(builder, is_path_ignored); workspace_method!(builder, update_settings); + workspace_method!(builder, register_project_folder); workspace_method!(builder, open_file); workspace_method!(builder, open_project); workspace_method!(builder, update_current_project); diff --git a/crates/biome_lsp/src/session.rs b/crates/biome_lsp/src/session.rs index 129944c17b93..872f24c16f4e 100644 --- a/crates/biome_lsp/src/session.rs +++ b/crates/biome_lsp/src/session.rs @@ -15,7 +15,7 @@ use biome_service::configuration::{ use biome_service::file_handlers::{AstroFileHandler, SvelteFileHandler, VueFileHandler}; use biome_service::workspace::{ FeaturesBuilder, GetFileContentParams, OpenProjectParams, PullDiagnosticsParams, - SupportsFeatureParams, UpdateProjectParams, + RegisterProjectFolderParams, SupportsFeatureParams, UpdateProjectParams, }; use biome_service::workspace::{RageEntry, RageParams, RageResult, UpdateSettingsParams}; use biome_service::Workspace; @@ -32,9 +32,9 @@ use std::sync::RwLock; use tokio::sync::Notify; use tokio::sync::OnceCell; use tower_lsp::lsp_types; -use tower_lsp::lsp_types::Unregistration; use tower_lsp::lsp_types::Url; use tower_lsp::lsp_types::{MessageType, Registration}; +use tower_lsp::lsp_types::{Unregistration, WorkspaceFolder}; use tracing::{debug, error, info}; pub(crate) struct ClientInformation { @@ -83,6 +83,8 @@ struct InitializeParams { client_capabilities: lsp_types::ClientCapabilities, client_information: Option, root_uri: Option, + #[allow(unused)] + workspace_folders: Option>, } #[repr(u8)] @@ -171,11 +173,13 @@ impl Session { client_capabilities: lsp_types::ClientCapabilities, client_information: Option, root_uri: Option, + workspace_folders: Option>, ) { let result = self.initialize_params.set(InitializeParams { client_capabilities, client_information, root_uri, + workspace_folders, }); if let Err(err) = result { @@ -236,7 +240,7 @@ impl Session { /// Get a [`Document`] matching the provided [`lsp_types::Url`] /// - /// If document does not exist, result is [SessionError::DocumentNotFound] + /// If document does not exist, result is [WorkspaceError::NotFound] pub(crate) fn document(&self, url: &lsp_types::Url) -> Result { self.documents .read() @@ -447,8 +451,15 @@ impl Session { match result { Ok((vcs_base_path, gitignore_matches)) => { + // We don't need the key for now, but will soon + let _ = self.workspace.register_project_folder( + RegisterProjectFolderParams { + path: fs.working_directory(), + set_as_current_workspace: true, + }, + ); let result = self.workspace.update_settings(UpdateSettingsParams { - working_directory: fs.working_directory(), + workspace_directory: fs.working_directory(), configuration, vcs_base_path, gitignore_matches, diff --git a/crates/biome_service/Cargo.toml b/crates/biome_service/Cargo.toml index e447a183d60e..5d63bf97af46 100644 --- a/crates/biome_service/Cargo.toml +++ b/crates/biome_service/Cargo.toml @@ -53,8 +53,8 @@ rustc-hash = { workspace = true } schemars = { workspace = true, features = ["indexmap1"], optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["raw_value"] } +slotmap = { workspace = true, features = ["serde"] } tracing = { workspace = true, features = ["attributes", "log"] } - [features] format_css = ["biome_css_formatter/format_css"] schema = [ diff --git a/crates/biome_service/src/configuration.rs b/crates/biome_service/src/configuration.rs index 7efa5a13e37b..0ab4c8be1740 100644 --- a/crates/biome_service/src/configuration.rs +++ b/crates/biome_service/src/configuration.rs @@ -1,4 +1,4 @@ -use crate::settings::WorkspaceSettings; +use crate::settings::Settings; use crate::{DynRef, WorkspaceError, VERSION}; use biome_analyze::AnalyzerRules; use biome_configuration::diagnostics::CantLoadExtendFile; @@ -316,8 +316,8 @@ pub fn create_config( Ok(()) } -/// Returns the rules applied to a specific [Path], given the [WorkspaceSettings] -pub fn to_analyzer_rules(settings: &WorkspaceSettings, path: &Path) -> AnalyzerRules { +/// Returns the rules applied to a specific [Path], given the [Settings] +pub fn to_analyzer_rules(settings: &Settings, path: &Path) -> AnalyzerRules { let linter_settings = &settings.linter; let overrides = &settings.override_settings; let mut analyzer_rules = AnalyzerRules::default(); diff --git a/crates/biome_service/src/settings.rs b/crates/biome_service/src/settings.rs index 7fe6ca738915..bdf077715a65 100644 --- a/crates/biome_service/src/settings.rs +++ b/crates/biome_service/src/settings.rs @@ -1,4 +1,4 @@ -use crate::workspace::DocumentFileSource; +use crate::workspace::{DocumentFileSource, ProjectKey, WorkspaceData}; use crate::{Matcher, WorkspaceError}; use biome_analyze::AnalyzerRules; use biome_configuration::diagnostics::InvalidIgnorePattern; @@ -30,62 +30,92 @@ use indexmap::IndexSet; use rustc_hash::FxHashMap; use std::borrow::Cow; use std::path::{Path, PathBuf}; +use std::sync::RwLockWriteGuard; use std::{ num::NonZeroU64, sync::{RwLock, RwLockReadGuard}, }; -/// Global settings for the entire workspace +#[derive(Debug, Default)] +pub struct PathToSettings { + path: BiomePath, + settings: Settings, +} + #[derive(Debug, Default)] pub struct WorkspaceSettings { - /// Formatter settings applied to all files in the workspaces - pub formatter: FormatSettings, - /// Linter settings applied to all files in the workspace - pub linter: LinterSettings, - /// Language specific settings - pub languages: LanguageListSettings, - /// Filesystem settings for the workspace - pub files: FilesSettings, - /// Analyzer settings - pub organize_imports: OrganizeImportsSettings, - /// overrides - pub override_settings: OverrideSettings, + data: WorkspaceData, + current_workspace: ProjectKey, } impl WorkspaceSettings { - /// Retrieves the settings of the formatter - pub fn formatter(&self) -> &FormatSettings { - &self.formatter + /// Retrieves the settings of the current workspace folder + pub fn current_settings(&self) -> &Settings { + let data = self + .data + .get_value_by_key(self.current_workspace) + .expect("You must have at least one workspace."); + &data.settings } - /// Whether the formatter is disabled for JavaScript files - pub fn javascript_formatter_disabled(&self) -> bool { - let enabled = self.languages.javascript.formatter.enabled.as_ref(); - enabled == Some(&false) + /// Register a new + pub fn register_current_project(&mut self, key: ProjectKey) { + self.current_workspace = key; } - /// Whether the formatter is disabled for JSON files - pub fn json_formatter_disabled(&self) -> bool { - let enabled = self.languages.json.formatter.enabled.as_ref(); - enabled == Some(&false) + /// Retrieves a mutable reference of the settings of the current project + pub fn get_current_settings_mut(&mut self) -> &mut Settings { + &mut self + .data + .get_mut(self.current_workspace) + .expect("You must have at least one workspace.") + .settings } - /// Whether the formatter is disabled for CSS files - pub fn css_formatter_disabled(&self) -> bool { - let enabled = self.languages.css.formatter.enabled.as_ref(); - enabled == Some(&false) + /// Register a new project using its folder. Use [WorkspaceSettings::get_current_settings_mut] to retrieve + /// its settings and change them. + pub fn insert_project(&mut self, workspace_path: impl Into) -> ProjectKey { + self.data.insert(PathToSettings { + path: BiomePath::new(workspace_path.into()), + settings: Settings::default(), + }) } - /// Retrieves the settings of the linter - pub fn linter(&self) -> &LinterSettings { - &self.linter + /// Retrieves the settings by path. + pub fn get_settings_by_path(&self, path: &BiomePath) -> &Settings { + debug_assert!(path.is_absolute(), "Workspaces paths must be absolutes."); + debug_assert!( + !self.data.is_empty(), + "You must have at least one workspace." + ); + let iter = self.data.iter(); + for (_, path_to_settings) in iter { + if path.strip_prefix(path_to_settings.path.as_path()).is_ok() { + return &path_to_settings.settings; + } + } + unreachable!("We should not reach here, the assertions should help.") } +} - /// Retrieves the settings of the organize imports - pub fn organize_imports(&self) -> &OrganizeImportsSettings { - &self.organize_imports - } +/// Global settings for the entire workspace +#[derive(Debug, Default)] +pub struct Settings { + /// Formatter settings applied to all files in the workspaces + pub formatter: FormatSettings, + /// Linter settings applied to all files in the workspace + pub linter: LinterSettings, + /// Language specific settings + pub languages: LanguageListSettings, + /// Filesystem settings for the workspace + pub files: FilesSettings, + /// Analyzer settings + pub organize_imports: OrganizeImportsSettings, + /// overrides + pub override_settings: OverrideSettings, +} +impl Settings { /// The [PartialConfiguration] is merged into the workspace #[tracing::instrument(level = "trace", skip(self))] pub fn merge_with_configuration( @@ -148,6 +178,39 @@ impl WorkspaceSettings { Ok(()) } + /// Retrieves the settings of the formatter + pub fn formatter(&self) -> &FormatSettings { + &self.formatter + } + + /// Whether the formatter is disabled for JavaScript files + pub fn javascript_formatter_disabled(&self) -> bool { + let enabled = self.languages.javascript.formatter.enabled.as_ref(); + enabled == Some(&false) + } + + /// Whether the formatter is disabled for JSON files + pub fn json_formatter_disabled(&self) -> bool { + let enabled = self.languages.json.formatter.enabled.as_ref(); + enabled == Some(&false) + } + + /// Whether the formatter is disabled for CSS files + pub fn css_formatter_disabled(&self) -> bool { + let enabled = self.languages.css.formatter.enabled.as_ref(); + enabled == Some(&false) + } + + /// Retrieves the settings of the linter + pub fn linter(&self) -> &LinterSettings { + &self.linter + } + + /// Retrieves the settings of the organize imports + pub fn organize_imports(&self) -> &OrganizeImportsSettings { + &self.organize_imports + } + /// It retrieves the severity based on the `code` of the rule and the current configuration. /// /// The code of the has the following pattern: `{group}/{rule_name}`. @@ -504,9 +567,9 @@ impl<'a> SettingsHandle<'a> { } } -impl<'a> AsRef for SettingsHandle<'a> { - fn as_ref(&self) -> &WorkspaceSettings { - &self.inner +impl<'a> AsRef for SettingsHandle<'a> { + fn as_ref(&self) -> &Settings { + self.inner.current_settings() } } @@ -521,15 +584,52 @@ impl<'a> SettingsHandle<'a> { L: ServiceLanguage, { L::resolve_format_options( - &self.inner.formatter, - &self.inner.override_settings, - &L::lookup_settings(&self.inner.languages).formatter, + &self.inner.current_settings().formatter, + &self.inner.current_settings().override_settings, + &L::lookup_settings(&self.inner.current_settings().languages).formatter, path, file_source, ) } } +#[derive(Debug)] +pub struct SettingsHandleMut<'a> { + inner: RwLockWriteGuard<'a, WorkspaceSettings>, +} + +impl<'a> SettingsHandleMut<'a> { + pub(crate) fn new(settings: &'a RwLock) -> Self { + Self { + inner: settings.write().unwrap(), + } + } +} + +impl<'a> AsMut for SettingsHandleMut<'a> { + fn as_mut(&mut self) -> &mut Settings { + self.inner.get_current_settings_mut() + } +} + +pub struct WorkspacesHandleMut<'a> { + inner: RwLockWriteGuard<'a, WorkspaceSettings>, +} + +impl<'a> WorkspacesHandleMut<'a> { + pub(crate) fn new(settings: &'a RwLock) -> Self { + Self { + inner: settings.write().unwrap(), + } + } +} + +impl<'a> AsMut for WorkspacesHandleMut<'a> { + fn as_mut(&mut self) -> &mut WorkspaceSettings { + &mut self.inner + } +} + #[derive(Debug, Default)] pub struct OverrideSettings { pub patterns: Vec, @@ -1042,7 +1142,7 @@ impl TryFrom for OrganizeImportsSettings { pub fn to_override_settings( working_directory: Option, overrides: Overrides, - current_settings: &WorkspaceSettings, + current_settings: &Settings, ) -> Result { let mut override_settings = OverrideSettings::default(); for mut pattern in overrides.0 { diff --git a/crates/biome_service/src/workspace.rs b/crates/biome_service/src/workspace.rs index c381fe9aa21e..512c082b55ce 100644 --- a/crates/biome_service/src/workspace.rs +++ b/crates/biome_service/src/workspace.rs @@ -63,6 +63,9 @@ use biome_formatter::Printed; use biome_fs::BiomePath; use biome_js_syntax::{TextRange, TextSize}; use biome_text_edit::TextEdit; +#[cfg(feature = "schema")] +use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; +use slotmap::{new_key_type, DenseSlotMap}; use std::collections::HashMap; use std::ffi::OsStr; use std::path::{Path, PathBuf}; @@ -71,7 +74,7 @@ use tracing::debug; pub use self::client::{TransportRequest, WorkspaceClient, WorkspaceTransport}; pub use crate::file_handlers::DocumentFileSource; -use crate::settings::WorkspaceSettings; +use crate::settings::Settings; mod client; mod server; @@ -149,7 +152,7 @@ impl FileFeaturesResult { pub(crate) fn with_settings_and_language( mut self, - settings: &WorkspaceSettings, + settings: &Settings, file_source: &DocumentFileSource, path: &Path, ) -> Self { @@ -405,7 +408,7 @@ pub struct UpdateSettingsParams { pub vcs_base_path: Option, // @ematipico TODO: have a better data structure for this pub gitignore_matches: Vec, - pub working_directory: Option, + pub workspace_directory: Option, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -731,6 +734,14 @@ pub struct IsPathIgnoredParams { pub features: Vec, } +#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct RegisterProjectFolderParams { + pub path: Option, + pub set_as_current_workspace: bool, +} + pub trait Workspace: Send + Sync + RefUnwindSafe { /// Checks whether a certain feature is supported. There are different conditions: /// - Biome doesn't recognize a file, so it can't provide the feature; @@ -758,6 +769,12 @@ pub trait Workspace: Send + Sync + RefUnwindSafe { /// Add a new project to the workspace fn open_project(&self, params: OpenProjectParams) -> Result<(), WorkspaceError>; + /// Register a possible workspace folders. Returns the key of said workspace. Use this key when you want to switch to different workspaces. + fn register_project_folder( + &self, + params: RegisterProjectFolderParams, + ) -> Result; + /// Sets the current project path fn update_current_project(&self, params: UpdateProjectParams) -> Result<(), WorkspaceError>; @@ -984,3 +1001,74 @@ fn test_order() { assert!(items[0] < items[1], "{} < {}", items[0], items[1]); } } + +new_key_type! { + pub struct ProjectKey; +} + +#[cfg(feature = "schema")] +impl JsonSchema for ProjectKey { + fn schema_name() -> String { + "ProjectKey".to_string() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + ::json_schema(gen) + } +} + +#[derive(Debug, Default)] +pub struct WorkspaceData { + /// [DenseSlotMap] is the slowest type in insertion/removal, but the fastest in iteration + /// + /// Users wouldn't change workspace folders very often, + paths: DenseSlotMap, +} + +impl WorkspaceData { + /// Inserts an item + pub fn insert(&mut self, item: T) -> ProjectKey { + self.paths.insert(item) + } + + /// Removes an item + pub fn remove(&mut self, key: ProjectKey) { + self.paths.remove(key); + } + + pub fn get_value_by_key(&self, key: ProjectKey) -> Option<&T> { + self.paths.get(key) + } + + pub fn get_mut(&mut self, key: ProjectKey) -> Option<&mut T> { + self.paths.get_mut(key) + } + + pub fn is_empty(&self) -> bool { + self.paths.is_empty() + } + + pub fn iter(&self) -> WorkspaceDataIterator<'_, T> { + WorkspaceDataIterator::new(self) + } +} + +pub struct WorkspaceDataIterator<'a, V> { + iterator: slotmap::dense::Iter<'a, ProjectKey, V>, +} + +impl<'a, V> WorkspaceDataIterator<'a, V> { + fn new(data: &'a WorkspaceData) -> Self { + Self { + iterator: data.paths.iter(), + } + } +} + +impl<'a, V> Iterator for WorkspaceDataIterator<'a, V> { + type Item = (ProjectKey, &'a V); + + fn next(&mut self) -> Option { + self.iterator.next() + } +} diff --git a/crates/biome_service/src/workspace/client.rs b/crates/biome_service/src/workspace/client.rs index d9207d7ad7cc..e45d7a6fbf92 100644 --- a/crates/biome_service/src/workspace/client.rs +++ b/crates/biome_service/src/workspace/client.rs @@ -1,7 +1,7 @@ use crate::workspace::{ FileFeaturesResult, GetFileContentParams, IsPathIgnoredParams, OpenProjectParams, - OrganizeImportsParams, OrganizeImportsResult, RageParams, RageResult, ServerInfo, - UpdateProjectParams, + OrganizeImportsParams, OrganizeImportsResult, ProjectKey, RageParams, RageResult, + RegisterProjectFolderParams, ServerInfo, UpdateProjectParams, }; use crate::{TransportError, Workspace, WorkspaceError}; use biome_formatter::Printed; @@ -110,7 +110,6 @@ where fn is_path_ignored(&self, params: IsPathIgnoredParams) -> Result { self.request("biome/is_path_ignored", params) } - fn update_settings(&self, params: UpdateSettingsParams) -> Result<(), WorkspaceError> { self.request("biome/update_settings", params) } @@ -123,6 +122,13 @@ where self.request("biome/open_project", params) } + fn register_project_folder( + &self, + params: RegisterProjectFolderParams, + ) -> Result { + self.request("biome/register_project_folder", params) + } + fn update_current_project(&self, params: UpdateProjectParams) -> Result<(), WorkspaceError> { self.request("biome/update_current_project", params) } diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 1183f46e95ad..f6d7c03b2b0f 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -2,22 +2,20 @@ use super::{ ChangeFileParams, CloseFileParams, FeatureName, FixFileResult, FormatFileParams, FormatOnTypeParams, FormatRangeParams, GetControlFlowGraphParams, GetFormatterIRParams, GetSyntaxTreeParams, GetSyntaxTreeResult, OpenFileParams, OpenProjectParams, - ParsePatternParams, ParsePatternResult, PatternId, PullActionsParams, PullActionsResult, - PullDiagnosticsParams, PullDiagnosticsResult, RenameResult, SearchPatternParams, SearchResults, - SupportsFeatureParams, UpdateProjectParams, UpdateSettingsParams, + ParsePatternParams, ParsePatternResult, PatternId, ProjectKey, PullActionsParams, + PullActionsResult, PullDiagnosticsParams, PullDiagnosticsResult, RegisterProjectFolderParams, + RenameResult, SearchPatternParams, SearchResults, SupportsFeatureParams, UpdateProjectParams, + UpdateSettingsParams, }; use crate::file_handlers::{ Capabilities, CodeActionsParams, DocumentFileSource, FixAllParams, LintParams, ParseResult, }; +use crate::settings::{SettingsHandleMut, WorkspaceSettings, WorkspacesHandleMut}; use crate::workspace::{ FileFeaturesResult, GetFileContentParams, IsPathIgnoredParams, OrganizeImportsParams, OrganizeImportsResult, RageEntry, RageParams, RageResult, ServerInfo, }; -use crate::{ - file_handlers::Features, - settings::{SettingsHandle, WorkspaceSettings}, - Workspace, WorkspaceError, -}; +use crate::{file_handlers::Features, settings::SettingsHandle, Workspace, WorkspaceError}; use biome_analyze::AnalysisFilter; use biome_diagnostics::{ serde::Diagnostic as SerdeDiagnostic, Diagnostic, DiagnosticExt, Severity, @@ -98,10 +96,20 @@ impl WorkspaceServer { } } + /// Provides a reference to the current settings fn settings(&self) -> SettingsHandle { SettingsHandle::new(&self.settings) } + /// Provides a mutable reference to the current settings + fn settings_mut(&self) -> SettingsHandleMut { + SettingsHandleMut::new(&self.settings) + } + + fn workspaces_mut(&self) -> WorkspacesHandleMut { + WorkspacesHandleMut::new(&self.settings) + } + /// Get the supported capabilities for a given file path fn get_file_capabilities(&self, path: &BiomePath) -> Capabilities { let language = self.get_file_source(path); @@ -338,14 +346,14 @@ impl Workspace for WorkspaceServer { let capabilities = self.get_file_capabilities(¶ms.path); let language = DocumentFileSource::from_path(¶ms.path); let path = params.path.as_path(); - let settings = self.settings.read().unwrap(); + let settings = self.settings(); let mut file_features = FileFeaturesResult::new(); let file_name = path.file_name().and_then(|s| s.to_str()); file_features = file_features .with_capabilities(&capabilities) - .with_settings_and_language(&settings, &language, path); + .with_settings_and_language(settings.as_ref(), &language, path); - if settings.files.ignore_unknown + if settings.as_ref().files.ignore_unknown && language == DocumentFileSource::Unknown && self.get_file_source(¶ms.path) == DocumentFileSource::Unknown { @@ -386,11 +394,11 @@ impl Workspace for WorkspaceServer { /// by another thread having previously panicked while holding the lock #[tracing::instrument(level = "trace", skip(self))] fn update_settings(&self, params: UpdateSettingsParams) -> Result<(), WorkspaceError> { - let mut settings = self.settings.write().unwrap(); + let mut settings = self.settings_mut(); - settings.merge_with_configuration( + settings.as_mut().merge_with_configuration( params.configuration, - params.working_directory, + params.workspace_directory, params.vcs_base_path, params.gitignore_matches.as_slice(), )?; @@ -399,7 +407,6 @@ impl Workspace for WorkspaceServer { self.file_features.clear(); Ok(()) } - /// Add a new file to the workspace fn open_file(&self, params: OpenFileParams) -> Result<(), WorkspaceError> { let index = self.set_source( @@ -419,7 +426,6 @@ impl Workspace for WorkspaceServer { ); Ok(()) } - fn open_project(&self, params: OpenProjectParams) -> Result<(), WorkspaceError> { let index = self.set_source(JsonFileSource::json().into()); self.syntax.remove(¶ms.path); @@ -435,6 +441,20 @@ impl Workspace for WorkspaceServer { Ok(()) } + fn register_project_folder( + &self, + params: RegisterProjectFolderParams, + ) -> Result { + let mut workspace = self.workspaces_mut(); + let key = workspace + .as_mut() + .insert_project(params.path.unwrap_or_default()); + if params.set_as_current_workspace { + workspace.as_mut().register_current_project(key); + } + Ok(key) + } + fn update_current_project(&self, params: UpdateProjectParams) -> Result<(), WorkspaceError> { let mut current_project_path = self.current_project_path.write().unwrap(); let _ = current_project_path.insert(params.path); @@ -586,8 +606,8 @@ impl Workspace for WorkspaceServer { .ok_or_else(self.build_capability_error(¶ms.path))?; let parse = self.get_parse(params.path.clone())?; - let settings = self.settings.read().unwrap(); - let rules = settings.linter().rules.as_ref(); + let settings = self.settings(); + let rules = settings.as_ref().linter().rules.as_ref(); let manifest = self.get_current_project()?.map(|pr| pr.manifest); let language = self.get_file_source(¶ms.path); Ok(code_actions(CodeActionsParams { @@ -671,10 +691,10 @@ impl Workspace for WorkspaceServer { .analyzer .fix_all .ok_or_else(self.build_capability_error(¶ms.path))?; - let settings = self.settings.read().unwrap(); + let settings = self.settings(); let parse = self.get_parse(params.path.clone())?; // Compute final rules (taking `overrides` into account) - let rules = settings.as_rules(params.path.as_path()); + let rules = settings.as_ref().as_rules(params.path.as_path()); let rule_filter_list = rules .as_ref() .map(|rules| rules.as_enabled_rules()) diff --git a/crates/biome_service/src/workspace_types.rs b/crates/biome_service/src/workspace_types.rs index 7e447f4377f7..36df09a7d986 100644 --- a/crates/biome_service/src/workspace_types.rs +++ b/crates/biome_service/src/workspace_types.rs @@ -451,10 +451,11 @@ macro_rules! workspace_method { } /// Returns a list of signature for all the methods in the [Workspace] trait -pub fn methods() -> [WorkspaceMethod; 19] { +pub fn methods() -> [WorkspaceMethod; 20] { [ WorkspaceMethod::of::("file_features"), workspace_method!(update_settings), + workspace_method!(register_project_folder), workspace_method!(update_current_project), workspace_method!(open_project), workspace_method!(open_file), diff --git a/crates/biome_test_utils/src/lib.rs b/crates/biome_test_utils/src/lib.rs index 3ccc4b523026..30159e4695b2 100644 --- a/crates/biome_test_utils/src/lib.rs +++ b/crates/biome_test_utils/src/lib.rs @@ -9,7 +9,7 @@ use biome_json_parser::{JsonParserOptions, ParseDiagnostic}; use biome_project::PackageJson; use biome_rowan::{SyntaxKind, SyntaxNode, SyntaxSlot}; use biome_service::configuration::to_analyzer_rules; -use biome_service::settings::{ServiceLanguage, WorkspaceSettings}; +use biome_service::settings::{ServiceLanguage, Settings}; use json_comments::StripComments; use similar::TextDiff; use std::ffi::{c_int, OsStr}; @@ -67,7 +67,7 @@ pub fn create_analyzer_options( ); } else { let configuration = deserialized.into_deserialized().unwrap_or_default(); - let mut settings = WorkspaceSettings::default(); + let mut settings = Settings::default(); analyzer_configuration.preferred_quote = configuration .javascript .as_ref() diff --git a/crates/biome_wasm/src/lib.rs b/crates/biome_wasm/src/lib.rs index 8ff379832869..fa96cb405a42 100644 --- a/crates/biome_wasm/src/lib.rs +++ b/crates/biome_wasm/src/lib.rs @@ -5,7 +5,7 @@ use biome_service::workspace::{ self, ChangeFileParams, CloseFileParams, FixFileParams, FormatFileParams, FormatOnTypeParams, FormatRangeParams, GetControlFlowGraphParams, GetFileContentParams, GetFormatterIRParams, GetSyntaxTreeParams, OrganizeImportsParams, PullActionsParams, PullDiagnosticsParams, - RenameParams, UpdateSettingsParams, + RegisterProjectFolderParams, RenameParams, UpdateSettingsParams, }; use biome_service::workspace::{OpenFileParams, SupportsFeatureParams}; @@ -56,6 +56,21 @@ impl Workspace { self.inner.update_settings(params).map_err(into_error) } + #[wasm_bindgen(js_name = registerProjectFolder)] + pub fn register_workspace_folder( + &self, + params: IRegisterProjectFolderParams, + ) -> Result { + let params: RegisterProjectFolderParams = + serde_wasm_bindgen::from_value(params.into()).map_err(into_error)?; + let result = self + .inner + .register_project_folder(params) + .map_err(into_error)?; + + to_value(&result).map(IProjectKey::from).map_err(into_error) + } + #[wasm_bindgen(js_name = openFile)] pub fn open_file(&self, params: IOpenFileParams) -> Result<(), Error> { let params: OpenFileParams = diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 64e80b479d75..6683262c4c26 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -21,7 +21,7 @@ export interface UpdateSettingsParams { configuration: PartialConfiguration; gitignore_matches: string[]; vcs_base_path?: string; - working_directory?: string; + workspace_directory?: string; } /** * The configuration that is contained inside the file `biome.json` @@ -1729,6 +1729,11 @@ export type FilenameCase = | "kebab-case" | "PascalCase" | "snake_case"; +export interface RegisterProjectFolderParams { + path?: string; + setAsCurrentWorkspace: boolean; +} +export type ProjectKey = string; export interface UpdateProjectParams { path: BiomePath; } @@ -2337,6 +2342,9 @@ export type Configuration = PartialConfiguration; export interface Workspace { fileFeatures(params: SupportsFeatureParams): Promise; updateSettings(params: UpdateSettingsParams): Promise; + registerProjectFolder( + params: RegisterProjectFolderParams, + ): Promise; updateCurrentProject(params: UpdateProjectParams): Promise; openProject(params: OpenProjectParams): Promise; openFile(params: OpenFileParams): Promise; @@ -2368,6 +2376,9 @@ export function createWorkspace(transport: Transport): Workspace { updateSettings(params) { return transport.request("biome/update_settings", params); }, + registerProjectFolder(params) { + return transport.request("biome/register_project_folder", params); + }, updateCurrentProject(params) { return transport.request("biome/update_current_project", params); }, diff --git a/packages/@biomejs/js-api/src/index.ts b/packages/@biomejs/js-api/src/index.ts index 1e6e8b2d768b..430b70af9b16 100644 --- a/packages/@biomejs/js-api/src/index.ts +++ b/packages/@biomejs/js-api/src/index.ts @@ -101,7 +101,9 @@ export class Biome { public static async create(options: BiomeCreate): Promise { const module = await loadModule(options.distribution); const workspace = new module.Workspace(); - return new Biome(module, workspace); + const biome = new Biome(module, workspace); + biome.registerProjectFolder(); + return biome; } /** @@ -126,12 +128,21 @@ export class Biome { this.workspace.updateSettings({ configuration, gitignore_matches: [], + workspace_directory: "./", }); } catch (e) { throw wrapError(e); } } + public registerProjectFolder(): void; + public registerProjectFolder(path?: string): void { + this.workspace.registerProjectFolder({ + path, + setAsCurrentWorkspace: true, + }); + } + private tryCatchWrapper(func: () => T): T { try { return func();