From ba8d566356acf213fa521c1e8b975a79232082ec Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 20 Dec 2024 16:52:32 -0500 Subject: [PATCH 01/44] Add support for reading `air.toml` in the CLI and LSP --- Cargo.lock | 78 +++++- Cargo.toml | 7 +- crates/air/Cargo.toml | 3 +- crates/air/src/commands/format.rs | 88 ++---- crates/air_r_formatter/src/context.rs | 14 + crates/air_r_formatter/src/lib.rs | 1 + crates/air_r_formatter/src/options.rs | 3 + .../src/options/magic_line_break.rs | 44 +++ crates/lsp/Cargo.toml | 1 + crates/lsp/src/capabilities.rs | 40 +++ crates/lsp/src/error.rs | 52 ++++ crates/lsp/src/handlers.rs | 41 ++- crates/lsp/src/handlers_format.rs | 25 +- crates/lsp/src/handlers_state.rs | 98 ++++--- crates/lsp/src/lib.rs | 3 + crates/lsp/src/main_loop.rs | 53 ++-- ...__tests__format_range_logical_lines-4.snap | 2 +- ...tests__format_range_unmatched_lists-2.snap | 2 +- ...tests__format_range_unmatched_lists-3.snap | 2 +- ...tests__format_range_unmatched_lists-4.snap | 2 +- ...__tests__format_range_unmatched_lists.snap | 2 +- crates/lsp/src/state.rs | 8 - crates/lsp/src/tower_lsp.rs | 2 +- crates/lsp/src/workspaces.rs | 174 ++++++++++++ crates/workspace/Cargo.toml | 31 +++ crates/workspace/src/lib.rs | 4 + crates/workspace/src/resolve.rs | 252 ++++++++++++++++++ crates/workspace/src/settings.rs | 60 +++++ crates/workspace/src/settings/indent_style.rs | 54 ++++ crates/workspace/src/settings/indent_width.rs | 148 ++++++++++ crates/workspace/src/settings/line_ending.rs | 31 +++ crates/workspace/src/settings/line_length.rs | 145 ++++++++++ .../src/settings/magic_line_break.rs | 53 ++++ ...__tests__deserialize_oob_indent_width.snap | 9 + ...h__tests__deserialize_oob_line_length.snap | 9 + crates/workspace/src/toml.rs | 117 ++++++++ crates/workspace/src/toml_options.rs | 121 +++++++++ editors/code/src/lsp.ts | 5 - 38 files changed, 1616 insertions(+), 168 deletions(-) create mode 100644 crates/air_r_formatter/src/options.rs create mode 100644 crates/air_r_formatter/src/options/magic_line_break.rs create mode 100644 crates/lsp/src/capabilities.rs create mode 100644 crates/lsp/src/error.rs create mode 100644 crates/lsp/src/workspaces.rs create mode 100644 crates/workspace/Cargo.toml create mode 100644 crates/workspace/src/lib.rs create mode 100644 crates/workspace/src/resolve.rs create mode 100644 crates/workspace/src/settings.rs create mode 100644 crates/workspace/src/settings/indent_style.rs create mode 100644 crates/workspace/src/settings/indent_width.rs create mode 100644 crates/workspace/src/settings/line_ending.rs create mode 100644 crates/workspace/src/settings/line_length.rs create mode 100644 crates/workspace/src/settings/magic_line_break.rs create mode 100644 crates/workspace/src/settings/snapshots/workspace__settings__indent_width__tests__deserialize_oob_indent_width.snap create mode 100644 crates/workspace/src/settings/snapshots/workspace__settings__line_length__tests__deserialize_oob_line_length.snap create mode 100644 crates/workspace/src/toml.rs create mode 100644 crates/workspace/src/toml_options.rs diff --git a/Cargo.lock b/Cargo.lock index 2985a699..dca9edda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,7 @@ dependencies = [ "thiserror 2.0.5", "tokio", "tracing", + "workspace", ] [[package]] @@ -328,6 +329,8 @@ dependencies = [ "drop_bomb", "indexmap", "rustc-hash", + "schemars", + "serde", "tracing", "unicode-width", ] @@ -1346,6 +1349,7 @@ dependencies = [ "triomphe", "url", "uuid", + "workspace", ] [[package]] @@ -1672,9 +1676,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustix" @@ -1849,6 +1853,15 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2203,6 +2216,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -2637,6 +2684,33 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "workspace" +version = "0.1.0" +dependencies = [ + "air_r_formatter", + "anyhow", + "biome_formatter", + "fs", + "ignore", + "insta", + "line_ending", + "rustc-hash", + "serde", + "tempfile", + "thiserror 2.0.5", + "toml", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index b6b56566..2627d3a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ line_ending = { path = "./crates/line_ending" } lsp = { path = "./crates/lsp" } lsp_test = { path = "./crates/lsp_test" } tests_macros = { path = "./crates/tests_macros" } +workspace = { path = "./crates/workspace" } anyhow = "1.0.89" assert_matches = "1.5.0" @@ -59,14 +60,17 @@ line-index = "0.1.2" memchr = "2.7.4" path-absolutize = "3.1.1" proc-macro2 = "1.0.86" -serde = { version = "1.0.215", features = ["derive"] } +rustc-hash = "2.1.0" +serde = "1.0.215" serde_json = "1.0.132" struct-field-names-as-array = "0.3.0" strum = "0.26" +tempfile = "3.9.0" time = "0.3.37" thiserror = "2.0.5" tokio = { version = "1.41.1" } tokio-util = "0.7.12" +toml = "0.8.19" # For https://github.com/ebkalderon/tower-lsp/pull/428 tower-lsp = { git = "https://github.com/lionel-/tower-lsp", branch = "bugfix/patches" } tracing = { version = "0.1.40", default-features = false, features = ["std"] } @@ -124,7 +128,6 @@ unnecessary_join = "warn" unnested_or_patterns = "warn" unreadable_literal = "warn" verbose_bit_mask = "warn" -zero_sized_map_values = "warn" # restriction cfg_not_test = "warn" diff --git a/crates/air/Cargo.toml b/crates/air/Cargo.toml index e11590ad..305e707f 100644 --- a/crates/air/Cargo.toml +++ b/crates/air/Cargo.toml @@ -28,9 +28,10 @@ lsp = { workspace = true } thiserror = { workspace = true } tokio = "1.41.1" tracing = { workspace = true } +workspace = { workspace = true } [dev-dependencies] -tempfile = "3.9.0" +tempfile = { workspace = true } [lints] workspace = true diff --git a/crates/air/src/commands/format.rs b/crates/air/src/commands/format.rs index 46c127a6..d2feacf8 100644 --- a/crates/air/src/commands/format.rs +++ b/crates/air/src/commands/format.rs @@ -8,23 +8,32 @@ use std::path::PathBuf; use air_r_formatter::context::RFormatOptions; use air_r_parser::RParserOptions; use fs::relativize_path; -use ignore::DirEntry; use itertools::Either; use itertools::Itertools; -use line_ending::LineEnding; use thiserror::Error; +use workspace::resolve::discover_r_file_paths; +use workspace::resolve::SettingsResolver; +use workspace::settings::FormatSettings; +use workspace::settings::Settings; use crate::args::FormatCommand; use crate::ExitStatus; pub(crate) fn format(command: FormatCommand) -> anyhow::Result { let mode = FormatMode::from_command(&command); - let paths = resolve_paths(&command.paths); + + let paths = discover_r_file_paths(&command.paths); + + let mut resolver = SettingsResolver::new(Settings::default()); + resolver.load_from_paths(&command.paths)?; let (actions, errors): (Vec<_>, Vec<_>) = paths .into_iter() .map(|path| match path { - Ok(path) => format_file(path, mode), + Ok(path) => { + let settings = resolver.resolve_or_fallback(&path); + format_file(path, mode, &settings.format) + } Err(err) => Err(err.into()), }) .partition_map(|result| match result { @@ -99,62 +108,6 @@ fn write_changed(actions: &[FormatFileAction], f: &mut impl Write) -> io::Result Ok(()) } -fn resolve_paths(paths: &[PathBuf]) -> Vec> { - let paths: Vec = paths.iter().map(fs::normalize_path).collect(); - - let (first_path, paths) = paths - .split_first() - .expect("Clap should ensure at least 1 path is supplied."); - - // TODO: Parallel directory visitor - let mut builder = ignore::WalkBuilder::new(first_path); - - for path in paths { - builder.add(path); - } - - let mut out = Vec::new(); - - for path in builder.build() { - match path { - Ok(entry) => { - if let Some(path) = is_valid_path(entry) { - out.push(Ok(path)); - } - } - Err(err) => { - out.push(Err(err)); - } - } - } - - out -} - -// Decide whether or not to accept an `entry` based on include/exclude rules. -fn is_valid_path(entry: DirEntry) -> Option { - // Ignore directories - if entry.file_type().map_or(true, |ft| ft.is_dir()) { - return None; - } - - // Accept all files that are passed-in directly, even non-R files - if entry.depth() == 0 { - let path = entry.into_path(); - return Some(path); - } - - // Otherwise check if we should accept this entry - // TODO: Many other checks based on user exclude/includes - let path = entry.into_path(); - - if !fs::has_r_extension(&path) { - return None; - } - - Some(path) -} - pub(crate) enum FormatFileAction { Formatted(PathBuf), Unchanged, @@ -166,18 +119,15 @@ impl FormatFileAction { } } -// TODO: Take workspace `FormatOptions` that get resolved to `RFormatOptions` -// for the formatter here. Respect user specified `LineEnding` option too, and -// only use inferred endings when `FormatOptions::LineEnding::Auto` is used. -fn format_file(path: PathBuf, mode: FormatMode) -> Result { +fn format_file( + path: PathBuf, + mode: FormatMode, + settings: &FormatSettings, +) -> Result { let source = std::fs::read_to_string(&path) .map_err(|err| FormatCommandError::Read(path.clone(), err))?; - let line_ending = match line_ending::infer(&source) { - LineEnding::Lf => biome_formatter::LineEnding::Lf, - LineEnding::Crlf => biome_formatter::LineEnding::Crlf, - }; - let options = RFormatOptions::default().with_line_ending(line_ending); + let options = settings.to_format_options(&source); let source = line_ending::normalize(source); let formatted = match format_source(source.as_str(), options) { diff --git a/crates/air_r_formatter/src/context.rs b/crates/air_r_formatter/src/context.rs index 84acba35..a4956636 100644 --- a/crates/air_r_formatter/src/context.rs +++ b/crates/air_r_formatter/src/context.rs @@ -17,6 +17,7 @@ use biome_formatter::TransformSourceMap; use crate::comments::FormatRLeadingComment; use crate::comments::RCommentStyle; use crate::comments::RComments; +use crate::options::MagicLineBreak; pub struct RFormatContext { options: RFormatOptions, @@ -77,6 +78,10 @@ pub struct RFormatOptions { /// The max width of a line. Defaults to 80. line_width: LineWidth, + + // TODO: Actually use this internally! + /// The behavior of magic line breaks. + magic_line_break: MagicLineBreak, } impl RFormatOptions { @@ -106,6 +111,11 @@ impl RFormatOptions { self } + pub fn with_magic_line_break(mut self, magic_line_break: MagicLineBreak) -> Self { + self.magic_line_break = magic_line_break; + self + } + pub fn set_indent_style(&mut self, indent_style: IndentStyle) { self.indent_style = indent_style; } @@ -121,6 +131,10 @@ impl RFormatOptions { pub fn set_line_width(&mut self, line_width: LineWidth) { self.line_width = line_width; } + + pub fn set_magic_line_break(&mut self, magic_line_break: MagicLineBreak) { + self.magic_line_break = magic_line_break; + } } impl FormatOptions for RFormatOptions { diff --git a/crates/air_r_formatter/src/lib.rs b/crates/air_r_formatter/src/lib.rs index 93fff37f..760efe64 100644 --- a/crates/air_r_formatter/src/lib.rs +++ b/crates/air_r_formatter/src/lib.rs @@ -21,6 +21,7 @@ use crate::cst::FormatRSyntaxNode; pub mod comments; pub mod context; mod cst; +pub mod options; mod prelude; mod r; pub(crate) mod separated; diff --git a/crates/air_r_formatter/src/options.rs b/crates/air_r_formatter/src/options.rs new file mode 100644 index 00000000..7c04088a --- /dev/null +++ b/crates/air_r_formatter/src/options.rs @@ -0,0 +1,3 @@ +mod magic_line_break; + +pub use magic_line_break::*; diff --git a/crates/air_r_formatter/src/options/magic_line_break.rs b/crates/air_r_formatter/src/options/magic_line_break.rs new file mode 100644 index 00000000..ec331bf5 --- /dev/null +++ b/crates/air_r_formatter/src/options/magic_line_break.rs @@ -0,0 +1,44 @@ +use std::fmt::Display; +use std::str::FromStr; + +#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)] +pub enum MagicLineBreak { + /// Respect + #[default] + Respect, + /// Ignore + Ignore, +} + +impl MagicLineBreak { + /// Returns `true` if magic line breaks should be respected. + pub const fn is_respect(&self) -> bool { + matches!(self, MagicLineBreak::Respect) + } + + /// Returns `true` if magic line breaks should be ignored. + pub const fn is_ignore(&self) -> bool { + matches!(self, MagicLineBreak::Ignore) + } +} + +impl FromStr for MagicLineBreak { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "respect" => Ok(Self::Respect), + "ignore" => Ok(Self::Ignore), + _ => Err("Unsupported value for this option"), + } + } +} + +impl Display for MagicLineBreak { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MagicLineBreak::Respect => std::write!(f, "Respect"), + MagicLineBreak::Ignore => std::write!(f, "Ignore"), + } + } +} diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 3c726341..6a356eb2 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -42,6 +42,7 @@ tree-sitter-r.workspace = true triomphe.workspace = true url.workspace = true uuid = { workspace = true, features = ["v4"] } +workspace = { workspace = true } [dev-dependencies] assert_matches.workspace = true diff --git a/crates/lsp/src/capabilities.rs b/crates/lsp/src/capabilities.rs new file mode 100644 index 00000000..9b46941f --- /dev/null +++ b/crates/lsp/src/capabilities.rs @@ -0,0 +1,40 @@ +use tower_lsp::lsp_types::ClientCapabilities; +use tower_lsp::lsp_types::PositionEncodingKind; + +/// A resolved representation of the [ClientCapabilities] the Client sends over that we +/// actually do something with +#[derive(Debug, Default)] +pub(crate) struct ResolvedClientCapabilities { + pub(crate) position_encodings: Vec, + pub(crate) dynamic_registration_for_did_change_configuration: bool, + pub(crate) dynamic_registration_for_did_change_watched_files: bool, +} + +impl ResolvedClientCapabilities { + pub(crate) fn new(capabilities: ClientCapabilities) -> Self { + let position_encodings = capabilities + .general + .and_then(|general_client_capabilities| general_client_capabilities.position_encodings) + .unwrap_or(vec![PositionEncodingKind::UTF16]); + + let dynamic_registration_for_did_change_configuration = capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.did_change_configuration) + .and_then(|did_change_configuration| did_change_configuration.dynamic_registration) + .unwrap_or(false); + + let dynamic_registration_for_did_change_watched_files = capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.did_change_watched_files) + .and_then(|watched_files| watched_files.dynamic_registration) + .unwrap_or_default(); + + Self { + position_encodings, + dynamic_registration_for_did_change_configuration, + dynamic_registration_for_did_change_watched_files, + } + } +} diff --git a/crates/lsp/src/error.rs b/crates/lsp/src/error.rs new file mode 100644 index 00000000..fe19c650 --- /dev/null +++ b/crates/lsp/src/error.rs @@ -0,0 +1,52 @@ +/// A tool for collecting multiple anyhow errors into a single [`anyhow::Result`] +/// +/// Only applicable if the intended `Ok()` value at the end is `()`. +#[derive(Debug, Default)] +pub(crate) struct ErrorVec { + errors: Vec, +} + +impl ErrorVec { + pub(crate) fn new() -> Self { + Self::default() + } + + /// Conditionally push to the error vector if the `result` is an `Err` case + pub(crate) fn push_err(&mut self, result: anyhow::Result) { + match result { + Ok(_) => (), + Err(error) => self.push(error), + } + } + + /// Push a new error to the error vector + pub(crate) fn push(&mut self, error: anyhow::Error) { + self.errors.push(error); + } + + /// Convert a error vector into a single [`anyhow::Result`] that knows how to print + /// each of the individual errors + pub(crate) fn into_result(self) -> anyhow::Result<()> { + if self.errors.is_empty() { + Ok(()) + } else { + Err(anyhow::anyhow!(self)) + } + } +} + +impl std::error::Error for ErrorVec {} + +impl std::fmt::Display for ErrorVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.errors.len() > 1 { + f.write_str("Multiple errors:\n")?; + } + + for error in &self.errors { + std::fmt::Display::fmt(error, f)?; + } + + Ok(()) + } +} diff --git a/crates/lsp/src/handlers.rs b/crates/lsp/src/handlers.rs index 53cb5ebc..e7c54902 100644 --- a/crates/lsp/src/handlers.rs +++ b/crates/lsp/src/handlers.rs @@ -7,6 +7,8 @@ use struct_field_names_as_array::FieldNamesAsArray; use tower_lsp::lsp_types; +use tower_lsp::lsp_types::DidChangeWatchedFilesRegistrationOptions; +use tower_lsp::lsp_types::FileSystemWatcher; use tower_lsp::Client; use tracing::Instrument; @@ -24,30 +26,55 @@ pub(crate) async fn handle_initialized( let span = tracing::info_span!("handle_initialized").entered(); // Register capabilities to the client - let mut regs: Vec = vec![]; + let mut registrations: Vec = vec![]; - if lsp_state.needs_registration.did_change_configuration { + if lsp_state + .capabilities + .dynamic_registration_for_did_change_configuration + { // The `didChangeConfiguration` request instructs the client to send // a notification when the tracked settings have changed. // // Note that some settings, such as editor indentation properties, may be // changed by extensions or by the user without changing the actual // underlying setting. Unfortunately we don't receive updates in that case. - let mut config_document_regs = collect_regs( + let mut config_document_registrations = collect_regs( VscDocumentConfig::FIELD_NAMES_AS_ARRAY.to_vec(), VscDocumentConfig::section_from_key, ); - let mut config_diagnostics_regs: Vec = collect_regs( + let mut config_diagnostics_registrations: Vec = collect_regs( VscDiagnosticsConfig::FIELD_NAMES_AS_ARRAY.to_vec(), VscDiagnosticsConfig::section_from_key, ); - regs.append(&mut config_document_regs); - regs.append(&mut config_diagnostics_regs); + registrations.append(&mut config_document_registrations); + registrations.append(&mut config_diagnostics_registrations); + } + + if lsp_state + .capabilities + .dynamic_registration_for_did_change_watched_files + { + // Watch for changes in `air.toml` files so we can react dynamically + let watch_air_toml_registration = lsp_types::Registration { + id: uuid::Uuid::new_v4().to_string(), + method: "workspace/didChangeWatchedFiles".into(), + register_options: Some( + serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { + watchers: vec![FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String("**/air.toml".into()), + kind: None, + }], + }) + .unwrap(), + ), + }; + + registrations.push(watch_air_toml_registration); } client - .register_capability(regs) + .register_capability(registrations) .instrument(span.exit()) .await?; Ok(()) diff --git a/crates/lsp/src/handlers_format.rs b/crates/lsp/src/handlers_format.rs index c9de94b3..3a635843 100644 --- a/crates/lsp/src/handlers_format.rs +++ b/crates/lsp/src/handlers_format.rs @@ -5,35 +5,32 @@ // // -use air_r_formatter::{context::RFormatOptions, format_node}; +use air_r_formatter::format_node; use air_r_syntax::{RExpressionList, RSyntaxKind, RSyntaxNode, WalkEvent}; -use biome_formatter::{IndentStyle, LineWidth}; use biome_rowan::{AstNode, Language, SyntaxElement}; use biome_text_size::{TextRange, TextSize}; use tower_lsp::lsp_types; +use crate::main_loop::LspState; use crate::state::WorldState; use crate::{from_proto, to_proto}; #[tracing::instrument(level = "info", skip_all)] pub(crate) fn document_formatting( params: lsp_types::DocumentFormattingParams, + lsp_state: &LspState, state: &WorldState, ) -> anyhow::Result>> { let doc = state.get_document(¶ms.text_document.uri)?; - let line_width = LineWidth::try_from(80).map_err(|err| anyhow::anyhow!("{err}"))?; - - // TODO: Handle FormattingOptions - let options = RFormatOptions::default() - .with_indent_style(IndentStyle::Space) - .with_line_width(line_width); + let settings = lsp_state.document_settings(¶ms.text_document.uri); + let format_options = settings.format.to_format_options(&doc.contents); if doc.parse.has_errors() { return Err(anyhow::anyhow!("Can't format when there are parse errors.")); } - let formatted = format_node(options.clone(), &doc.parse.syntax())?; + let formatted = format_node(format_options, &doc.parse.syntax())?; let output = formatted.print()?.into_code(); // Do we need to check that `doc` is indeed an R file? What about special @@ -47,18 +44,16 @@ pub(crate) fn document_formatting( #[tracing::instrument(level = "info", skip_all)] pub(crate) fn document_range_formatting( params: lsp_types::DocumentRangeFormattingParams, + lsp_state: &LspState, state: &WorldState, ) -> anyhow::Result>> { let doc = state.get_document(¶ms.text_document.uri)?; - let line_width = LineWidth::try_from(80).map_err(|err| anyhow::anyhow!("{err}"))?; let range = from_proto::text_range(&doc.line_index.index, params.range, doc.line_index.encoding)?; - // TODO: Handle FormattingOptions - let options = RFormatOptions::default() - .with_indent_style(IndentStyle::Space) - .with_line_width(line_width); + let settings = lsp_state.document_settings(¶ms.text_document.uri); + let format_options = settings.format.to_format_options(&doc.contents); let logical_lines = find_deepest_enclosing_logical_lines(doc.parse.syntax(), range); if logical_lines.is_empty() { @@ -96,7 +91,7 @@ pub(crate) fn document_range_formatting( let format_info = biome_formatter::format_sub_tree( root.syntax(), - air_r_formatter::RFormatLanguage::new(options), + air_r_formatter::RFormatLanguage::new(format_options), )?; if format_info.range().is_none() { diff --git a/crates/lsp/src/handlers_state.rs b/crates/lsp/src/handlers_state.rs index a570cf31..24b1bcdd 100644 --- a/crates/lsp/src/handlers_state.rs +++ b/crates/lsp/src/handlers_state.rs @@ -7,12 +7,15 @@ use anyhow::anyhow; use biome_lsp_converters::PositionEncoding; +use biome_lsp_converters::WideEncoding; use serde_json::Value; use struct_field_names_as_array::FieldNamesAsArray; use tower_lsp::lsp_types; use tower_lsp::lsp_types::ConfigurationItem; use tower_lsp::lsp_types::DidChangeConfigurationParams; use tower_lsp::lsp_types::DidChangeTextDocumentParams; +use tower_lsp::lsp_types::DidChangeWatchedFilesParams; +use tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams; use tower_lsp::lsp_types::DidCloseTextDocumentParams; use tower_lsp::lsp_types::DidOpenTextDocumentParams; use tower_lsp::lsp_types::FormattingOptions; @@ -27,17 +30,21 @@ use tower_lsp::lsp_types::WorkspaceFoldersServerCapabilities; use tower_lsp::lsp_types::WorkspaceServerCapabilities; use tracing::Instrument; use url::Url; +use workspace::settings::Settings; +use crate::capabilities::ResolvedClientCapabilities; use crate::config::indent_style_from_lsp; use crate::config::DocumentConfig; use crate::config::VscDiagnosticsConfig; use crate::config::VscDocumentConfig; use crate::documents::Document; +use crate::error::ErrorVec; use crate::logging; use crate::logging::LogMessageSender; use crate::main_loop::LspState; use crate::state::workspace_uris; use crate::state::WorldState; +use crate::workspaces::WorkspaceSettingsResolver; // Handlers that mutate the world state @@ -62,7 +69,6 @@ pub struct ConsoleInputs { pub(crate) fn initialize( params: InitializeParams, lsp_state: &mut LspState, - state: &mut WorldState, log_tx: LogMessageSender, ) -> anyhow::Result { // TODO: Get user specified options from `params.initialization_options` @@ -76,44 +82,30 @@ pub(crate) fn initialize( params.client_info.as_ref(), ); - // Defaults to UTF-16 - let mut position_encoding = None; - - if let Some(caps) = params.capabilities.general { - // If the client supports UTF-8 we use that, even if it's not its - // preferred encoding (at position 0). Otherwise we use the mandatory - // UTF-16 encoding that all clients and servers must support, even if - // the client would have preferred UTF-32. Note that VSCode and Positron - // only support UTF-16. - if let Some(caps) = caps.position_encodings { - if caps.contains(&lsp_types::PositionEncodingKind::UTF8) { - lsp_state.position_encoding = PositionEncoding::Utf8; - position_encoding = Some(lsp_types::PositionEncodingKind::UTF8); - } - } - } - - // Take note of supported capabilities so we can register them in the - // `Initialized` handler - if let Some(ws_caps) = params.capabilities.workspace { - if matches!(ws_caps.did_change_configuration, Some(caps) if matches!(caps.dynamic_registration, Some(true))) - { - lsp_state.needs_registration.did_change_configuration = true; - } - } + // Initialize the workspace settings resolver using the initial set of client provided `workspace_folders` + lsp_state.workspace_settings_resolver = WorkspaceSettingsResolver::from_workspace_folders( + params.workspace_folders.unwrap_or_default(), + Settings::default(), + ); - // Initialize the workspace folders - let mut folders: Vec = Vec::new(); - if let Some(workspace_folders) = params.workspace_folders { - for folder in workspace_folders.iter() { - state.workspace.folders.push(folder.uri.clone()); - if let Ok(path) = folder.uri.to_file_path() { - if let Some(path) = path.to_str() { - folders.push(path.to_string()); - } - } - } - } + lsp_state.capabilities = ResolvedClientCapabilities::new(params.capabilities); + + // If the client supports UTF-8 we use that, even if it's not its + // preferred encoding (at position 0). Otherwise we use the mandatory + // UTF-16 encoding that all clients and servers must support, even if + // the client would have preferred UTF-32. Note that VSCode and Positron + // only support UTF-16. + let position_encoding = if lsp_state + .capabilities + .position_encodings + .contains(&lsp_types::PositionEncodingKind::UTF8) + { + lsp_state.position_encoding = PositionEncoding::Utf8; + Some(lsp_types::PositionEncodingKind::UTF8) + } else { + lsp_state.position_encoding = PositionEncoding::Wide(WideEncoding::Utf16); + Some(lsp_types::PositionEncodingKind::UTF16) + }; Ok(InitializeResult { server_info: Some(ServerInfo { @@ -199,6 +191,36 @@ pub(crate) async fn did_change_configuration( .await } +pub(crate) fn did_change_workspace_folders( + params: DidChangeWorkspaceFoldersParams, + lsp_state: &mut LspState, +) -> anyhow::Result<()> { + // Collect all `errors` to ensure we don't drop events after a first error + let mut errors = ErrorVec::new(); + + for lsp_types::WorkspaceFolder { uri, .. } in params.event.added { + errors.push_err(lsp_state.open_workspace_folder(&uri, Settings::default())); + } + for lsp_types::WorkspaceFolder { uri, .. } in params.event.removed { + errors.push_err(lsp_state.close_workspace_folder(&uri)); + } + + errors.into_result() +} + +pub(crate) fn did_change_watched_files( + params: DidChangeWatchedFilesParams, + lsp_state: &mut LspState, +) -> anyhow::Result<()> { + for change in ¶ms.changes { + lsp_state + .workspace_settings_resolver + .reload_workspaces_matched_by_url(&change.uri); + } + + Ok(()) +} + #[tracing::instrument(level = "info", skip_all)] pub(crate) fn did_change_formatting_options( uri: &Url, diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index 65451b9f..adb21047 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -3,10 +3,12 @@ pub use tower_lsp::start_lsp; +pub mod capabilities; pub mod config; pub mod crates; pub mod documents; pub mod encoding; +pub mod error; pub mod from_proto; pub mod handlers; pub mod handlers_ext; @@ -18,6 +20,7 @@ pub mod rust_analyzer; pub mod state; pub mod to_proto; pub mod tower_lsp; +pub mod workspaces; #[cfg(test)] pub mod test_utils; diff --git a/crates/lsp/src/main_loop.rs b/crates/lsp/src/main_loop.rs index 780a635b..dd0037df 100644 --- a/crates/lsp/src/main_loop.rs +++ b/crates/lsp/src/main_loop.rs @@ -18,7 +18,10 @@ use tokio::task::JoinHandle; use tower_lsp::lsp_types::Diagnostic; use tower_lsp::Client; use url::Url; +use workspace::resolve::SettingsResolver; +use workspace::settings::Settings; +use crate::capabilities::ResolvedClientCapabilities; use crate::handlers; use crate::handlers_ext; use crate::handlers_format; @@ -31,6 +34,7 @@ use crate::tower_lsp::LspMessage; use crate::tower_lsp::LspNotification; use crate::tower_lsp::LspRequest; use crate::tower_lsp::LspResponse; +use crate::workspaces::WorkspaceSettingsResolver; pub(crate) type TokioUnboundedSender = tokio::sync::mpsc::UnboundedSender; pub(crate) type TokioUnboundedReceiver = tokio::sync::mpsc::UnboundedReceiver; @@ -148,6 +152,9 @@ pub(crate) struct GlobalState { /// Unlike `WorldState`, `ParserState` cannot be cloned and is only accessed by /// exclusive handlers. pub(crate) struct LspState { + /// Resolver to look up [`Settings`] given a document [`Url`] + pub(crate) workspace_settings_resolver: WorkspaceSettingsResolver, + /// The negociated encoding for document positions. Note that documents are /// always stored as UTF-8 in Rust Strings. This encoding is only used to /// translate UTF-16 positions sent by the client to UTF-8 ones. @@ -156,26 +163,42 @@ pub(crate) struct LspState { /// The set of tree-sitter document parsers managed by the `GlobalState`. pub(crate) parsers: HashMap, - /// List of capabilities for which we need to send a registration request - /// when we get the `Initialized` notification. - pub(crate) needs_registration: ClientCaps, - // Add handle to aux loop here? + /// List of client capabilities that we care about + pub(crate) capabilities: ResolvedClientCapabilities, } impl Default for LspState { fn default() -> Self { Self { + workspace_settings_resolver: WorkspaceSettingsResolver::default(), // Default encoding specified in the LSP protocol position_encoding: PositionEncoding::Wide(WideEncoding::Utf16), parsers: Default::default(), - needs_registration: Default::default(), + capabilities: ResolvedClientCapabilities::default(), } } } -#[derive(Debug, Default)] -pub(crate) struct ClientCaps { - pub(crate) did_change_configuration: bool, +impl LspState { + pub(crate) fn document_settings(&self, url: &Url) -> &Settings { + self.workspace_settings_resolver.settings_for_url(url) + } + + pub(crate) fn open_workspace_folder( + &mut self, + url: &Url, + fallback: Settings, + ) -> anyhow::Result<()> { + self.workspace_settings_resolver + .open_workspace_folder(url, fallback) + } + + pub(crate) fn close_workspace_folder( + &mut self, + url: &Url, + ) -> anyhow::Result> { + self.workspace_settings_resolver.close_workspace_folder(url) + } } enum LoopControl { @@ -300,14 +323,14 @@ impl GlobalState { LspNotification::Initialized(_params) => { handlers::handle_initialized(&self.client, &self.lsp_state).await?; }, - LspNotification::DidChangeWorkspaceFolders(_params) => { - // TODO: Restart indexer with new folders. + LspNotification::DidChangeWorkspaceFolders(params) => { + handlers_state::did_change_workspace_folders(params, &mut self.lsp_state)?; }, LspNotification::DidChangeConfiguration(params) => { handlers_state::did_change_configuration(params, &self.client, &mut self.world).await?; }, - LspNotification::DidChangeWatchedFiles(_params) => { - // TODO: Re-index the changed files. + LspNotification::DidChangeWatchedFiles(params) => { + handlers_state::did_change_watched_files(params, &mut self.lsp_state)?; }, LspNotification::DidOpenTextDocument(params) => { handlers_state::did_open(params, &self.lsp_state, &mut self.world)?; @@ -329,17 +352,17 @@ impl GlobalState { LspRequest::Initialize(params) => { // Unwrap: `Initialize` method should only be called once. let log_tx = self.log_tx.take().unwrap(); - respond(tx, handlers_state::initialize(params, &mut self.lsp_state, &mut self.world, log_tx), LspResponse::Initialize)?; + respond(tx, handlers_state::initialize(params, &mut self.lsp_state, log_tx), LspResponse::Initialize)?; }, LspRequest::Shutdown => { out = LoopControl::Shutdown; respond(tx, Ok(()), LspResponse::Shutdown)?; }, LspRequest::DocumentFormatting(params) => { - respond(tx, handlers_format::document_formatting(params, &self.world), LspResponse::DocumentFormatting)?; + respond(tx, handlers_format::document_formatting(params, &self.lsp_state, &self.world), LspResponse::DocumentFormatting)?; }, LspRequest::DocumentRangeFormatting(params) => { - respond(tx, handlers_format::document_range_formatting(params, &self.world), LspResponse::DocumentRangeFormatting)?; + respond(tx, handlers_format::document_range_formatting(params, &self.lsp_state, &self.world), LspResponse::DocumentRangeFormatting)?; }, LspRequest::AirViewFile(params) => { respond(tx, handlers_ext::view_file(params, &self.world), LspResponse::AirViewFile)?; diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap index cda77a86..478729cf 100644 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap @@ -4,5 +4,5 @@ expression: output --- 1+1 { - 2 + 2 + 2 + 2 } diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap index 776dab17..8c4b6082 100644 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap @@ -5,6 +5,6 @@ expression: output2 0+0 1 + 1 { - 2 + 2 + 2 + 2 } 3+3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap index 5d9e27d0..eccb6f4e 100644 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap @@ -5,6 +5,6 @@ expression: output3 0+0 1 + 1 { - 2 + 2 + 2 + 2 } 3 + 3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap index 4234f400..699c29fb 100644 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap @@ -5,6 +5,6 @@ expression: output4 0+0 1+1 { - 2 + 2 + 2 + 2 } 3 + 3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap index 33752478..6587090f 100644 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap +++ b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap @@ -5,6 +5,6 @@ expression: output1 0+0 1 + 1 { - 2 + 2 + 2 + 2 } 3+3 diff --git a/crates/lsp/src/state.rs b/crates/lsp/src/state.rs index 1bd38433..219a5165 100644 --- a/crates/lsp/src/state.rs +++ b/crates/lsp/src/state.rs @@ -14,9 +14,6 @@ pub(crate) struct WorldState { /// Watched documents pub(crate) documents: HashMap, - /// Watched folders - pub(crate) workspace: Workspace, - /// The scopes for the console. This currently contains a list (outer `Vec`) /// of names (inner `Vec`) within the environments on the search path, starting /// from the global environment and ending with the base package. Eventually @@ -46,11 +43,6 @@ pub(crate) struct WorldState { pub(crate) config: LspConfig, } -#[derive(Clone, Default, Debug)] -pub(crate) struct Workspace { - pub folders: Vec, -} - impl WorldState { pub(crate) fn get_document(&self, uri: &Url) -> anyhow::Result<&Document> { if let Some(doc) = self.documents.get(uri) { diff --git a/crates/lsp/src/tower_lsp.rs b/crates/lsp/src/tower_lsp.rs index ba114988..60de59d0 100644 --- a/crates/lsp/src/tower_lsp.rs +++ b/crates/lsp/src/tower_lsp.rs @@ -345,7 +345,7 @@ mod tests { text_document_sync, .. } => { - assert_eq!(position_encoding, None); + assert_eq!(position_encoding, Some(PositionEncodingKind::UTF16)); assert_eq!(text_document_sync, Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::INCREMENTAL))); }); diff --git a/crates/lsp/src/workspaces.rs b/crates/lsp/src/workspaces.rs new file mode 100644 index 00000000..1d64eae2 --- /dev/null +++ b/crates/lsp/src/workspaces.rs @@ -0,0 +1,174 @@ +use std::path::Path; +use std::path::PathBuf; + +use tower_lsp::lsp_types::WorkspaceFolder; +use url::Url; +use workspace::resolve::PathResolver; +use workspace::resolve::SettingsResolver; +use workspace::settings::Settings; + +/// Resolver for retrieving [`Settings`] associated with a workspace specific [`Path`] +#[derive(Debug, Default)] +pub(crate) struct WorkspaceSettingsResolver { + /// Resolves a `path` to the closest workspace specific `SettingsResolver`. + /// That `SettingsResolver` can then return `Settings` for the `path`. + path_to_settings_resolver: PathResolver, +} + +impl WorkspaceSettingsResolver { + /// Construct a new workspace settings resolver from an initial set of workspace folders + pub(crate) fn from_workspace_folders( + workspace_folders: Vec, + fallback: Settings, + ) -> Self { + let settings_resolver_fallback = SettingsResolver::new(fallback.clone()); + let path_to_settings_resolver = PathResolver::new(settings_resolver_fallback); + + let mut resolver = Self { + path_to_settings_resolver, + }; + + // Add each workspace folder's settings into the resolver. + // If we fail for any reason (i.e. parse failure of an `air.toml`) then + // we log an error and try to resolve the remaining workspace folders. We don't want + // to propagate an error here because we don't want to prevent the server from + // starting up entirely. + // TODO: This is one place it would be nice to show a toast notification back + // to the user, but we probably need to add support to the Aux thread for that? + for workspace_folder in workspace_folders { + if let Err(error) = + resolver.open_workspace_folder(&workspace_folder.uri, fallback.clone()) + { + tracing::error!( + "Failed to load workspace settings for '{uri}':\n{error}", + uri = workspace_folder.uri, + error = error + ); + } + } + + resolver + } + + pub(crate) fn open_workspace_folder( + &mut self, + url: &Url, + fallback: Settings, + ) -> anyhow::Result<()> { + let path = match Self::url_to_path(url)? { + Some(path) => path, + None => { + tracing::warn!("Ignoring non-file workspace URL: {url}"); + return Ok(()); + } + }; + + let mut settings_resolver = SettingsResolver::new(fallback); + settings_resolver.load_from_paths(&[&path])?; + + tracing::trace!("Adding workspace settings: {}", path.display()); + self.path_to_settings_resolver.add(&path, settings_resolver); + + Ok(()) + } + + pub(crate) fn close_workspace_folder( + &mut self, + url: &Url, + ) -> anyhow::Result> { + match Self::url_to_path(url)? { + Some(path) => { + tracing::trace!("Removing workspace settings: {}", path.display()); + Ok(self.path_to_settings_resolver.remove(&path)) + } + None => { + tracing::warn!("Ignoring non-file workspace URL: {url}"); + Ok(None) + } + } + } + + /// Return the appropriate [`Settings`] for a given document [`Url`]. + pub(crate) fn settings_for_url(&self, url: &Url) -> &Settings { + if let Ok(Some(path)) = Self::url_to_path(url) { + return self.settings_for_path(&path); + } + + // For `untitled` schemes, we have special behavior. + // If there is exactly 1 workspace, we resolve using a path of + // `{workspace_path}/untitled` to provide relevant settings for this workspace. + if url.scheme() == "untitled" && self.path_to_settings_resolver.len() == 1 { + tracing::trace!("Using workspace settings for 'untitled' URL: {url}"); + let workspace_path = self.path_to_settings_resolver.keys().next().unwrap(); + let path = workspace_path.join("untitled"); + return self.settings_for_path(&path); + } + + tracing::trace!("Using default settings for non-file URL: {url}"); + self.path_to_settings_resolver.fallback().fallback() + } + + /// Reloads all workspaces matched by the [`Url`] + /// + /// This is utilized by the watched files handler to reload the settings + /// resolver whenever an `air.toml` is modified. + pub(crate) fn reload_workspaces_matched_by_url(&mut self, url: &Url) { + let path = match Self::url_to_path(url) { + Ok(Some(path)) => path, + Ok(None) => { + tracing::trace!("Ignoring non-`file` changed URL: {url}"); + return; + } + Err(error) => { + tracing::error!("Failed to reload workspaces associated with {url}:\n{error}"); + return; + } + }; + + if !path.ends_with("air.toml") { + // We could get called with a changed file that isn't an `air.toml` if we are + // watching more than `air.toml` files + tracing::trace!("Ignoring non-`air.toml` changed URL: {url}"); + return; + } + + for (workspace_path, settings_resolver) in self.path_to_settings_resolver.matches_mut(&path) + { + tracing::trace!("Reloading workspace settings: {}", workspace_path.display()); + + settings_resolver.clear(); + + if let Err(error) = settings_resolver.load_from_paths(&[workspace_path]) { + tracing::error!( + "Failed to reload workspace settings for {path}:\n{error}", + path = workspace_path.display(), + error = error + ); + } + } + } + + /// Return the appropriate [`Settings`] for a given [`Path`]. + /// + /// This actually performs a double resolution. It first resolves to the + /// workspace specific `SettingsResolver` that matches this path, and then uses that + /// resolver to actually resolve the `Settings` for this path. We do it this way + /// to ensure we can easily add and remove workspaces (including all of their + /// hierarchical paths). + fn settings_for_path(&self, path: &Path) -> &Settings { + let settings_resolver = self.path_to_settings_resolver.resolve_or_fallback(path); + settings_resolver.resolve_or_fallback(path) + } + + fn url_to_path(url: &Url) -> anyhow::Result> { + if url.scheme() != "file" { + return Ok(None); + } + + let path = url + .to_file_path() + .map_err(|()| anyhow::anyhow!("Failed to convert workspace URL to file path: {url}"))?; + + Ok(Some(path)) + } +} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml new file mode 100644 index 00000000..888fe4d6 --- /dev/null +++ b/crates/workspace/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "workspace" +version = "0.1.0" +publish = false +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +air_r_formatter = { workspace = true } +biome_formatter = { workspace = true, features = ["serde"] } +fs = { workspace = true } +ignore = { workspace = true } +line_ending = { workspace = true } +rustc-hash = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true, features = ["derive"] } +toml = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +insta = { workspace = true } +tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs new file mode 100644 index 00000000..cb9835a0 --- /dev/null +++ b/crates/workspace/src/lib.rs @@ -0,0 +1,4 @@ +pub mod resolve; +pub mod settings; +pub mod toml; +pub mod toml_options; diff --git a/crates/workspace/src/resolve.rs b/crates/workspace/src/resolve.rs new file mode 100644 index 00000000..622e59b4 --- /dev/null +++ b/crates/workspace/src/resolve.rs @@ -0,0 +1,252 @@ +// --- source +// authors = ["Charlie Marsh"] +// license = "MIT" +// origin = "https://github.com/astral-sh/ruff/tree/main/crates/ruff_workspace" +// --- + +use std::collections::btree_map::Keys; +use std::collections::btree_map::Range; +use std::collections::btree_map::RangeMut; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use ignore::DirEntry; +use rustc_hash::FxHashSet; +use thiserror::Error; + +use crate::settings::Settings; +use crate::toml::find_air_toml_in_directory; +use crate::toml::parse_air_toml; +use crate::toml::ParseTomlError; + +/// Resolves a [`Path`] to its associated `T` +/// +/// To use a [`PathResolver`]: +/// - Load directories into it using [`PathResolver::add()`] +/// - Resolve a [`Path`] to its associated `T` with [`PathResolver::resolve()`] +/// +/// See [`PathResolver::resolve()`] for more details on the implementation. +#[derive(Debug, Default)] +pub struct PathResolver { + /// Fallback value to be used when a `path` isn't associated with anything in the `map` + fallback: T, + + /// An ordered `BTreeMap` from a `path` (normally, a directory) to a `T` + map: BTreeMap, +} + +impl PathResolver { + /// Create a new empty [`PathResolver`] + pub fn new(fallback: T) -> Self { + Self { + fallback, + map: BTreeMap::new(), + } + } + + pub fn fallback(&self) -> &T { + &self.fallback + } + + pub fn add(&mut self, path: &Path, value: T) -> Option { + self.map.insert(path.to_path_buf(), value) + } + + pub fn remove(&mut self, path: &Path) -> Option { + self.map.remove(path) + } + + pub fn len(&self) -> usize { + self.map.len() + } + + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + pub fn keys(&self) -> Keys<'_, PathBuf, T> { + self.map.keys() + } + + pub fn clear(&mut self) { + self.map.clear(); + } + + /// Resolve a [`Path`] to its associated `T` + /// + /// This resolver works by finding the closest directory to the `path` to search for. + /// + /// The [`BTreeMap`] is an ordered map, so if you do: + /// + /// ```text + /// resolver.add("a/b", value1) + /// resolver.add("a/b/c", value2) + /// resolver.add("a/b/d", value3) + /// resolver.resolve("a/b/c/test.R") + /// ``` + /// + /// Then it detects both `"a/b"` and `"a/b/c"` as being "less than" the path of + /// `"a/b/c/test.R"`, and then chooses `"a/b/c"` because it is at the back of + /// that returned sorted list (i.e. the "closest" match). + pub fn resolve(&self, path: &Path) -> Option<&T> { + self.resolve_entry(path).map(|(_, value)| value) + } + + /// Same as `resolve()`, but returns the internal `fallback` if no associated value + /// is found. + pub fn resolve_or_fallback(&self, path: &Path) -> &T { + self.resolve(path).unwrap_or(self.fallback()) + } + + /// Same as `resolve()`, but returns the `(key, value)` pair. + /// + /// Useful when you need the matched workspace path + pub fn resolve_entry(&self, path: &Path) -> Option<(&PathBuf, &T)> { + self.matches(path).next_back() + } + + /// Returns all matches matched by the `path` rather than just the closest one + pub fn matches(&self, path: &Path) -> Range<'_, PathBuf, T> { + self.map.range(..path.to_path_buf()) + } + + /// Returns all matches matched by the `path` rather than just the closest one + pub fn matches_mut(&mut self, path: &Path) -> RangeMut<'_, PathBuf, T> { + self.map.range_mut(..path.to_path_buf()) + } +} + +pub type SettingsResolver = PathResolver; + +#[derive(Debug, Error)] +pub enum SettingsResolverError { + #[error(transparent)] + ParseToml(#[from] ParseTomlError), +} + +impl SettingsResolver { + /// This is the core function for walking a set of `paths` looking for `air.toml`s + /// and loading in any directories it finds + /// + /// For each `path`, we: + /// - Walk up its ancestors, looking for an `air.toml` + /// - TODO(hierarchical): Walk down its children, looking for nested `air.toml`s + /// + /// Whenever we find an `air.toml`, we add the directory it was found in and + /// the parsed [`Settings`] into the resolver. + pub fn load_from_paths>( + &mut self, + paths: &[P], + ) -> Result<(), SettingsResolverError> { + let paths: Vec = paths.iter().map(fs::normalize_path).collect(); + + let mut seen = FxHashSet::default(); + + // Load the `resolver` with `Settings` associated with each `path` + for path in &paths { + for ancestor in path.ancestors() { + if seen.insert(ancestor) { + if let Some(toml) = find_air_toml_in_directory(ancestor) { + let settings = Self::parse_settings(&toml)?; + self.add(ancestor, settings); + break; + } + } else { + // We already visited this ancestor, we can stop here. + break; + } + } + } + + // TODO(hierarchical): Also iterate through the directories and collect `air.toml` + // found nested withing the directories for hierarchical support + + Ok(()) + } + + /// Parse [Settings] from a given `air.toml` + // TODO(hierarchical): Allow for an `extends` option in `air.toml`, which will make things + // more complex, but will be very useful once we support hierarchical configuration as a + // way of "inheriting" most top level configuration while slightly tweaking it in a nested directory. + fn parse_settings(toml: &Path) -> Result { + let options = parse_air_toml(toml)?; + let settings = options.into_settings(); + Ok(settings) + } +} + +/// For each provided `path`, recursively search for any R files within that `path` +/// that match our inclusion criteria +/// +/// NOTE: Make sure that the inclusion criteria that guide `path` discovery are also +/// consistently applied to [SettingsResolver::load_from_paths()]. +pub fn discover_r_file_paths>(paths: &[P]) -> Vec> { + let paths: Vec = paths.iter().map(fs::normalize_path).collect(); + + let Some((first_path, paths)) = paths.split_first() else { + // No paths provided + return Vec::new(); + }; + + // TODO: Parallel directory visitor + let mut builder = ignore::WalkBuilder::new(first_path); + + for path in paths { + builder.add(path); + } + + // TODO: Make these configurable options (possibly just one?) + // Right now we explicitly call them even though they are `true` by default + // to remind us to expose them. + // + // "This toggles, as a group, all the filters that are enabled by default" + // builder.standard_filters(true) + builder.hidden(true); + builder.parents(true); + builder.ignore(false); + builder.git_ignore(true); + builder.git_global(true); + builder.git_exclude(true); + + let mut paths = Vec::new(); + + // Walk all `paths` recursively, collecting R files that we can format + for path in builder.build() { + match path { + Ok(entry) => { + if let Some(path) = is_match(entry) { + paths.push(Ok(path)); + } + } + Err(err) => { + paths.push(Err(err)); + } + } + } + + paths +} + +// Decide whether or not to accept an `entry` based on include/exclude rules. +fn is_match(entry: DirEntry) -> Option { + // Ignore directories + if entry.file_type().map_or(true, |ft| ft.is_dir()) { + return None; + } + + // Accept all files that are passed-in directly, even non-R files + if entry.depth() == 0 { + let path = entry.into_path(); + return Some(path); + } + + // Otherwise check if we should accept this entry + // TODO: Many other checks based on user exclude/includes + let path = entry.into_path(); + + if !fs::has_r_extension(&path) { + return None; + } + + Some(path) +} diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs new file mode 100644 index 00000000..dec96df6 --- /dev/null +++ b/crates/workspace/src/settings.rs @@ -0,0 +1,60 @@ +mod indent_style; +mod indent_width; +// TODO: Can we pick a better crate name for `line_ending` so these don't collide? +#[path = "settings/line_ending.rs"] +mod line_ending_setting; +mod line_length; +mod magic_line_break; + +pub use indent_style::*; +pub use indent_width::*; +pub use line_ending_setting::*; +pub use line_length::*; +pub use magic_line_break::*; + +use air_r_formatter::context::RFormatOptions; +use line_ending; + +/// Resolved configuration settings used within air +/// +/// May still require a source document to finalize some options, such as +/// `LineEnding::Auto` in the formatter. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct Settings { + /// Settings to configure code formatting. + pub format: FormatSettings, +} + +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct FormatSettings { + pub indent_style: IndentStyle, + pub indent_width: IndentWidth, + pub line_ending: LineEnding, + pub line_length: LineLength, + pub magic_line_break: MagicLineBreak, +} + +impl FormatSettings { + // Finalize `RFormatOptions` in preparation for a formatting operation on `source` + pub fn to_format_options(&self, source: &str) -> RFormatOptions { + let line_ending = match self.line_ending { + LineEnding::Lf => biome_formatter::LineEnding::Lf, + LineEnding::Crlf => biome_formatter::LineEnding::Crlf, + #[cfg(target_os = "windows")] + LineEnding::Native => biome_formatter::LineEnding::Crlf, + #[cfg(not(target_os = "windows"))] + LineEnding::Native => biome_formatter::LineEnding::Lf, + LineEnding::Auto => match line_ending::infer(source) { + line_ending::LineEnding::Lf => biome_formatter::LineEnding::Lf, + line_ending::LineEnding::Crlf => biome_formatter::LineEnding::Crlf, + }, + }; + + RFormatOptions::new() + .with_indent_style(self.indent_style.into()) + .with_indent_width(self.indent_width.into()) + .with_line_ending(line_ending) + .with_line_width(self.line_length.into()) + .with_magic_line_break(self.magic_line_break.into()) + } +} diff --git a/crates/workspace/src/settings/indent_style.rs b/crates/workspace/src/settings/indent_style.rs new file mode 100644 index 00000000..f84d94da --- /dev/null +++ b/crates/workspace/src/settings/indent_style.rs @@ -0,0 +1,54 @@ +use std::fmt::Display; +use std::str::FromStr; + +#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum IndentStyle { + /// Tab + #[default] + Tab, + /// Space + Space, +} + +impl IndentStyle { + /// Returns `true` if this is an [IndentStyle::Tab]. + pub const fn is_tab(&self) -> bool { + matches!(self, IndentStyle::Tab) + } + + /// Returns `true` if this is an [IndentStyle::Space]. + pub const fn is_space(&self) -> bool { + matches!(self, IndentStyle::Space) + } +} + +impl FromStr for IndentStyle { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "tab" => Ok(Self::Tab), + "space" => Ok(Self::Space), + _ => Err("Unsupported value for this option"), + } + } +} + +impl Display for IndentStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IndentStyle::Tab => std::write!(f, "Tab"), + IndentStyle::Space => std::write!(f, "Space"), + } + } +} + +impl From for biome_formatter::IndentStyle { + fn from(value: IndentStyle) -> Self { + match value { + IndentStyle::Tab => biome_formatter::IndentStyle::Tab, + IndentStyle::Space => biome_formatter::IndentStyle::Space, + } + } +} diff --git a/crates/workspace/src/settings/indent_width.rs b/crates/workspace/src/settings/indent_width.rs new file mode 100644 index 00000000..f9717faa --- /dev/null +++ b/crates/workspace/src/settings/indent_width.rs @@ -0,0 +1,148 @@ +use std::fmt; +use std::num::NonZeroU8; + +/// Validated value for the `indent-width` formatter options +/// +/// The allowed range of values is 1..=24 +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +pub struct IndentWidth(NonZeroU8); + +impl IndentWidth { + /// Maximum allowed value for a valid [IndentWidth] + const MAX: u8 = 24; + + /// Return the numeric value for this [IndentWidth] + pub fn value(&self) -> u8 { + self.0.get() + } +} + +impl Default for IndentWidth { + fn default() -> Self { + Self(NonZeroU8::new(4).unwrap()) + } +} + +impl std::fmt::Debug for IndentWidth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + std::fmt::Debug::fmt(&self.0, f) + } +} + +impl std::fmt::Display for IndentWidth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} + +impl<'de> serde::Deserialize<'de> for IndentWidth { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value: u8 = serde::Deserialize::deserialize(deserializer)?; + let indent_width = IndentWidth::try_from(value).map_err(serde::de::Error::custom)?; + Ok(indent_width) + } +} + +/// Error type returned when converting a u8 or NonZeroU8 to a [`IndentWidth`] fails +#[derive(Clone, Copy, Debug)] +pub struct IndentWidthFromIntError(u8); + +impl std::error::Error for IndentWidthFromIntError {} + +impl TryFrom for IndentWidth { + type Error = IndentWidthFromIntError; + + fn try_from(value: u8) -> Result { + match NonZeroU8::try_from(value) { + Ok(value) => IndentWidth::try_from(value), + Err(_) => Err(IndentWidthFromIntError(value)), + } + } +} + +impl TryFrom for IndentWidth { + type Error = IndentWidthFromIntError; + + fn try_from(value: NonZeroU8) -> Result { + if value.get() <= Self::MAX { + Ok(IndentWidth(value)) + } else { + Err(IndentWidthFromIntError(value.get())) + } + } +} + +impl std::fmt::Display for IndentWidthFromIntError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "The indent width must be a value between 1 and {max}, not {value}.", + max = IndentWidth::MAX, + value = self.0 + ) + } +} + +impl From for u8 { + fn from(value: IndentWidth) -> Self { + value.0.get() + } +} + +impl From for NonZeroU8 { + fn from(value: IndentWidth) -> Self { + value.0 + } +} + +impl From for biome_formatter::IndentWidth { + fn from(value: IndentWidth) -> Self { + // Unwrap: We assert that we match biome's `IndentWidth` perfectly + biome_formatter::IndentWidth::try_from(value.value()).unwrap() + } +} + +#[cfg(test)] +mod tests { + use anyhow::Context; + use anyhow::Result; + + use crate::settings::IndentWidth; + + #[derive(serde::Deserialize)] + #[serde(deny_unknown_fields, rename_all = "kebab-case")] + struct Options { + indent_width: Option, + } + + #[test] + fn deserialize_indent_width() -> Result<()> { + let options: Options = toml::from_str( + r" +indent-width = 2 +", + )?; + + assert_eq!( + options.indent_width, + Some(IndentWidth::try_from(2).unwrap()) + ); + + Ok(()) + } + + #[test] + fn deserialize_oob_indent_width() -> Result<()> { + let result: std::result::Result = toml::from_str( + r" +indent-width = 25 +", + ); + let error = result.err().context("Expected OOB `IndentWidth` error")?; + insta::assert_snapshot!(error); + Ok(()) + } +} diff --git a/crates/workspace/src/settings/line_ending.rs b/crates/workspace/src/settings/line_ending.rs new file mode 100644 index 00000000..b2bcf870 --- /dev/null +++ b/crates/workspace/src/settings/line_ending.rs @@ -0,0 +1,31 @@ +use std::fmt; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum LineEnding { + /// The newline style is detected automatically on a file per file basis. + /// Files with mixed line endings will be converted to the first detected line ending. + /// Defaults to [`LineEnding::Lf`] for a files that contain no line endings. + #[default] + Auto, + + /// Line endings will be converted to `\n` as is common on Unix. + Lf, + + /// Line endings will be converted to `\r\n` as is common on Windows. + Crlf, + + /// Line endings will be converted to `\n` on Unix and `\r\n` on Windows. + Native, +} + +impl fmt::Display for LineEnding { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Auto => write!(f, "auto"), + Self::Lf => write!(f, "lf"), + Self::Crlf => write!(f, "crlf"), + Self::Native => write!(f, "native"), + } + } +} diff --git a/crates/workspace/src/settings/line_length.rs b/crates/workspace/src/settings/line_length.rs new file mode 100644 index 00000000..934ef64d --- /dev/null +++ b/crates/workspace/src/settings/line_length.rs @@ -0,0 +1,145 @@ +use std::fmt; +use std::num::NonZeroU16; + +/// Validated value for the `line-length` formatter options +/// +/// The allowed range of values is 1..=320 +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct LineLength(NonZeroU16); + +impl LineLength { + /// Maximum allowed value for a valid [LineLength] + const MAX: u16 = 320; + + /// Return the numeric value for this [LineLength] + pub fn value(&self) -> u16 { + self.0.get() + } +} + +impl Default for LineLength { + fn default() -> Self { + Self(NonZeroU16::new(80).unwrap()) + } +} + +impl std::fmt::Debug for LineLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + std::fmt::Debug::fmt(&self.0, f) + } +} + +impl fmt::Display for LineLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl<'de> serde::Deserialize<'de> for LineLength { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value: u16 = serde::Deserialize::deserialize(deserializer)?; + let line_length = LineLength::try_from(value).map_err(serde::de::Error::custom)?; + Ok(line_length) + } +} + +/// Error type returned when converting a u16 or NonZeroU16 to a [`LineLength`] fails +#[derive(Clone, Copy, Debug)] +pub struct LineLengthFromIntError(u16); + +impl std::error::Error for LineLengthFromIntError {} + +impl TryFrom for LineLength { + type Error = LineLengthFromIntError; + + fn try_from(value: u16) -> Result { + match NonZeroU16::try_from(value) { + Ok(value) => LineLength::try_from(value), + Err(_) => Err(LineLengthFromIntError(value)), + } + } +} + +impl TryFrom for LineLength { + type Error = LineLengthFromIntError; + + fn try_from(value: NonZeroU16) -> Result { + if value.get() <= Self::MAX { + Ok(LineLength(value)) + } else { + Err(LineLengthFromIntError(value.get())) + } + } +} + +impl std::fmt::Display for LineLengthFromIntError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "The line length must be a value between 1 and {max}, not {value}.", + max = LineLength::MAX, + value = self.0 + ) + } +} + +impl From for u16 { + fn from(value: LineLength) -> Self { + value.0.get() + } +} + +impl From for NonZeroU16 { + fn from(value: LineLength) -> Self { + value.0 + } +} + +impl From for biome_formatter::LineWidth { + fn from(value: LineLength) -> Self { + // Unwrap: We assert that we match biome's `LineWidth` perfectly + biome_formatter::LineWidth::try_from(value.value()).unwrap() + } +} + +#[cfg(test)] +mod tests { + use anyhow::Context; + use anyhow::Result; + + use crate::settings::LineLength; + + #[derive(serde::Deserialize)] + #[serde(deny_unknown_fields, rename_all = "kebab-case")] + struct Options { + line_length: Option, + } + + #[test] + fn deserialize_line_length() -> Result<()> { + let options: Options = toml::from_str( + r" +line-length = 50 +", + )?; + + assert_eq!(options.line_length, Some(LineLength::try_from(50).unwrap())); + + Ok(()) + } + + #[test] + fn deserialize_oob_line_length() -> Result<()> { + let result: std::result::Result = toml::from_str( + r" +line-length = 400 +", + ); + let error = result.err().context("Expected OOB `LineLength` error")?; + insta::assert_snapshot!(error); + Ok(()) + } +} diff --git a/crates/workspace/src/settings/magic_line_break.rs b/crates/workspace/src/settings/magic_line_break.rs new file mode 100644 index 00000000..e6d26d27 --- /dev/null +++ b/crates/workspace/src/settings/magic_line_break.rs @@ -0,0 +1,53 @@ +use std::fmt::Display; +use std::str::FromStr; + +#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)] +pub enum MagicLineBreak { + /// Respect + #[default] + Respect, + /// Ignore + Ignore, +} + +impl MagicLineBreak { + /// Returns `true` if magic line breaks should be respected. + pub const fn is_respect(&self) -> bool { + matches!(self, MagicLineBreak::Respect) + } + + /// Returns `true` if magic line breaks should be ignored. + pub const fn is_ignore(&self) -> bool { + matches!(self, MagicLineBreak::Ignore) + } +} + +impl FromStr for MagicLineBreak { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "respect" => Ok(Self::Respect), + "ignore" => Ok(Self::Ignore), + _ => Err("Unsupported value for this option"), + } + } +} + +impl Display for MagicLineBreak { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MagicLineBreak::Respect => std::write!(f, "Respect"), + MagicLineBreak::Ignore => std::write!(f, "Ignore"), + } + } +} + +impl From for air_r_formatter::options::MagicLineBreak { + fn from(value: MagicLineBreak) -> Self { + match value { + MagicLineBreak::Respect => air_r_formatter::options::MagicLineBreak::Respect, + MagicLineBreak::Ignore => air_r_formatter::options::MagicLineBreak::Ignore, + } + } +} diff --git a/crates/workspace/src/settings/snapshots/workspace__settings__indent_width__tests__deserialize_oob_indent_width.snap b/crates/workspace/src/settings/snapshots/workspace__settings__indent_width__tests__deserialize_oob_indent_width.snap new file mode 100644 index 00000000..dad86226 --- /dev/null +++ b/crates/workspace/src/settings/snapshots/workspace__settings__indent_width__tests__deserialize_oob_indent_width.snap @@ -0,0 +1,9 @@ +--- +source: crates/workspace/src/settings/indent_width.rs +expression: error +--- +TOML parse error at line 2, column 16 + | +2 | indent-width = 25 + | ^^ +The indent width must be a value between 1 and 24, not 25. diff --git a/crates/workspace/src/settings/snapshots/workspace__settings__line_length__tests__deserialize_oob_line_length.snap b/crates/workspace/src/settings/snapshots/workspace__settings__line_length__tests__deserialize_oob_line_length.snap new file mode 100644 index 00000000..9570385c --- /dev/null +++ b/crates/workspace/src/settings/snapshots/workspace__settings__line_length__tests__deserialize_oob_line_length.snap @@ -0,0 +1,9 @@ +--- +source: crates/workspace/src/settings/line_length.rs +expression: error +--- +TOML parse error at line 2, column 15 + | +2 | line-length = 400 + | ^^^ +The line length must be a value between 1 and 320, not 400. diff --git a/crates/workspace/src/toml.rs b/crates/workspace/src/toml.rs new file mode 100644 index 00000000..98ffb238 --- /dev/null +++ b/crates/workspace/src/toml.rs @@ -0,0 +1,117 @@ +//! Utilities for locating (and extracting configuration from) an air.toml. + +use crate::toml_options::TomlOptions; +use std::fmt::Display; +use std::fmt::Formatter; +use std::io; +use std::path::{Path, PathBuf}; + +/// Parse an `air.toml` file. +pub fn parse_air_toml>(path: P) -> Result { + let contents = std::fs::read_to_string(path.as_ref()) + .map_err(|err| ParseTomlError::Read(path.as_ref().to_path_buf(), err))?; + + toml::from_str(&contents) + .map_err(|err| ParseTomlError::Deserialize(path.as_ref().to_path_buf(), err)) +} + +#[derive(Debug)] +pub enum ParseTomlError { + Read(PathBuf, io::Error), + Deserialize(PathBuf, toml::de::Error), +} + +impl std::error::Error for ParseTomlError {} + +impl Display for ParseTomlError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Read(path, err) => { + write!( + f, + "Failed to read {path}:\n{err}", + path = fs::relativize_path(path), + ) + } + Self::Deserialize(path, err) => { + write!( + f, + "Failed to parse {path}:\n{err}", + path = fs::relativize_path(path), + ) + } + } + } +} + +/// Return the path to the `air.toml` file in a given directory. +pub fn find_air_toml_in_directory>(path: P) -> Option { + // Check for `air.toml`. + let toml = path.as_ref().join("air.toml"); + + if toml.is_file() { + Some(toml) + } else { + None + } +} + +/// Find the path to the closest `air.toml` if one exists, walking up the filesystem +pub fn find_air_toml>(path: P) -> Option { + for directory in path.as_ref().ancestors() { + if let Some(toml) = find_air_toml_in_directory(directory) { + return Some(toml); + } + } + None +} + +#[cfg(test)] +mod tests { + use anyhow::{Context, Result}; + use std::fs; + use tempfile::TempDir; + + use crate::settings::LineEnding; + use crate::toml::find_air_toml; + use crate::toml::parse_air_toml; + use crate::toml_options::TomlOptions; + + #[test] + + fn deserialize_empty() -> Result<()> { + let options: TomlOptions = toml::from_str(r"")?; + assert_eq!(options.global.indent_width, None); + assert_eq!(options.global.line_length, None); + assert_eq!(options.format, None); + Ok(()) + } + + #[test] + fn find_and_parse_air_toml() -> Result<()> { + let tempdir = TempDir::new()?; + let toml = tempdir.path().join("air.toml"); + fs::write( + toml, + r#" +line-length = 88 + +[format] +line-ending = "auto" +"#, + )?; + + let toml = find_air_toml(tempdir.path()).context("Failed to find air.toml")?; + let options = parse_air_toml(toml)?; + + let line_ending = options + .format + .context("Expected to find [format] table")? + .line_ending + .context("Expected to find `line-ending` field")?; + + assert_eq!(line_ending, LineEnding::Auto); + + Ok(()) + } +} diff --git a/crates/workspace/src/toml_options.rs b/crates/workspace/src/toml_options.rs new file mode 100644 index 00000000..fb397d6a --- /dev/null +++ b/crates/workspace/src/toml_options.rs @@ -0,0 +1,121 @@ +use crate::settings::FormatSettings; +use crate::settings::IndentStyle; +use crate::settings::IndentWidth; +use crate::settings::LineEnding; +use crate::settings::LineLength; +use crate::settings::MagicLineBreak; +use crate::settings::Settings; + +/// The Rust representation of `air.toml` +/// +/// The names and types of the fields in this struct determine the names and types +/// that can be specified in the `air.toml`. +/// +/// Every field is optional at this point, nothing is "finalized". +/// Finalization is done in [TomlOptions::into_settings]. +/// +/// Global options are specified at top level in the TOML file. +/// All other options are nested within their own `[table]`. +#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct TomlOptions { + /// Global options affecting multiple commands. + #[serde(flatten)] + pub global: GlobalTomlOptions, + + /// Options to configure code formatting. + pub format: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct GlobalTomlOptions { + /// The line length at which the formatter prefers to wrap lines. + /// + /// The value must be greater than or equal to `1` and less than or equal to `320`. + /// + /// Note: While the formatter will attempt to format lines such that they remain + /// within the `line-length`, it isn't a hard upper bound, and formatted lines may + /// exceed the `line-length`. + pub line_length: Option, + + /// The number of spaces per indentation level (tab). + /// + /// The value must be greater than or equal to `1` and less than or equal to `24`. + /// + /// Used by the formatter to determine the visual width of a tab. + /// + /// This option changes the number of spaces the formatter inserts when + /// using `indent-style = "space"`. It also represents the width of a tab when + /// `indent-style = "tab"` for the purposes of computing the `line-length`. + pub indent_width: Option, +} + +/// Configures the way air formats your code. +#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +pub struct FormatTomlOptions { + /// Whether to use spaces or tabs for indentation. + /// + /// `indent-style = "tab"` (default): + /// + /// ```r + /// fn <- function() { + /// cat("Hello") # A tab `\t` indents the `cat()` call. + /// } + /// ``` + /// + /// `indent-style = "space"`: + /// + /// ```r + /// fn <- function() { + /// cat("Hello") # Spaces indent the `cat()` call. + /// } + /// ``` + /// + /// We recommend you use tabs for accessibility. + /// + /// See `indent-width` to configure the number of spaces per indentation and the tab width. + pub indent_style: Option, + + /// The character air uses at the end of a line. + /// + /// * `auto`: The newline style is detected automatically on a file per file basis. Files with mixed line endings will be converted to the first detected line ending. Defaults to `\n` for files that contain no line endings. + /// * `lf`: Line endings will be converted to `\n`. The default line ending on Unix. + /// * `cr-lf`: Line endings will be converted to `\r\n`. The default line ending on Windows. + /// * `native`: Line endings will be converted to `\n` on Unix and `\r\n` on Windows. + pub line_ending: Option, + + /// Air respects a small set of magic line breaks as an indication that certain + /// function calls or function signatures should be left expanded. If this option + /// is set to `true`, magic line breaks are ignored. + /// + /// It may be preferable to ignore magic line breaks if you prefer that `line-length` + /// should be the only value that influences line breaks. + pub ignore_magic_line_break: Option, +} + +impl TomlOptions { + pub fn into_settings(self) -> Settings { + let format = self.format.unwrap_or_default(); + + let format = FormatSettings { + indent_style: format.indent_style.unwrap_or_default(), + indent_width: self.global.indent_width.unwrap_or_default(), + line_ending: format.line_ending.unwrap_or_default(), + line_length: self.global.line_length.unwrap_or_default(), + magic_line_break: match format.ignore_magic_line_break { + Some(ignore_magic_line_break) => { + if ignore_magic_line_break { + MagicLineBreak::Ignore + } else { + MagicLineBreak::Respect + } + } + None => MagicLineBreak::Respect, + }, + }; + + Settings { format } + } +} diff --git a/editors/code/src/lsp.ts b/editors/code/src/lsp.ts index f37fc43c..3c93faef 100644 --- a/editors/code/src/lsp.ts +++ b/editors/code/src/lsp.ts @@ -65,11 +65,6 @@ export class Lsp { { language: "r", pattern: "**/*.{r,R}" }, { language: "r", pattern: "**/*.{rprofile,Rprofile}" }, ], - synchronize: { - // Notify the server about file changes to R files contained in the workspace - fileEvents: - vscode.workspace.createFileSystemWatcher("**/*.[Rr]"), - }, outputChannel: this.channel, }; From 2a8b8918bc6c58e34a0a10337454beb609dbb00c Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Tue, 24 Dec 2024 10:08:12 -0500 Subject: [PATCH 02/44] Temp --- Cargo.lock | 107 ++- Cargo.toml | 11 + crates/air/Cargo.toml | 2 +- crates/air/src/commands/language_server.rs | 18 +- crates/ruff_server/Cargo.toml | 45 ++ crates/ruff_server/build.rs | 34 + crates/ruff_server/src/crates.rs | 3 + crates/ruff_server/src/edit.rs | 83 ++ crates/ruff_server/src/edit/range.rs | 171 +++++ crates/ruff_server/src/edit/replacement.rs | 234 ++++++ crates/ruff_server/src/edit/text_document.rs | 229 ++++++ crates/ruff_server/src/format.rs | 48 ++ crates/ruff_server/src/lib.rs | 20 + crates/ruff_server/src/logging.rs | 288 +++++++ crates/ruff_server/src/message.rs | 62 ++ crates/ruff_server/src/server.rs | 274 +++++++ crates/ruff_server/src/server/api.rs | 253 +++++++ .../src/server/api/notifications.rs | 16 + .../src/server/api/notifications/cancel.rs | 29 + .../server/api/notifications/did_change.rs | 43 ++ .../notifications/did_change_configuration.rs | 29 + .../notifications/did_change_watched_files.rs | 32 + .../api/notifications/did_change_workspace.rs | 39 + .../src/server/api/notifications/did_close.rs | 35 + .../src/server/api/notifications/did_open.rs | 41 + crates/ruff_server/src/server/api/requests.rs | 11 + .../src/server/api/requests/format.rs | 75 ++ .../src/server/api/requests/format_range.rs | 71 ++ crates/ruff_server/src/server/api/traits.rs | 84 +++ crates/ruff_server/src/server/client.rs | 175 +++++ crates/ruff_server/src/server/connection.rs | 150 ++++ crates/ruff_server/src/server/schedule.rs | 118 +++ .../ruff_server/src/server/schedule/task.rs | 103 +++ .../ruff_server/src/server/schedule/thread.rs | 109 +++ .../src/server/schedule/thread/pool.rs | 113 +++ .../src/server/schedule/thread/priority.rs | 297 ++++++++ crates/ruff_server/src/session.rs | 152 ++++ .../ruff_server/src/session/capabilities.rs | 41 + crates/ruff_server/src/session/index.rs | 275 +++++++ crates/ruff_server/src/session/workspaces.rs | 181 +++++ crates/ruff_source_file/Cargo.toml | 26 + crates/ruff_source_file/src/lib.rs | 278 +++++++ crates/ruff_source_file/src/line_index.rs | 714 ++++++++++++++++++ crates/ruff_source_file/src/line_ranges.rs | 396 ++++++++++ crates/ruff_source_file/src/newlines.rs | 457 +++++++++++ crates/ruff_text_size/Cargo.toml | 25 + crates/ruff_text_size/src/lib.rs | 36 + crates/ruff_text_size/src/range.rs | 544 +++++++++++++ crates/ruff_text_size/src/schemars_impls.rs | 33 + crates/ruff_text_size/src/serde_impls.rs | 47 ++ crates/ruff_text_size/src/size.rs | 196 +++++ crates/ruff_text_size/src/traits.rs | 99 +++ crates/ruff_text_size/tests/auto_traits.rs | 18 + crates/ruff_text_size/tests/constructors.rs | 24 + crates/ruff_text_size/tests/indexing.rs | 8 + crates/ruff_text_size/tests/main.rs | 79 ++ crates/ruff_text_size/tests/serde.rs | 83 ++ 57 files changed, 7151 insertions(+), 13 deletions(-) create mode 100644 crates/ruff_server/Cargo.toml create mode 100644 crates/ruff_server/build.rs create mode 100644 crates/ruff_server/src/crates.rs create mode 100644 crates/ruff_server/src/edit.rs create mode 100644 crates/ruff_server/src/edit/range.rs create mode 100644 crates/ruff_server/src/edit/replacement.rs create mode 100644 crates/ruff_server/src/edit/text_document.rs create mode 100644 crates/ruff_server/src/format.rs create mode 100644 crates/ruff_server/src/lib.rs create mode 100644 crates/ruff_server/src/logging.rs create mode 100644 crates/ruff_server/src/message.rs create mode 100644 crates/ruff_server/src/server.rs create mode 100644 crates/ruff_server/src/server/api.rs create mode 100644 crates/ruff_server/src/server/api/notifications.rs create mode 100644 crates/ruff_server/src/server/api/notifications/cancel.rs create mode 100644 crates/ruff_server/src/server/api/notifications/did_change.rs create mode 100644 crates/ruff_server/src/server/api/notifications/did_change_configuration.rs create mode 100644 crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs create mode 100644 crates/ruff_server/src/server/api/notifications/did_change_workspace.rs create mode 100644 crates/ruff_server/src/server/api/notifications/did_close.rs create mode 100644 crates/ruff_server/src/server/api/notifications/did_open.rs create mode 100644 crates/ruff_server/src/server/api/requests.rs create mode 100644 crates/ruff_server/src/server/api/requests/format.rs create mode 100644 crates/ruff_server/src/server/api/requests/format_range.rs create mode 100644 crates/ruff_server/src/server/api/traits.rs create mode 100644 crates/ruff_server/src/server/client.rs create mode 100644 crates/ruff_server/src/server/connection.rs create mode 100644 crates/ruff_server/src/server/schedule.rs create mode 100644 crates/ruff_server/src/server/schedule/task.rs create mode 100644 crates/ruff_server/src/server/schedule/thread.rs create mode 100644 crates/ruff_server/src/server/schedule/thread/pool.rs create mode 100644 crates/ruff_server/src/server/schedule/thread/priority.rs create mode 100644 crates/ruff_server/src/session.rs create mode 100644 crates/ruff_server/src/session/capabilities.rs create mode 100644 crates/ruff_server/src/session/index.rs create mode 100644 crates/ruff_server/src/session/workspaces.rs create mode 100644 crates/ruff_source_file/Cargo.toml create mode 100644 crates/ruff_source_file/src/lib.rs create mode 100644 crates/ruff_source_file/src/line_index.rs create mode 100644 crates/ruff_source_file/src/line_ranges.rs create mode 100644 crates/ruff_source_file/src/newlines.rs create mode 100644 crates/ruff_text_size/Cargo.toml create mode 100644 crates/ruff_text_size/src/lib.rs create mode 100644 crates/ruff_text_size/src/range.rs create mode 100644 crates/ruff_text_size/src/schemars_impls.rs create mode 100644 crates/ruff_text_size/src/serde_impls.rs create mode 100644 crates/ruff_text_size/src/size.rs create mode 100644 crates/ruff_text_size/src/traits.rs create mode 100644 crates/ruff_text_size/tests/auto_traits.rs create mode 100644 crates/ruff_text_size/tests/constructors.rs create mode 100644 crates/ruff_text_size/tests/indexing.rs create mode 100644 crates/ruff_text_size/tests/main.rs create mode 100644 crates/ruff_text_size/tests/serde.rs diff --git a/Cargo.lock b/Cargo.lock index dca9edda..1d25a354 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,9 +43,9 @@ dependencies = [ "itertools", "line_ending", "lsp", + "ruff_server", "tempfile", "thiserror 2.0.5", - "tokio", "tracing", "workspace", ] @@ -1220,6 +1220,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jod-thread" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" + [[package]] name = "json-strip-comments" version = "1.0.4" @@ -1352,6 +1358,19 @@ dependencies = [ "workspace", ] +[[package]] +name = "lsp-server" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9462c4dc73e17f971ec1f171d44bfffb72e65a130117233388a0ebc7ec5656f9" +dependencies = [ + "crossbeam-channel", + "log", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "lsp-types" version = "0.94.1" @@ -1365,6 +1384,19 @@ dependencies = [ "url", ] +[[package]] +name = "lsp-types" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "lsp_test" version = "0.0.0" @@ -1668,6 +1700,52 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ruff_server" +version = "0.1.1" +dependencies = [ + "air_r_formatter", + "air_r_parser", + "anyhow", + "cargo_metadata", + "crossbeam", + "ignore", + "insta", + "jod-thread", + "libc", + "lsp-server", + "lsp-types 0.95.1", + "regex", + "ruff_source_file", + "ruff_text_size", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.5", + "tracing", + "tracing-subscriber", + "workspace", +] + +[[package]] +name = "ruff_source_file" +version = "0.0.0" +dependencies = [ + "memchr", + "ruff_text_size", + "serde", +] + +[[package]] +name = "ruff_text_size" +version = "0.0.0" +dependencies = [ + "schemars", + "serde", + "serde_test", + "static_assertions", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1789,18 +1867,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -1862,6 +1940,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_test" +version = "1.0.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" +dependencies = [ + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1949,6 +2036,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2282,7 +2375,7 @@ dependencies = [ "dashmap 5.5.3", "futures", "httparse", - "lsp-types", + "lsp-types 0.94.1", "memchr", "serde", "serde_json", @@ -2304,7 +2397,7 @@ dependencies = [ "dashmap 5.5.3", "futures", "httparse", - "lsp-types", + "lsp-types 0.94.1", "memchr", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 2627d3a6..840ae1f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,9 @@ line_ending = { path = "./crates/line_ending" } lsp = { path = "./crates/lsp" } lsp_test = { path = "./crates/lsp_test" } tests_macros = { path = "./crates/tests_macros" } +ruff_server = { path = "./crates/ruff_server" } +ruff_source_file = { path = "./crates/ruff_source_file" } +ruff_text_size = { path = "./crates/ruff_text_size" } workspace = { path = "./crates/workspace" } anyhow = "1.0.89" @@ -56,13 +59,21 @@ httparse = "1.9.5" ignore = "0.4.23" insta = "1.40.0" itertools = "0.13.0" +jod-thread = "0.1.2" +libc = "0.2.153" line-index = "0.1.2" +lsp-server = "0.7.8" +lsp-types = "0.95.1" memchr = "2.7.4" path-absolutize = "3.1.1" proc-macro2 = "1.0.86" +regex = "1.11.1" rustc-hash = "2.1.0" +schemars = "0.8.21" serde = "1.0.215" serde_json = "1.0.132" +serde_test = "1.0.177" +static_assertions = "1.1.0" struct-field-names-as-array = "0.3.0" strum = "0.26" tempfile = "3.9.0" diff --git a/crates/air/Cargo.toml b/crates/air/Cargo.toml index 305e707f..94367be8 100644 --- a/crates/air/Cargo.toml +++ b/crates/air/Cargo.toml @@ -25,8 +25,8 @@ ignore = { workspace = true } itertools = { workspace = true } line_ending = { workspace = true } lsp = { workspace = true } +ruff_server = { workspace = true } thiserror = { workspace = true } -tokio = "1.41.1" tracing = { workspace = true } workspace = { workspace = true } diff --git a/crates/air/src/commands/language_server.rs b/crates/air/src/commands/language_server.rs index 30fd07f5..490d847f 100644 --- a/crates/air/src/commands/language_server.rs +++ b/crates/air/src/commands/language_server.rs @@ -1,10 +1,18 @@ +use std::num::NonZeroUsize; + +use ruff_server::Server; + use crate::args::LanguageServerCommand; use crate::ExitStatus; -#[tokio::main] -pub(crate) async fn language_server(_command: LanguageServerCommand) -> anyhow::Result { - // Returns after shutdown - lsp::start_lsp(tokio::io::stdin(), tokio::io::stdout()).await; +pub(crate) fn language_server(_command: LanguageServerCommand) -> anyhow::Result { + let four = NonZeroUsize::new(4).unwrap(); + + // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4. + let worker_threads = std::thread::available_parallelism() + .unwrap_or(four) + .max(four); - Ok(ExitStatus::Success) + let server = Server::new(worker_threads)?; + server.run().map(|()| ExitStatus::Success) } diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml new file mode 100644 index 00000000..46c1a73a --- /dev/null +++ b/crates/ruff_server/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "ruff_server" +version = "0.1.1" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[lib] + +[dependencies] +air_r_formatter = { workspace = true } +air_r_parser = { workspace = true } +ruff_source_file = { workspace = true } +ruff_text_size = { workspace = true } +workspace = { workspace = true } + +anyhow = { workspace = true } +crossbeam = { workspace = true } +ignore = { workspace = true } +jod-thread = { workspace = true } +lsp-server = { workspace = true } +lsp-types = { workspace = true } +regex = { workspace = true } +rustc-hash = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +[dev-dependencies] +insta = { workspace = true } + +[target.'cfg(target_vendor = "apple")'.dependencies] +libc = { workspace = true } + +[build-dependencies] +cargo_metadata.workspace = true + +[lints] +workspace = true diff --git a/crates/ruff_server/build.rs b/crates/ruff_server/build.rs new file mode 100644 index 00000000..17cde13a --- /dev/null +++ b/crates/ruff_server/build.rs @@ -0,0 +1,34 @@ +use std::env; +use std::fs; +use std::path::Path; + +extern crate cargo_metadata; + +fn main() { + write_workspace_crate_names(); +} + +/// Write out a constant array of air crate names as `AIR_CRATE_NAMES` at build time +fn write_workspace_crate_names() { + let dir = env::var_os("OUT_DIR").unwrap(); + let path = Path::new(&dir).join("crates.rs"); + + // Equivalent to `cargo metadata --no-deps` + let mut cmd = cargo_metadata::MetadataCommand::new(); + cmd.no_deps(); + let metadata = cmd.exec().unwrap(); + + let packages: Vec = metadata + .workspace_packages() + .iter() + .map(|package| package.name.clone()) + .map(|package| String::from("\"") + package.as_str() + "\",") + .collect(); + + let packages = packages.join(" "); + + let contents = format!("pub(crate) const AIR_CRATE_NAMES: &[&str] = &[{packages}];"); + + fs::write(&path, contents).unwrap(); + println!("cargo::rerun-if-changed=build.rs"); +} diff --git a/crates/ruff_server/src/crates.rs b/crates/ruff_server/src/crates.rs new file mode 100644 index 00000000..053fe3c1 --- /dev/null +++ b/crates/ruff_server/src/crates.rs @@ -0,0 +1,3 @@ +// Generates `AIR_CRATE_NAMES`, a const array of the crate names in the air workspace, +// see `server/src/build.rs` +include!(concat!(env!("OUT_DIR"), "/crates.rs")); diff --git a/crates/ruff_server/src/edit.rs b/crates/ruff_server/src/edit.rs new file mode 100644 index 00000000..5ea2240d --- /dev/null +++ b/crates/ruff_server/src/edit.rs @@ -0,0 +1,83 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +//! Types and utilities for working with text, modifying source files, and `Ruff <-> LSP` type conversion. + +mod range; +mod replacement; +mod text_document; + +use lsp_types::{PositionEncodingKind, Url}; +pub(crate) use range::{RangeExt, ToRangeExt}; +pub(crate) use replacement::Replacement; +pub use text_document::TextDocument; +pub(crate) use text_document::{DocumentVersion, LanguageId}; + +/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`]. +// Please maintain the order from least to greatest priority for the derived `Ord` impl. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum PositionEncoding { + /// UTF 16 is the encoding supported by all LSP clients. + #[default] + UTF16, + + /// Second choice because UTF32 uses a fixed 4 byte encoding for each character (makes conversion relatively easy) + UTF32, + + /// Ruff's preferred encoding + UTF8, +} + +/// A unique document ID, derived from a URL passed as part of an LSP request. +/// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook. +#[derive(Clone, Debug)] +pub enum DocumentKey { + Text(Url), +} + +impl DocumentKey { + /// Converts the key back into its original URL. + #[allow(dead_code)] + pub(crate) fn into_url(self) -> Url { + match self { + DocumentKey::Text(url) => url, + } + } +} + +impl std::fmt::Display for DocumentKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Text(url) => url.fmt(f), + } + } +} + +impl From for lsp_types::PositionEncodingKind { + fn from(value: PositionEncoding) -> Self { + match value { + PositionEncoding::UTF8 => lsp_types::PositionEncodingKind::UTF8, + PositionEncoding::UTF16 => lsp_types::PositionEncodingKind::UTF16, + PositionEncoding::UTF32 => lsp_types::PositionEncodingKind::UTF32, + } + } +} + +impl TryFrom<&lsp_types::PositionEncodingKind> for PositionEncoding { + type Error = (); + + fn try_from(value: &PositionEncodingKind) -> Result { + Ok(if value == &PositionEncodingKind::UTF8 { + PositionEncoding::UTF8 + } else if value == &PositionEncodingKind::UTF16 { + PositionEncoding::UTF16 + } else if value == &PositionEncodingKind::UTF32 { + PositionEncoding::UTF32 + } else { + return Err(()); + }) + } +} diff --git a/crates/ruff_server/src/edit/range.rs b/crates/ruff_server/src/edit/range.rs new file mode 100644 index 00000000..29fb12fd --- /dev/null +++ b/crates/ruff_server/src/edit/range.rs @@ -0,0 +1,171 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use super::PositionEncoding; +use lsp_types as types; +use ruff_source_file::OneIndexed; +use ruff_source_file::{LineIndex, SourceLocation}; +use ruff_text_size::{TextRange, TextSize}; + +pub(crate) trait RangeExt { + fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) + -> TextRange; +} + +pub(crate) trait ToRangeExt { + fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range; +} + +fn u32_index_to_usize(index: u32) -> usize { + usize::try_from(index).expect("u32 fits in usize") +} + +impl RangeExt for lsp_types::Range { + fn to_text_range( + &self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> TextRange { + let start_line = index.line_range( + OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), + text, + ); + let end_line = index.line_range( + OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), + text, + ); + + let (start_column_offset, end_column_offset) = match encoding { + PositionEncoding::UTF8 => ( + TextSize::new(self.start.character), + TextSize::new(self.end.character), + ), + + PositionEncoding::UTF16 => { + // Fast path for ASCII only documents + if index.is_ascii() { + ( + TextSize::new(self.start.character), + TextSize::new(self.end.character), + ) + } else { + // UTF16 encodes characters either as one or two 16 bit words. + // The position in `range` is the 16-bit word offset from the start of the line (and not the character offset) + // UTF-16 with a text that may use variable-length characters. + ( + utf8_column_offset(self.start.character, &text[start_line]), + utf8_column_offset(self.end.character, &text[end_line]), + ) + } + } + PositionEncoding::UTF32 => { + // UTF-32 uses 4 bytes for each character. Meaning, the position in range is a character offset. + return TextRange::new( + index.offset( + OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), + OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.character)), + text, + ), + index.offset( + OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), + OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.character)), + text, + ), + ); + } + }; + + TextRange::new( + start_line.start() + start_column_offset.clamp(TextSize::new(0), start_line.end()), + end_line.start() + end_column_offset.clamp(TextSize::new(0), end_line.end()), + ) + } +} + +impl ToRangeExt for TextRange { + fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range { + types::Range { + start: source_location_to_position(&offset_to_source_location( + self.start(), + text, + index, + encoding, + )), + end: source_location_to_position(&offset_to_source_location( + self.end(), + text, + index, + encoding, + )), + } + } +} + +/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. +fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { + let mut utf8_code_unit_offset = TextSize::new(0); + + let mut i = 0u32; + + for c in line.chars() { + if i >= utf16_code_unit_offset { + break; + } + + // Count characters encoded as two 16 bit words as 2 characters. + { + utf8_code_unit_offset += + TextSize::new(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); + i += u32::try_from(c.len_utf16()).expect("utf16 len always <=2"); + } + } + + utf8_code_unit_offset +} + +fn offset_to_source_location( + offset: TextSize, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, +) -> SourceLocation { + match encoding { + PositionEncoding::UTF8 => { + let row = index.line_index(offset); + let column = offset - index.line_start(row, text); + + SourceLocation { + column: OneIndexed::from_zero_indexed(column.to_usize()), + row, + } + } + PositionEncoding::UTF16 => { + let row = index.line_index(offset); + + let column = if index.is_ascii() { + (offset - index.line_start(row, text)).to_usize() + } else { + let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)]; + up_to_line.encode_utf16().count() + }; + + SourceLocation { + column: OneIndexed::from_zero_indexed(column), + row, + } + } + PositionEncoding::UTF32 => index.source_location(offset, text), + } +} + +fn source_location_to_position(location: &SourceLocation) -> types::Position { + types::Position { + line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"), + character: u32::try_from(location.column.to_zero_indexed()) + .expect("character usize fits in u32"), + } +} diff --git a/crates/ruff_server/src/edit/replacement.rs b/crates/ruff_server/src/edit/replacement.rs new file mode 100644 index 00000000..701d120d --- /dev/null +++ b/crates/ruff_server/src/edit/replacement.rs @@ -0,0 +1,234 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use ruff_text_size::{TextLen, TextRange, TextSize}; + +#[derive(Debug)] +pub(crate) struct Replacement { + pub(crate) source_range: TextRange, + pub(crate) modified_range: TextRange, +} + +impl Replacement { + /// Creates a [`Replacement`] that describes the `source_range` of `source` to replace + /// with `modified` sliced by `modified_range`. + pub(crate) fn between( + source: &str, + source_line_starts: &[TextSize], + modified: &str, + modified_line_starts: &[TextSize], + ) -> Self { + let mut source_start = TextSize::default(); + let mut modified_start = TextSize::default(); + + for (source_line_start, modified_line_start) in source_line_starts + .iter() + .copied() + .zip(modified_line_starts.iter().copied()) + .skip(1) + { + if source[TextRange::new(source_start, source_line_start)] + != modified[TextRange::new(modified_start, modified_line_start)] + { + break; + } + source_start = source_line_start; + modified_start = modified_line_start; + } + + let mut source_end = source.text_len(); + let mut modified_end = modified.text_len(); + + for (source_line_start, modified_line_start) in source_line_starts + .iter() + .rev() + .copied() + .zip(modified_line_starts.iter().rev().copied()) + { + if source_line_start < source_start + || modified_line_start < modified_start + || source[TextRange::new(source_line_start, source_end)] + != modified[TextRange::new(modified_line_start, modified_end)] + { + break; + } + source_end = source_line_start; + modified_end = modified_line_start; + } + + Replacement { + source_range: TextRange::new(source_start, source_end), + modified_range: TextRange::new(modified_start, modified_end), + } + } +} + +#[cfg(test)] +mod tests { + use ruff_source_file::LineIndex; + use ruff_text_size::TextRange; + + use super::Replacement; + + fn compute_replacement(source: &str, modified: &str) -> (Replacement, String) { + let source_index = LineIndex::from_source_text(source); + let modified_index = LineIndex::from_source_text(modified); + let replacement = Replacement::between( + source, + source_index.line_starts(), + modified, + modified_index.line_starts(), + ); + let mut expected = source.to_string(); + expected.replace_range( + replacement.source_range.start().to_usize()..replacement.source_range.end().to_usize(), + &modified[replacement.modified_range], + ); + (replacement, expected) + } + + #[test] + fn delete_first_line() { + let source = "aaaa +bbbb +cccc +"; + let modified = "bbbb +cccc +"; + let (replacement, expected) = compute_replacement(source, modified); + assert_eq!(replacement.source_range, TextRange::new(0.into(), 5.into())); + assert_eq!(replacement.modified_range, TextRange::empty(0.into())); + assert_eq!(modified, &expected); + } + + #[test] + fn delete_middle_line() { + let source = "aaaa +bbbb +cccc +dddd +"; + let modified = "aaaa +bbbb +dddd +"; + let (replacement, expected) = compute_replacement(source, modified); + assert_eq!( + replacement.source_range, + TextRange::new(10.into(), 15.into()) + ); + assert_eq!(replacement.modified_range, TextRange::empty(10.into())); + assert_eq!(modified, &expected); + } + + #[test] + fn delete_multiple_lines() { + let source = "aaaa +bbbb +cccc +dddd +eeee +ffff +"; + let modified = "aaaa +cccc +dddd +ffff +"; + let (replacement, expected) = compute_replacement(source, modified); + assert_eq!( + replacement.source_range, + TextRange::new(5.into(), 25.into()) + ); + assert_eq!( + replacement.modified_range, + TextRange::new(5.into(), 15.into()) + ); + assert_eq!(modified, &expected); + } + + #[test] + fn insert_first_line() { + let source = "bbbb +cccc +"; + let modified = "aaaa +bbbb +cccc +"; + let (replacement, expected) = compute_replacement(source, modified); + assert_eq!(replacement.source_range, TextRange::empty(0.into())); + assert_eq!( + replacement.modified_range, + TextRange::new(0.into(), 5.into()) + ); + assert_eq!(modified, &expected); + } + + #[test] + fn insert_middle_line() { + let source = "aaaa +cccc +"; + let modified = "aaaa +bbbb +cccc +"; + let (replacement, expected) = compute_replacement(source, modified); + assert_eq!(replacement.source_range, TextRange::empty(5.into())); + assert_eq!( + replacement.modified_range, + TextRange::new(5.into(), 10.into()) + ); + assert_eq!(modified, &expected); + } + + #[test] + fn insert_multiple_lines() { + let source = "aaaa +cccc +eeee +"; + let modified = "aaaa +bbbb +cccc +dddd +"; + let (replacement, expected) = compute_replacement(source, modified); + assert_eq!( + replacement.source_range, + TextRange::new(5.into(), 15.into()) + ); + assert_eq!( + replacement.modified_range, + TextRange::new(5.into(), 20.into()) + ); + assert_eq!(modified, &expected); + } + + #[test] + fn replace_lines() { + let source = "aaaa +bbbb +cccc +"; + let modified = "aaaa +bbcb +cccc +"; + let (replacement, expected) = compute_replacement(source, modified); + assert_eq!( + replacement.source_range, + TextRange::new(5.into(), 10.into()) + ); + assert_eq!( + replacement.modified_range, + TextRange::new(5.into(), 10.into()) + ); + assert_eq!(modified, &expected); + } +} diff --git a/crates/ruff_server/src/edit/text_document.rs b/crates/ruff_server/src/edit/text_document.rs new file mode 100644 index 00000000..71c91a98 --- /dev/null +++ b/crates/ruff_server/src/edit/text_document.rs @@ -0,0 +1,229 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use lsp_types::TextDocumentContentChangeEvent; +use ruff_source_file::LineIndex; + +use crate::edit::PositionEncoding; + +use super::RangeExt; + +pub(crate) type DocumentVersion = i32; + +/// The state of an individual document in the server. Stays up-to-date +/// with changes made by the user, including unsaved changes. +#[derive(Debug, Clone)] +pub struct TextDocument { + /// The string contents of the document. + contents: String, + /// A computed line index for the document. This should always reflect + /// the current version of `contents`. Using a function like [`Self::modify`] + /// will re-calculate the line index automatically when the `contents` value is updated. + index: LineIndex, + /// The latest version of the document, set by the LSP client. The server will panic in + /// debug mode if we attempt to update the document with an 'older' version. + version: DocumentVersion, + /// The language ID of the document as provided by the client. + language_id: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LanguageId { + R, + Other, +} + +impl From<&str> for LanguageId { + fn from(language_id: &str) -> Self { + match language_id { + "r" => Self::R, + _ => Self::Other, + } + } +} + +impl TextDocument { + pub fn new(contents: String, version: DocumentVersion) -> Self { + let index = LineIndex::from_source_text(&contents); + Self { + contents, + index, + version, + language_id: None, + } + } + + #[must_use] + pub fn with_language_id(mut self, language_id: &str) -> Self { + self.language_id = Some(LanguageId::from(language_id)); + self + } + + #[allow(dead_code)] + pub fn into_contents(self) -> String { + self.contents + } + + pub fn contents(&self) -> &str { + &self.contents + } + + pub fn index(&self) -> &LineIndex { + &self.index + } + + pub fn version(&self) -> DocumentVersion { + self.version + } + + pub fn language_id(&self) -> Option { + self.language_id + } + + pub fn apply_changes( + &mut self, + changes: Vec, + new_version: DocumentVersion, + encoding: PositionEncoding, + ) { + if let [lsp_types::TextDocumentContentChangeEvent { + range: None, text, .. + }] = changes.as_slice() + { + tracing::debug!("Fast path - replacing entire document"); + self.modify(|contents, version| { + contents.clone_from(text); + *version = new_version; + }); + return; + } + + let mut new_contents = self.contents().to_string(); + let mut active_index = self.index().clone(); + + for TextDocumentContentChangeEvent { + range, + text: change, + .. + } in changes + { + if let Some(range) = range { + let range = range.to_text_range(&new_contents, &active_index, encoding); + + new_contents.replace_range( + usize::from(range.start())..usize::from(range.end()), + &change, + ); + } else { + new_contents = change; + } + + active_index = LineIndex::from_source_text(&new_contents); + } + + self.modify_with_manual_index(|contents, version, index| { + *index = active_index; + *contents = new_contents; + *version = new_version; + }); + } + + pub fn update_version(&mut self, new_version: DocumentVersion) { + self.modify_with_manual_index(|_, version, _| { + *version = new_version; + }); + } + + // A private function for modifying the document's internal state + fn modify(&mut self, func: impl FnOnce(&mut String, &mut DocumentVersion)) { + self.modify_with_manual_index(|c, v, i| { + func(c, v); + *i = LineIndex::from_source_text(c); + }); + } + + // A private function for overriding how we update the line index by default. + fn modify_with_manual_index( + &mut self, + func: impl FnOnce(&mut String, &mut DocumentVersion, &mut LineIndex), + ) { + let old_version = self.version; + func(&mut self.contents, &mut self.version, &mut self.index); + debug_assert!(self.version >= old_version); + } +} + +#[cfg(test)] +mod tests { + use crate::edit::{PositionEncoding, TextDocument}; + use lsp_types::{Position, TextDocumentContentChangeEvent}; + + #[test] + fn redo_edit() { + let mut document = TextDocument::new( + r#"""" +测试comment +一些测试内容 +""" +import click + + +@click.group() +def interface(): + pas +"# + .to_string(), + 0, + ); + + // Add an `s`, remove it again (back to the original code), and then re-add the `s` + document.apply_changes( + vec![ + TextDocumentContentChangeEvent { + range: Some(lsp_types::Range::new( + Position::new(9, 7), + Position::new(9, 7), + )), + range_length: Some(0), + text: "s".to_string(), + }, + TextDocumentContentChangeEvent { + range: Some(lsp_types::Range::new( + Position::new(9, 7), + Position::new(9, 8), + )), + range_length: Some(1), + text: String::new(), + }, + TextDocumentContentChangeEvent { + range: Some(lsp_types::Range::new( + Position::new(9, 7), + Position::new(9, 7), + )), + range_length: Some(0), + text: "s".to_string(), + }, + ], + 1, + PositionEncoding::UTF16, + ); + + assert_eq!( + &document.contents, + r#"""" +测试comment +一些测试内容 +""" +import click + + +@click.group() +def interface(): + pass +"# + ); + } +} diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs new file mode 100644 index 00000000..80b2e862 --- /dev/null +++ b/crates/ruff_server/src/format.rs @@ -0,0 +1,48 @@ +use air_r_parser::RParserOptions; +use workspace::settings::FormatSettings; + +pub(crate) fn format( + source: &str, + formatter_settings: &FormatSettings, +) -> crate::Result> { + let parse = air_r_parser::parse(source, RParserOptions::default()); + + if parse.has_errors() { + return Err(anyhow::anyhow!("Can't format when there are parse errors.")); + } + + // Do we need to check that `doc` is indeed an R file? What about special + // files that don't have extensions like `NAMESPACE`, do we hard-code a + // list? What about unnamed temporary files? + + let format_options = formatter_settings.to_format_options(source); + let formatted = air_r_formatter::format_node(format_options, &parse.syntax())?; + let code = formatted.print()?.into_code(); + + Ok(Some(code)) +} + +// pub(crate) fn format_range( +// document: &TextDocument, +// formatter_settings: &FormatSettings, +// range: TextRange, +// ) -> crate::Result> { +// let format_options = formatter_settings.to_format_options(source_type, document.contents()); +// +// match ruff_python_formatter::format_range(document.contents(), range, format_options) { +// Ok(formatted) => { +// if formatted.as_code() == document.contents() { +// Ok(None) +// } else { +// Ok(Some(formatted)) +// } +// } +// // Special case - syntax/parse errors are handled here instead of +// // being propagated as visible server errors. +// Err(FormatModuleError::ParseError(error)) => { +// tracing::warn!("Unable to format document range: {error}"); +// Ok(None) +// } +// Err(err) => Err(err.into()), +// } +// } diff --git a/crates/ruff_server/src/lib.rs b/crates/ruff_server/src/lib.rs new file mode 100644 index 00000000..68a3e52f --- /dev/null +++ b/crates/ruff_server/src/lib.rs @@ -0,0 +1,20 @@ +//! ## The Ruff Language Server + +pub use server::Server; + +#[macro_use] +mod message; + +mod crates; +mod edit; +mod format; +mod logging; +mod server; +mod session; + +pub(crate) const SERVER_NAME: &str = "Air Language Server"; +pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// A common result type used in most cases where a +/// result type is needed. +pub(crate) type Result = anyhow::Result; diff --git a/crates/ruff_server/src/logging.rs b/crates/ruff_server/src/logging.rs new file mode 100644 index 00000000..80c7c377 --- /dev/null +++ b/crates/ruff_server/src/logging.rs @@ -0,0 +1,288 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +//! The logging system for `air lsp`. +//! +//! ## Air crate logs +//! +//! For air crates, a single log level is supplied as one of: error, warn, info, debug, +//! or trace, which is applied to all air crates that log. +//! +//! Resolution strategy: +//! +//! - The environment variable `AIR_LOG_LEVEL` is consulted. +//! +//! - The LSP `InitializeParams.initializationOptions.logLevel` option is consulted. This +//! can be set in VS Code / Positron using `air.logLevel`, or in Zed by supplying +//! `initialization_options`. +//! +//! - If neither are supplied, we fallback to `"info"`. +//! +//! ## Dependency crate logs +//! +//! For dependency crates, either a single log level can be supplied, or comma separated +//! `target=level` pairs can be supplied, like `tower_lsp=debug,tokio=info`. +//! +//! Resolution strategy: +//! +//! - The environment variable `AIR_DEPENDENCY_LOG_LEVELS` is consulted. +//! +//! - The LSP `InitializeParams.initializationOptions.dependencyLogLevels` option is +//! consulted. This can be set in VS Code / Positron using `air.dependencyLogLevel`, or +//! in Zed by supplying `initialization_options`. +//! +//! - If neither are supplied, we fallback to no logging for dependency crates. +//! +//! ## IDE support +//! +//! For VS Code and Zed, which are known to support `window/logMessage` well, logging will +//! emit a `window/logMessage` message. Otherwise, logging will write to `stderr`, +//! which should appear in the logs for most LSP clients. +use core::str; +use lsp_server::Message; +use lsp_types::notification::LogMessage; +use lsp_types::notification::Notification; +use lsp_types::ClientInfo; +use lsp_types::LogMessageParams; +use lsp_types::MessageType; +use serde::Deserialize; +use std::fmt::Display; +use std::io::{Error as IoError, ErrorKind, Write}; +use std::str::FromStr; +use tracing_subscriber::filter; +use tracing_subscriber::fmt::time::LocalTime; +use tracing_subscriber::fmt::TestWriter; +use tracing_subscriber::{ + fmt::{writer::BoxMakeWriter, MakeWriter}, + layer::SubscriberExt, + Layer, +}; + +use crate::crates; +use crate::server::ClientSender; + +// TODO: +// - Add `air.logLevel` and `air.dependencyLogLevels` as VS Code extension options that set +// the log levels, and pass them through the arbitrary `initializationOptions` field of +// `InitializeParams`. + +const AIR_LOG_LEVEL: &str = "AIR_LOG_LEVEL"; +const AIR_DEPENDENCY_LOG_LEVELS: &str = "AIR_DEPENDENCY_LOG_LEVELS"; + +// A log writer that uses LSPs logMessage method. +struct LogWriter<'a> { + client_tx: &'a ClientSender, +} + +impl<'a> LogWriter<'a> { + fn new(client_tx: &'a ClientSender) -> Self { + Self { client_tx } + } +} + +impl Write for LogWriter<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let message = str::from_utf8(buf).map_err(|e| IoError::new(ErrorKind::InvalidData, e))?; + let message = message.to_string(); + + let params = serde_json::to_value(LogMessageParams { + typ: MessageType::LOG, + message, + }) + .map_err(|e| IoError::new(ErrorKind::Other, e))?; + + self.client_tx + .send(Message::Notification(lsp_server::Notification { + method: LogMessage::METHOD.to_owned(), + params, + })) + .map_err(|e| IoError::new(ErrorKind::Other, e))?; + + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +struct LogWriterMaker { + client_tx: ClientSender, +} + +impl LogWriterMaker { + fn new(client_tx: ClientSender) -> Self { + Self { client_tx } + } +} + +impl<'a> MakeWriter<'a> for LogWriterMaker { + type Writer = LogWriter<'a>; + + fn make_writer(&'a self) -> Self::Writer { + LogWriter::new(&self.client_tx) + } +} + +pub(crate) fn init_logging( + client_tx: ClientSender, + log_level: Option, + dependency_log_levels: Option, + client_info: Option<&ClientInfo>, +) { + let log_level = resolve_log_level(log_level); + let dependency_log_levels = resolve_dependency_log_levels(dependency_log_levels); + + let writer = if client_info.is_some_and(|client_info| { + client_info.name.starts_with("Zed") || client_info.name.starts_with("Visual Studio Code") + }) { + // These IDEs are known to support `window/logMessage` well + BoxMakeWriter::new(LogWriterMaker::new(client_tx)) + } else if is_test_client(client_info) { + // Ensures a standard `cargo test` captures output unless `-- --nocapture` is used + BoxMakeWriter::new(TestWriter::default()) + } else { + // Fallback for other editors / IDEs + BoxMakeWriter::new(std::io::stderr) + }; + + let layer = tracing_subscriber::fmt::layer() + // Spend the effort cleaning up the logs before writing them. + // Particularly useful for instrumented logs with spans. + .pretty() + // Disable ANSI escapes, those are not supported in Code + .with_ansi(false) + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + // Don't display the thread ID or thread name + .with_thread_ids(false) + .with_thread_names(false) + // Don't display the event's target (module path). + // Mostly redundant with file paths. + .with_target(false) + // Display local time rather than UTC + .with_timer(LocalTime::rfc_3339()) + // Display the log level + .with_level(true) + .with_writer(writer) + .with_filter(log_filter(log_level, dependency_log_levels)); + + let subscriber = tracing_subscriber::Registry::default().with(layer); + + if is_test_client(client_info) { + // During parallel testing, `set_global_default()` gets called multiple times + // per process. That causes it to error, but we ignore this. + tracing::subscriber::set_global_default(subscriber).ok(); + } else { + tracing::subscriber::set_global_default(subscriber) + .expect("Should be able to set the global subscriber exactly once."); + } + + tracing::info!("Logging initialized with level: {log_level}"); +} + +/// We use a special `TestWriter` during tests to be compatible with `cargo test`'s +/// typical output capturing behavior. +/// +/// Important notes: +/// - `cargo test` swallows all logs unless you use `-- --nocapture`. +/// - Tests run in parallel, so logs can be interleaved unless you run `--test-threads 1`. +/// +/// We use `cargo test -- --nocapture --test-threads 1` on CI because of all of this. +fn is_test_client(client_info: Option<&ClientInfo>) -> bool { + client_info.map_or(false, |client_info| client_info.name == "AirTestClient") +} + +fn log_filter(log_level: LogLevel, dependency_log_levels: Option) -> filter::Targets { + // Initialize `filter` from dependency log levels. + // If nothing is supplied, dependency logs are completely off. + let mut filter = match dependency_log_levels { + Some(dependency_log_levels) => match filter::Targets::from_str(&dependency_log_levels) { + Ok(level) => level, + Err(_) => filter::Targets::new(), + }, + None => filter::Targets::new(), + }; + + let log_level = log_level.tracing_level(); + + // Apply the air log level to each air crate that logs + for target in crates::AIR_CRATE_NAMES { + filter = filter.with_target(*target, log_level); + } + + filter +} + +fn resolve_log_level(log_level: Option) -> LogLevel { + // Check log environment variable, this overrides any Client options + if let Some(log_level) = std::env::var(AIR_LOG_LEVEL) + .ok() + .and_then(|level| serde_json::from_value(serde_json::Value::String(level)).ok()) + { + return log_level; + } + + // Client specified log level through initialization parameters + if let Some(log_level) = log_level { + return log_level; + } + + // Default to info logs for air crates + LogLevel::Info +} + +fn resolve_dependency_log_levels(dependency_log_levels: Option) -> Option { + // Check dependency log environment variable, this overrides any Client options + if let Ok(dependency_log_levels) = std::env::var(AIR_DEPENDENCY_LOG_LEVELS) { + return Some(dependency_log_levels); + } + + // Client specified log level through initialization parameters + if dependency_log_levels.is_some() { + return dependency_log_levels; + } + + // Default to no logs for dependency crates + None +} + +#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "lowercase")] +pub(crate) enum LogLevel { + Error, + Warn, + #[default] + Info, + Debug, + Trace, +} + +impl LogLevel { + fn tracing_level(self) -> tracing::Level { + match self { + Self::Error => tracing::Level::ERROR, + Self::Warn => tracing::Level::WARN, + Self::Info => tracing::Level::INFO, + Self::Debug => tracing::Level::DEBUG, + Self::Trace => tracing::Level::TRACE, + } + } +} + +impl Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Error => f.write_str("Error"), + Self::Warn => f.write_str("Warn"), + Self::Info => f.write_str("Info"), + Self::Debug => f.write_str("Debug"), + Self::Trace => f.write_str("Trace"), + } + } +} diff --git a/crates/ruff_server/src/message.rs b/crates/ruff_server/src/message.rs new file mode 100644 index 00000000..ecd26ec8 --- /dev/null +++ b/crates/ruff_server/src/message.rs @@ -0,0 +1,62 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use anyhow::Context; +use lsp_types::notification::Notification; +use std::sync::OnceLock; + +use crate::server::ClientSender; + +// TODO: This won't work well with tests +static MESSENGER: OnceLock = OnceLock::new(); + +pub(crate) fn init_messenger(client_sender: ClientSender) { + MESSENGER + .set(client_sender) + .expect("Messenger should only be initialized once"); +} + +pub(crate) fn show_message(message: String, message_type: lsp_types::MessageType) { + try_show_message(message, message_type).unwrap(); +} + +pub(super) fn try_show_message( + message: String, + message_type: lsp_types::MessageType, +) -> crate::Result<()> { + MESSENGER + .get() + .ok_or_else(|| anyhow::anyhow!("Messenger not initialized"))? + .send(lsp_server::Message::Notification( + lsp_server::Notification { + method: lsp_types::notification::ShowMessage::METHOD.into(), + params: serde_json::to_value(lsp_types::ShowMessageParams { + typ: message_type, + message, + })?, + }, + )) + .context("Failed to send message")?; + + Ok(()) +} + +/// Sends a request to display an error to the client with a formatted message. The error is sent +/// in a `window/showMessage` notification. +macro_rules! show_err_msg { + ($msg:expr$(, $($arg:tt),*)?) => { + crate::message::show_message(::core::format_args!($msg, $($($arg),*)?).to_string(), lsp_types::MessageType::ERROR) + }; +} + +/// Sends a request to display a warning to the client with a formatted message. The warning is +/// sent in a `window/showMessage` notification. +#[allow(unused_macros)] +macro_rules! show_warn_msg { + ($msg:expr$(, $($arg:tt),*)?) => { + crate::message::show_message(::core::format_args!($msg, $($($arg),*)?).to_string(), lsp_types::MessageType::WARNING) + }; +} diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs new file mode 100644 index 00000000..359901fd --- /dev/null +++ b/crates/ruff_server/src/server.rs @@ -0,0 +1,274 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +//! Scheduling, I/O, and API endpoints. + +use lsp_server as lsp; +use lsp_types as types; +use lsp_types::InitializeParams; +use std::num::NonZeroUsize; +// The new PanicInfoHook name requires MSRV >= 1.82 +#[allow(deprecated)] +use std::panic::PanicInfo; +use types::DidChangeWatchedFilesRegistrationOptions; +use types::FileSystemWatcher; +use types::OneOf; +use types::TextDocumentSyncCapability; +use types::TextDocumentSyncKind; +use types::TextDocumentSyncOptions; +use types::WorkspaceFoldersServerCapabilities; + +use self::connection::Connection; +use self::connection::ConnectionInitializer; +use self::schedule::event_loop_thread; +use self::schedule::Scheduler; +use self::schedule::Task; +use crate::edit::PositionEncoding; +use crate::session::ResolvedClientCapabilities; +use crate::session::Session; + +mod api; +mod client; +mod connection; +mod schedule; + +use crate::message::try_show_message; +pub(crate) use connection::ClientSender; + +pub(crate) type Result = std::result::Result; + +pub struct Server { + connection: Connection, + client_capabilities: ResolvedClientCapabilities, + worker_threads: NonZeroUsize, + session: Session, +} + +impl Server { + pub fn new(worker_threads: NonZeroUsize) -> crate::Result { + let connection = ConnectionInitializer::stdio(); + + let (id, initialize_params) = connection.initialize_start()?; + + let client_capabilities = initialize_params.capabilities; + let client_capabilities = ResolvedClientCapabilities::new(client_capabilities); + let position_encoding = Self::find_best_position_encoding(&client_capabilities); + let server_capabilities = Self::server_capabilities(position_encoding); + + let connection = connection.initialize_finish( + id, + &server_capabilities, + crate::SERVER_NAME, + crate::SERVER_VERSION, + )?; + + let InitializeParams { + workspace_folders, + client_info, + .. + } = initialize_params; + + let workspace_folders = workspace_folders.unwrap_or_default(); + + // TODO: Get user specified options from `initialization_options` + let log_level = None; + let dependency_log_levels = None; + + crate::logging::init_logging( + connection.make_sender(), + log_level, + dependency_log_levels, + client_info.as_ref(), + ); + + crate::message::init_messenger(connection.make_sender()); + + Ok(Self { + connection, + worker_threads, + session: Session::new( + client_capabilities.clone(), + position_encoding, + workspace_folders, + )?, + client_capabilities, + }) + } + + pub fn run(self) -> crate::Result<()> { + // The new PanicInfoHook name requires MSRV >= 1.82 + #[allow(deprecated)] + type PanicHook = Box) + 'static + Sync + Send>; + struct RestorePanicHook { + hook: Option, + } + + impl Drop for RestorePanicHook { + fn drop(&mut self) { + if let Some(hook) = self.hook.take() { + std::panic::set_hook(hook); + } + } + } + + // unregister any previously registered panic hook + // The hook will be restored when this function exits. + let _ = RestorePanicHook { + hook: Some(std::panic::take_hook()), + }; + + // When we panic, try to notify the client. + std::panic::set_hook(Box::new(move |panic_info| { + use std::io::Write; + + let backtrace = std::backtrace::Backtrace::force_capture(); + tracing::error!("{panic_info}\n{backtrace}"); + + // we also need to print to stderr directly for when using `$logTrace` because + // the message won't be sent to the client. + // But don't use `eprintln` because `eprintln` itself may panic if the pipe is broken. + let mut stderr = std::io::stderr().lock(); + writeln!(stderr, "{panic_info}\n{backtrace}").ok(); + + try_show_message( + "The Ruff language server exited with a panic. See the logs for more details." + .to_string(), + lsp_types::MessageType::ERROR, + ) + .ok(); + })); + + event_loop_thread(move || { + Self::event_loop( + &self.connection, + &self.client_capabilities, + self.session, + self.worker_threads, + )?; + self.connection.close()?; + Ok(()) + })? + .join() + } + + #[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet. + fn event_loop( + connection: &Connection, + resolved_client_capabilities: &ResolvedClientCapabilities, + mut session: Session, + worker_threads: NonZeroUsize, + ) -> crate::Result<()> { + let mut scheduler = + schedule::Scheduler::new(&mut session, worker_threads, connection.make_sender()); + + Self::try_register_capabilities(resolved_client_capabilities, &mut scheduler); + for msg in connection.incoming() { + if connection.handle_shutdown(&msg)? { + break; + } + let task = match msg { + lsp::Message::Request(req) => api::request(req), + lsp::Message::Notification(notification) => api::notification(notification), + lsp::Message::Response(response) => scheduler.response(response), + }; + scheduler.dispatch(task); + } + + Ok(()) + } + + // TODO: Add in other dynamic configuration + fn try_register_capabilities( + resolved_client_capabilities: &ResolvedClientCapabilities, + scheduler: &mut Scheduler, + ) { + let _span = tracing::info_span!("try_register_capabilities").entered(); + + // Register capabilities to the client + let mut registrations: Vec = vec![]; + + if resolved_client_capabilities.dynamic_registration_for_did_change_watched_files { + // Watch for changes in `air.toml` files so we can react dynamically + let watch_air_toml_registration = lsp_types::Registration { + id: String::from("air-toml-watcher"), + method: "workspace/didChangeWatchedFiles".into(), + register_options: Some( + serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { + watchers: vec![FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String("**/air.toml".into()), + kind: None, + }], + }) + .unwrap(), + ), + }; + + registrations.push(watch_air_toml_registration); + } else { + tracing::warn!("LSP client does not support watched files dynamic capability - automatic configuration reloading will not be available."); + } + + if !registrations.is_empty() { + let params = lsp_types::RegistrationParams { registrations }; + + let response_handler = |()| { + tracing::info!("Dynamic configuration successfully registered"); + Task::nothing() + }; + + if let Err(error) = scheduler + .request::(params, response_handler) + { + tracing::error!( + "An error occurred when trying to dynamically register capabilities: {error}" + ); + } + } + } + + fn find_best_position_encoding( + client_capabilities: &ResolvedClientCapabilities, + ) -> PositionEncoding { + // If the client supports UTF-8 we use that, even if it's not its + // preferred encoding (at position 0). Otherwise we use the mandatory + // UTF-16 encoding that all clients and servers must support, even if + // the client would have preferred UTF-32. Note that VSCode and Positron + // only support UTF-16. + if client_capabilities + .position_encodings + .contains(&lsp_types::PositionEncodingKind::UTF8) + { + PositionEncoding::UTF8 + } else { + PositionEncoding::UTF16 + } + } + + fn server_capabilities(position_encoding: PositionEncoding) -> types::ServerCapabilities { + types::ServerCapabilities { + position_encoding: Some(position_encoding.into()), + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + will_save: Some(false), + will_save_wait_until: Some(false), + ..Default::default() + }, + )), + workspace: Some(types::WorkspaceServerCapabilities { + workspace_folders: Some(WorkspaceFoldersServerCapabilities { + supported: Some(true), + change_notifications: Some(OneOf::Left(true)), + }), + file_operations: None, + }), + document_formatting_provider: Some(OneOf::Left(true)), + document_range_formatting_provider: Some(OneOf::Left(true)), + ..Default::default() + } + } +} diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs new file mode 100644 index 00000000..3f1d084f --- /dev/null +++ b/crates/ruff_server/src/server/api.rs @@ -0,0 +1,253 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use crate::{server::schedule::Task, session::Session}; +use lsp_server as server; + +mod notifications; +mod requests; +mod traits; + +use notifications as notification; +use requests as request; + +use self::traits::{NotificationHandler, RequestHandler}; + +use super::{client::Responder, schedule::BackgroundSchedule, Result}; + +/// Defines the `document_url` method for implementers of [`traits::Notification`] and [`traits::Request`], +/// given the parameter type used by the implementer. +macro_rules! define_document_url { + ($params:ident: &$p:ty) => { + fn document_url($params: &$p) -> std::borrow::Cow { + std::borrow::Cow::Borrowed(&$params.text_document.uri) + } + }; +} + +use define_document_url; + +pub(super) fn request<'a>(req: server::Request) -> Task<'a> { + let id = req.id.clone(); + + match req.method.as_str() { + request::Format::METHOD => { + background_request_task::(req, BackgroundSchedule::Fmt) + } + request::FormatRange::METHOD => { + background_request_task::(req, BackgroundSchedule::Fmt) + } + method => { + tracing::warn!("Received request {method} which does not have a handler"); + return Task::nothing(); + } + } + .unwrap_or_else(|err| { + tracing::error!("Encountered error when routing request with ID {id}: {err}"); + show_err_msg!( + "Ruff failed to handle a request from the editor. Check the logs for more details." + ); + let result: Result<()> = Err(err); + Task::immediate(id, result) + }) +} + +pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { + match notif.method.as_str() { + notification::Cancel::METHOD => local_notification_task::(notif), + notification::DidChange::METHOD => { + local_notification_task::(notif) + } + notification::DidChangeConfiguration::METHOD => { + local_notification_task::(notif) + } + notification::DidChangeWatchedFiles::METHOD => { + local_notification_task::(notif) + } + notification::DidChangeWorkspace::METHOD => { + local_notification_task::(notif) + } + notification::DidClose::METHOD => local_notification_task::(notif), + notification::DidOpen::METHOD => local_notification_task::(notif), + method => { + tracing::warn!("Received notification {method} which does not have a handler."); + return Task::nothing(); + } + } + .unwrap_or_else(|err| { + tracing::error!("Encountered error when routing notification: {err}"); + show_err_msg!("Ruff failed to handle a notification from the editor. Check the logs for more details."); + Task::nothing() + }) +} + +#[allow(dead_code)] +fn local_request_task<'a, R: traits::SyncRequestHandler>( + req: server::Request, +) -> super::Result> { + let (id, params) = cast_request::(req)?; + Ok(Task::local(|session, notifier, requester, responder| { + let result = R::run(session, notifier, requester, params); + respond::(id, result, &responder); + })) +} + +fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>( + req: server::Request, + schedule: BackgroundSchedule, +) -> super::Result> { + let (id, params) = cast_request::(req)?; + Ok(Task::background(schedule, move |session: &Session| { + // TODO(jane): we should log an error if we can't take a snapshot. + let Some(snapshot) = session.take_snapshot(R::document_url(¶ms).into_owned()) else { + return Box::new(|_, _| {}); + }; + Box::new(move |notifier, responder| { + let result = R::run_with_snapshot(snapshot, notifier, params); + respond::(id, result, &responder); + }) + })) +} + +fn local_notification_task<'a, N: traits::SyncNotificationHandler>( + notif: server::Notification, +) -> super::Result> { + let (id, params) = cast_notification::(notif)?; + Ok(Task::local(move |session, notifier, requester, _| { + if let Err(err) = N::run(session, notifier, requester, params) { + tracing::error!("An error occurred while running {id}: {err}"); + show_err_msg!("Ruff encountered a problem. Check the logs for more details."); + } + })) +} + +#[allow(dead_code)] +fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationHandler>( + req: server::Notification, + schedule: BackgroundSchedule, +) -> super::Result> { + let (id, params) = cast_notification::(req)?; + Ok(Task::background(schedule, move |session: &Session| { + // TODO(jane): we should log an error if we can't take a snapshot. + let Some(snapshot) = session.take_snapshot(N::document_url(¶ms).into_owned()) else { + return Box::new(|_, _| {}); + }; + Box::new(move |notifier, _| { + if let Err(err) = N::run_with_snapshot(snapshot, notifier, params) { + tracing::error!("An error occurred while running {id}: {err}"); + show_err_msg!("Ruff encountered a problem. Check the logs for more details."); + } + }) + })) +} + +/// Tries to cast a serialized request from the server into +/// a parameter type for a specific request handler. +/// It is *highly* recommended to not override this function in your +/// implementation. +fn cast_request( + request: server::Request, +) -> super::Result<( + server::RequestId, + <::RequestType as lsp_types::request::Request>::Params, +)> +where + Req: traits::RequestHandler, +{ + request + .extract(Req::METHOD) + .map_err(|err| match err { + json_err @ server::ExtractError::JsonError { .. } => { + anyhow::anyhow!("JSON parsing failure:\n{json_err}") + } + server::ExtractError::MethodMismatch(_) => { + unreachable!("A method mismatch should not be possible here unless you've used a different handler (`Req`) \ + than the one whose method name was matched against earlier.") + } + }) + .with_failure_code(server::ErrorCode::InternalError) +} + +/// Sends back a response to the server using a [`Responder`]. +fn respond( + id: server::RequestId, + result: crate::server::Result< + <::RequestType as lsp_types::request::Request>::Result, + >, + responder: &Responder, +) where + Req: traits::RequestHandler, +{ + if let Err(err) = &result { + tracing::error!("An error occurred with result ID {id}: {err}"); + show_err_msg!("Ruff encountered a problem. Check the logs for more details."); + } + if let Err(err) = responder.respond(id, result) { + tracing::error!("Failed to send response: {err}"); + } +} + +/// Tries to cast a serialized request from the server into +/// a parameter type for a specific request handler. +fn cast_notification( + notification: server::Notification, +) -> super::Result< + ( + &'static str, + <::NotificationType as lsp_types::notification::Notification>::Params, +)> where N: traits::NotificationHandler{ + Ok(( + N::METHOD, + notification + .extract(N::METHOD) + .map_err(|err| match err { + json_err @ server::ExtractError::JsonError { .. } => { + anyhow::anyhow!("JSON parsing failure:\n{json_err}") + } + server::ExtractError::MethodMismatch(_) => { + unreachable!("A method mismatch should not be possible here unless you've used a different handler (`N`) \ + than the one whose method name was matched against earlier.") + } + }) + .with_failure_code(server::ErrorCode::InternalError)?, + )) +} + +pub(crate) struct Error { + pub(crate) code: server::ErrorCode, + pub(crate) error: anyhow::Error, +} + +/// A trait to convert result types into the server result type, [`super::Result`]. +trait LSPResult { + fn with_failure_code(self, code: server::ErrorCode) -> super::Result; +} + +impl> LSPResult for core::result::Result { + fn with_failure_code(self, code: server::ErrorCode) -> super::Result { + self.map_err(|err| Error::new(err.into(), code)) + } +} + +impl Error { + pub(crate) fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { + Self { code, error: err } + } +} + +// Right now, we treat the error code as invisible data that won't +// be printed. +impl std::fmt::Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.error.fmt(f) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.error.fmt(f) + } +} diff --git a/crates/ruff_server/src/server/api/notifications.rs b/crates/ruff_server/src/server/api/notifications.rs new file mode 100644 index 00000000..231f8f2d --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications.rs @@ -0,0 +1,16 @@ +mod cancel; +mod did_change; +mod did_change_configuration; +mod did_change_watched_files; +mod did_change_workspace; +mod did_close; +mod did_open; + +use super::traits::{NotificationHandler, SyncNotificationHandler}; +pub(super) use cancel::Cancel; +pub(super) use did_change::DidChange; +pub(super) use did_change_configuration::DidChangeConfiguration; +pub(super) use did_change_watched_files::DidChangeWatchedFiles; +pub(super) use did_change_workspace::DidChangeWorkspace; +pub(super) use did_close::DidClose; +pub(super) use did_open::DidOpen; diff --git a/crates/ruff_server/src/server/api/notifications/cancel.rs b/crates/ruff_server/src/server/api/notifications/cancel.rs new file mode 100644 index 00000000..d174ade2 --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications/cancel.rs @@ -0,0 +1,29 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use crate::server::client::{Notifier, Requester}; +use crate::server::Result; +use crate::session::Session; +use lsp_types as types; +use lsp_types::notification as notif; + +pub(crate) struct Cancel; + +impl super::NotificationHandler for Cancel { + type NotificationType = notif::Cancel; +} + +impl super::SyncNotificationHandler for Cancel { + fn run( + _session: &mut Session, + _notifier: Notifier, + _requester: &mut Requester, + _params: types::CancelParams, + ) -> Result<()> { + // TODO(jane): Handle this once we have task cancellation in the scheduler. + Ok(()) + } +} diff --git a/crates/ruff_server/src/server/api/notifications/did_change.rs b/crates/ruff_server/src/server/api/notifications/did_change.rs new file mode 100644 index 00000000..329e830e --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications/did_change.rs @@ -0,0 +1,43 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use crate::server::api::LSPResult; +use crate::server::client::{Notifier, Requester}; +use crate::server::Result; +use crate::session::Session; +use lsp_server::ErrorCode; +use lsp_types as types; +use lsp_types::notification as notif; + +pub(crate) struct DidChange; + +impl super::NotificationHandler for DidChange { + type NotificationType = notif::DidChangeTextDocument; +} + +impl super::SyncNotificationHandler for DidChange { + fn run( + session: &mut Session, + _notifier: Notifier, + _requester: &mut Requester, + types::DidChangeTextDocumentParams { + text_document: + types::VersionedTextDocumentIdentifier { + uri, + version: new_version, + }, + content_changes, + }: types::DidChangeTextDocumentParams, + ) -> Result<()> { + let key = session.key_from_url(uri); + + session + .update_text_document(&key, content_changes, new_version) + .with_failure_code(ErrorCode::InternalError)?; + + Ok(()) + } +} diff --git a/crates/ruff_server/src/server/api/notifications/did_change_configuration.rs b/crates/ruff_server/src/server/api/notifications/did_change_configuration.rs new file mode 100644 index 00000000..b57af172 --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications/did_change_configuration.rs @@ -0,0 +1,29 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use crate::server::client::{Notifier, Requester}; +use crate::server::Result; +use crate::session::Session; +use lsp_types as types; +use lsp_types::notification as notif; + +pub(crate) struct DidChangeConfiguration; + +impl super::NotificationHandler for DidChangeConfiguration { + type NotificationType = notif::DidChangeConfiguration; +} + +impl super::SyncNotificationHandler for DidChangeConfiguration { + fn run( + _session: &mut Session, + _notifier: Notifier, + _requester: &mut Requester, + _params: types::DidChangeConfigurationParams, + ) -> Result<()> { + // TODO(jane): get this wired up after the pre-release + Ok(()) + } +} diff --git a/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs new file mode 100644 index 00000000..85cf6ba4 --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs @@ -0,0 +1,32 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use crate::server::client::{Notifier, Requester}; +use crate::server::Result; +use crate::session::Session; +use lsp_types as types; +use lsp_types::notification as notif; + +pub(crate) struct DidChangeWatchedFiles; + +impl super::NotificationHandler for DidChangeWatchedFiles { + type NotificationType = notif::DidChangeWatchedFiles; +} + +impl super::SyncNotificationHandler for DidChangeWatchedFiles { + fn run( + session: &mut Session, + _notifier: Notifier, + _requester: &mut Requester, + params: types::DidChangeWatchedFilesParams, + ) -> Result<()> { + for change in ¶ms.changes { + session.reload_settings(&change.uri); + } + + Ok(()) + } +} diff --git a/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs b/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs new file mode 100644 index 00000000..39786a91 --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs @@ -0,0 +1,39 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use crate::server::api::LSPResult; +use crate::server::client::{Notifier, Requester}; +use crate::server::Result; +use crate::session::Session; +use lsp_types as types; +use lsp_types::notification as notif; + +pub(crate) struct DidChangeWorkspace; + +impl super::NotificationHandler for DidChangeWorkspace { + type NotificationType = notif::DidChangeWorkspaceFolders; +} + +impl super::SyncNotificationHandler for DidChangeWorkspace { + fn run( + session: &mut Session, + _notifier: Notifier, + _requester: &mut Requester, + params: types::DidChangeWorkspaceFoldersParams, + ) -> Result<()> { + for types::WorkspaceFolder { uri, .. } in params.event.added { + session + .open_workspace_folder(uri) + .with_failure_code(lsp_server::ErrorCode::InvalidParams)?; + } + for types::WorkspaceFolder { uri, .. } in params.event.removed { + session + .close_workspace_folder(&uri) + .with_failure_code(lsp_server::ErrorCode::InvalidParams)?; + } + Ok(()) + } +} diff --git a/crates/ruff_server/src/server/api/notifications/did_close.rs b/crates/ruff_server/src/server/api/notifications/did_close.rs new file mode 100644 index 00000000..1dc148c4 --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications/did_close.rs @@ -0,0 +1,35 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use crate::server::api::LSPResult; +use crate::server::client::{Notifier, Requester}; +use crate::server::Result; +use crate::session::Session; +use lsp_types as types; +use lsp_types::notification as notif; + +pub(crate) struct DidClose; + +impl super::NotificationHandler for DidClose { + type NotificationType = notif::DidCloseTextDocument; +} + +impl super::SyncNotificationHandler for DidClose { + fn run( + session: &mut Session, + _notifier: Notifier, + _requester: &mut Requester, + types::DidCloseTextDocumentParams { + text_document: types::TextDocumentIdentifier { uri }, + }: types::DidCloseTextDocumentParams, + ) -> Result<()> { + let key = session.key_from_url(uri); + + session + .close_document(&key) + .with_failure_code(lsp_server::ErrorCode::InternalError) + } +} diff --git a/crates/ruff_server/src/server/api/notifications/did_open.rs b/crates/ruff_server/src/server/api/notifications/did_open.rs new file mode 100644 index 00000000..fd8863b8 --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications/did_open.rs @@ -0,0 +1,41 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use crate::edit::TextDocument; +use crate::server::client::{Notifier, Requester}; +use crate::server::Result; +use crate::session::Session; +use lsp_types as types; +use lsp_types::notification as notif; + +pub(crate) struct DidOpen; + +impl super::NotificationHandler for DidOpen { + type NotificationType = notif::DidOpenTextDocument; +} + +impl super::SyncNotificationHandler for DidOpen { + fn run( + session: &mut Session, + _notifier: Notifier, + _requester: &mut Requester, + types::DidOpenTextDocumentParams { + text_document: + types::TextDocumentItem { + uri, + text, + version, + language_id, + }, + }: types::DidOpenTextDocumentParams, + ) -> Result<()> { + let document = TextDocument::new(text, version).with_language_id(&language_id); + + session.open_text_document(uri.clone(), document); + + Ok(()) + } +} diff --git a/crates/ruff_server/src/server/api/requests.rs b/crates/ruff_server/src/server/api/requests.rs new file mode 100644 index 00000000..a08a2edf --- /dev/null +++ b/crates/ruff_server/src/server/api/requests.rs @@ -0,0 +1,11 @@ +mod format; +mod format_range; + +use super::{ + define_document_url, + traits::{BackgroundDocumentRequestHandler, RequestHandler}, +}; +pub(super) use format::Format; +pub(super) use format_range::FormatRange; + +type FormatResponse = Option>; diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs new file mode 100644 index 00000000..d44a038b --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -0,0 +1,75 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use lsp_types::{self as types, request as req}; +use types::TextEdit; + +use ruff_source_file::LineIndex; + +use crate::edit::{PositionEncoding, Replacement, TextDocument, ToRangeExt}; +use crate::server::api::LSPResult; +use crate::server::{client::Notifier, Result}; +use crate::session::{DocumentQuery, DocumentSnapshot}; + +pub(crate) struct Format; + +impl super::RequestHandler for Format { + type RequestType = req::Formatting; +} + +impl super::BackgroundDocumentRequestHandler for Format { + super::define_document_url!(params: &types::DocumentFormattingParams); + fn run_with_snapshot( + snapshot: DocumentSnapshot, + _notifier: Notifier, + _params: types::DocumentFormattingParams, + ) -> Result { + format_document(&snapshot) + } +} + +/// Formats either a full text document or an specific notebook cell. If the query within the snapshot is a notebook document +/// with no selected cell, this will throw an error. +pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result { + let text_document = snapshot.query().as_single_document(); + let query = snapshot.query(); + format_text_document(text_document, query, snapshot.encoding()) +} + +fn format_text_document( + text_document: &TextDocument, + query: &DocumentQuery, + encoding: PositionEncoding, +) -> Result { + let document_settings = query.settings(); + let formatter_settings = &document_settings.format; + + let source = text_document.contents(); + + let formatted = crate::format::format(source, formatter_settings) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + let Some(formatted) = formatted else { + return Ok(None); + }; + + let unformatted_index = text_document.index(); + let formatted_index: LineIndex = LineIndex::from_source_text(&formatted); + + let Replacement { + source_range, + modified_range: formatted_range, + } = Replacement::between( + source, + unformatted_index.line_starts(), + &formatted, + formatted_index.line_starts(), + ); + + Ok(Some(vec![TextEdit { + range: source_range.to_range(source, unformatted_index, encoding), + new_text: formatted[formatted_range].to_owned(), + }])) +} diff --git a/crates/ruff_server/src/server/api/requests/format_range.rs b/crates/ruff_server/src/server/api/requests/format_range.rs new file mode 100644 index 00000000..7b5ebda6 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/format_range.rs @@ -0,0 +1,71 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use lsp_types::{self as types, request as req, Range}; + +use crate::edit::{PositionEncoding, TextDocument}; +use crate::server::{client::Notifier, Result}; +use crate::session::{DocumentQuery, DocumentSnapshot}; + +pub(crate) struct FormatRange; + +impl super::RequestHandler for FormatRange { + type RequestType = req::RangeFormatting; +} + +impl super::BackgroundDocumentRequestHandler for FormatRange { + super::define_document_url!(params: &types::DocumentRangeFormattingParams); + fn run_with_snapshot( + snapshot: DocumentSnapshot, + _notifier: Notifier, + params: types::DocumentRangeFormattingParams, + ) -> Result { + format_document_range(&snapshot, params.range) + } +} + +/// Formats the specified [`Range`] in the [`DocumentSnapshot`]. +fn format_document_range( + snapshot: &DocumentSnapshot, + range: Range, +) -> Result { + let text_document = snapshot.query().as_single_document(); + let query = snapshot.query(); + format_text_document_range(text_document, range, query, snapshot.encoding()) +} + +/// Formats the specified [`Range`] in the [`TextDocument`]. +fn format_text_document_range( + _text_document: &TextDocument, + _range: Range, + _query: &DocumentQuery, + _encoding: PositionEncoding, +) -> Result { + Ok(None) + // let document_settings = query.settings(); + // let formatter_settings = &document_settings.format; + // + // let text = text_document.contents(); + // let index = text_document.index(); + // let range = range.to_text_range(text, index, encoding); + // + // let formatted_range = crate::format::format_range( + // text_document, + // query.source_type(), + // formatter_settings, + // range, + // ) + // .with_failure_code(lsp_server::ErrorCode::InternalError)?; + // + // Ok(formatted_range.map(|formatted_range| { + // vec![types::TextEdit { + // range: formatted_range + // .source_range() + // .to_range(text, index, encoding), + // new_text: formatted_range.into_code(), + // }] + // })) +} diff --git a/crates/ruff_server/src/server/api/traits.rs b/crates/ruff_server/src/server/api/traits.rs new file mode 100644 index 00000000..d86889ad --- /dev/null +++ b/crates/ruff_server/src/server/api/traits.rs @@ -0,0 +1,84 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +//! A stateful LSP implementation that calls into the Ruff API. + +use crate::server::client::{Notifier, Requester}; +use crate::session::{DocumentSnapshot, Session}; + +use lsp_types::notification::Notification as LSPNotification; +use lsp_types::request::Request; + +/// A supertrait for any server request handler. +pub(super) trait RequestHandler { + type RequestType: Request; + const METHOD: &'static str = <::RequestType as Request>::METHOD; +} + +/// A request handler that needs mutable access to the session. +/// This will block the main message receiver loop, meaning that no +/// incoming requests or notifications will be handled while `run` is +/// executing. Try to avoid doing any I/O or long-running computations. +pub(super) trait SyncRequestHandler: RequestHandler { + fn run( + session: &mut Session, + notifier: Notifier, + requester: &mut Requester, + params: <::RequestType as Request>::Params, + ) -> super::Result<<::RequestType as Request>::Result>; +} + +/// A request handler that can be run on a background thread. +pub(super) trait BackgroundDocumentRequestHandler: RequestHandler { + /// `document_url` can be implemented automatically with + /// `define_document_url!(params: &)` in the trait + /// implementation. + fn document_url( + params: &<::RequestType as Request>::Params, + ) -> std::borrow::Cow; + + fn run_with_snapshot( + snapshot: DocumentSnapshot, + notifier: Notifier, + params: <::RequestType as Request>::Params, + ) -> super::Result<<::RequestType as Request>::Result>; +} + +/// A supertrait for any server notification handler. +pub(super) trait NotificationHandler { + type NotificationType: LSPNotification; + const METHOD: &'static str = + <::NotificationType as LSPNotification>::METHOD; +} + +/// A notification handler that needs mutable access to the session. +/// This will block the main message receiver loop, meaning that no +/// incoming requests or notifications will be handled while `run` is +/// executing. Try to avoid doing any I/O or long-running computations. +pub(super) trait SyncNotificationHandler: NotificationHandler { + fn run( + session: &mut Session, + notifier: Notifier, + requester: &mut Requester, + params: <::NotificationType as LSPNotification>::Params, + ) -> super::Result<()>; +} + +/// A notification handler that can be run on a background thread. +pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler { + /// `document_url` can be implemented automatically with + /// `define_document_url!(params: &)` in the trait + /// implementation. + fn document_url( + params: &<::NotificationType as LSPNotification>::Params, + ) -> std::borrow::Cow; + + fn run_with_snapshot( + snapshot: DocumentSnapshot, + notifier: Notifier, + params: <::NotificationType as LSPNotification>::Params, + ) -> super::Result<()>; +} diff --git a/crates/ruff_server/src/server/client.rs b/crates/ruff_server/src/server/client.rs new file mode 100644 index 00000000..6cb673f7 --- /dev/null +++ b/crates/ruff_server/src/server/client.rs @@ -0,0 +1,175 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use std::any::TypeId; + +use lsp_server::{Notification, RequestId}; +use rustc_hash::FxHashMap; +use serde_json::Value; + +use super::{schedule::Task, ClientSender}; + +type ResponseBuilder<'s> = Box Task<'s>>; + +pub(crate) struct Client<'s> { + notifier: Notifier, + responder: Responder, + pub(super) requester: Requester<'s>, +} + +#[derive(Clone)] +pub(crate) struct Notifier(ClientSender); + +#[derive(Clone)] +pub(crate) struct Responder(ClientSender); + +pub(crate) struct Requester<'s> { + sender: ClientSender, + next_request_id: i32, + response_handlers: FxHashMap>, +} + +impl Client<'_> { + pub(super) fn new(sender: ClientSender) -> Self { + Self { + notifier: Notifier(sender.clone()), + responder: Responder(sender.clone()), + requester: Requester { + sender, + next_request_id: 1, + response_handlers: FxHashMap::default(), + }, + } + } + + pub(super) fn notifier(&self) -> Notifier { + self.notifier.clone() + } + + pub(super) fn responder(&self) -> Responder { + self.responder.clone() + } +} + +#[allow(dead_code)] // we'll need to use `Notifier` in the future +impl Notifier { + pub(crate) fn notify(&self, params: N::Params) -> crate::Result<()> + where + N: lsp_types::notification::Notification, + { + let method = N::METHOD.to_string(); + + let message = lsp_server::Message::Notification(Notification::new(method, params)); + + self.0.send(message) + } + + pub(crate) fn notify_method(&self, method: String) -> crate::Result<()> { + self.0 + .send(lsp_server::Message::Notification(Notification::new( + method, + Value::Null, + ))) + } +} + +impl Responder { + pub(crate) fn respond( + &self, + id: RequestId, + result: crate::server::Result, + ) -> crate::Result<()> + where + R: serde::Serialize, + { + self.0.send( + match result { + Ok(res) => lsp_server::Response::new_ok(id, res), + Err(crate::server::api::Error { code, error }) => { + lsp_server::Response::new_err(id, code as i32, format!("{error}")) + } + } + .into(), + ) + } +} + +impl<'s> Requester<'s> { + /// Sends a request of kind `R` to the client, with associated parameters. + /// The task provided by `response_handler` will be dispatched as soon as the response + /// comes back from the client. + pub(crate) fn request( + &mut self, + params: R::Params, + response_handler: impl Fn(R::Result) -> Task<'s> + 'static, + ) -> crate::Result<()> + where + R: lsp_types::request::Request, + { + let serialized_params = serde_json::to_value(params)?; + + self.response_handlers.insert( + self.next_request_id.into(), + Box::new(move |response: lsp_server::Response| { + match (response.error, response.result) { + (Some(err), _) => { + tracing::error!( + "Got an error from the client (code {}): {}", + err.code, + err.message + ); + Task::nothing() + } + (None, Some(response)) => match serde_json::from_value(response) { + Ok(response) => response_handler(response), + Err(error) => { + tracing::error!("Failed to deserialize response from server: {error}"); + Task::nothing() + } + }, + (None, None) => { + if TypeId::of::() == TypeId::of::<()>() { + // We can't call `response_handler(())` directly here, but + // since we _know_ the type expected is `()`, we can use + // `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`, + // so this branch works in the general case but we'll only + // hit it if the concrete type is `()`, so the `unwrap()` is safe here. + response_handler(serde_json::from_value(Value::Null).unwrap()); + } else { + tracing::error!( + "Server response was invalid: did not contain a result or error" + ); + } + Task::nothing() + } + } + }), + ); + + self.sender + .send(lsp_server::Message::Request(lsp_server::Request { + id: self.next_request_id.into(), + method: R::METHOD.into(), + params: serialized_params, + }))?; + + self.next_request_id += 1; + + Ok(()) + } + + pub(crate) fn pop_response_task(&mut self, response: lsp_server::Response) -> Task<'s> { + if let Some(handler) = self.response_handlers.remove(&response.id) { + handler(response) + } else { + tracing::error!( + "Received a response with ID {}, which was not expected", + response.id + ); + Task::nothing() + } + } +} diff --git a/crates/ruff_server/src/server/connection.rs b/crates/ruff_server/src/server/connection.rs new file mode 100644 index 00000000..aa62de23 --- /dev/null +++ b/crates/ruff_server/src/server/connection.rs @@ -0,0 +1,150 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use lsp_server as lsp; +use lsp_types::{notification::Notification, request::Request}; +use std::sync::{Arc, Weak}; + +type ConnectionSender = crossbeam::channel::Sender; +type ConnectionReceiver = crossbeam::channel::Receiver; + +/// A builder for `Connection` that handles LSP initialization. +pub(crate) struct ConnectionInitializer { + connection: lsp::Connection, + threads: lsp::IoThreads, +} + +/// Handles inbound and outbound messages with the client. +pub(crate) struct Connection { + sender: Arc, + receiver: ConnectionReceiver, + threads: lsp::IoThreads, +} + +impl ConnectionInitializer { + /// Create a new LSP server connection over stdin/stdout. + pub(super) fn stdio() -> Self { + let (connection, threads) = lsp::Connection::stdio(); + Self { + connection, + threads, + } + } + + /// Starts the initialization process with the client by listening for an initialization request. + /// Returns a request ID that should be passed into `initialize_finish` later, + /// along with the initialization parameters that were provided. + pub(super) fn initialize_start( + &self, + ) -> crate::Result<(lsp::RequestId, lsp_types::InitializeParams)> { + let (id, params) = self.connection.initialize_start()?; + Ok((id, serde_json::from_value(params)?)) + } + + /// Finishes the initialization process with the client, + /// returning an initialized `Connection`. + pub(super) fn initialize_finish( + self, + id: lsp::RequestId, + server_capabilities: &lsp_types::ServerCapabilities, + name: &str, + version: &str, + ) -> crate::Result { + self.connection.initialize_finish( + id, + serde_json::json!({ + "capabilities": server_capabilities, + "serverInfo": { + "name": name, + "version": version + } + }), + )?; + let Self { + connection: lsp::Connection { sender, receiver }, + threads, + } = self; + Ok(Connection { + sender: Arc::new(sender), + receiver, + threads, + }) + } +} + +impl Connection { + /// Make a new `ClientSender` for sending messages to the client. + pub(super) fn make_sender(&self) -> ClientSender { + ClientSender { + weak_sender: Arc::downgrade(&self.sender), + } + } + + /// An iterator over incoming messages from the client. + pub(super) fn incoming(&self) -> crossbeam::channel::Iter { + self.receiver.iter() + } + + /// Check and respond to any incoming shutdown requests; returns`true` if the server should be shutdown. + pub(super) fn handle_shutdown(&self, message: &lsp::Message) -> crate::Result { + match message { + lsp::Message::Request(lsp::Request { id, method, .. }) + if method == lsp_types::request::Shutdown::METHOD => + { + self.sender + .send(lsp::Response::new_ok(id.clone(), ()).into())?; + tracing::info!("Shutdown request received. Waiting for an exit notification..."); + match self.receiver.recv_timeout(std::time::Duration::from_secs(30))? { + lsp::Message::Notification(lsp::Notification { method, .. }) if method == lsp_types::notification::Exit::METHOD => { + tracing::info!("Exit notification received. Server shutting down..."); + Ok(true) + }, + message => anyhow::bail!("Server received unexpected message {message:?} while waiting for exit notification") + } + } + lsp::Message::Notification(lsp::Notification { method, .. }) + if method == lsp_types::notification::Exit::METHOD => + { + tracing::error!("Server received an exit notification before a shutdown request was sent. Exiting..."); + Ok(true) + } + _ => Ok(false), + } + } + + /// Join the I/O threads that underpin this connection. + /// This is guaranteed to be nearly immediate since + /// we close the only active channels to these threads prior + /// to joining them. + pub(super) fn close(self) -> crate::Result<()> { + std::mem::drop( + Arc::into_inner(self.sender) + .expect("the client sender shouldn't have more than one strong reference"), + ); + std::mem::drop(self.receiver); + self.threads.join()?; + Ok(()) + } +} + +/// A weak reference to an underlying sender channel, used for communication with the client. +/// If the `Connection` that created this `ClientSender` is dropped, any `send` calls will throw +/// an error. +#[derive(Clone, Debug)] +pub(crate) struct ClientSender { + weak_sender: Weak, +} + +// note: additional wrapper functions for senders may be implemented as needed. +impl ClientSender { + pub(crate) fn send(&self, msg: lsp::Message) -> crate::Result<()> { + let Some(sender) = self.weak_sender.upgrade() else { + anyhow::bail!("The connection with the client has been closed"); + }; + + Ok(sender.send(msg)?) + } +} diff --git a/crates/ruff_server/src/server/schedule.rs b/crates/ruff_server/src/server/schedule.rs new file mode 100644 index 00000000..e79965c9 --- /dev/null +++ b/crates/ruff_server/src/server/schedule.rs @@ -0,0 +1,118 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use std::num::NonZeroUsize; + +use crate::session::Session; + +mod task; +mod thread; + +pub(super) use task::{BackgroundSchedule, Task}; + +use self::{ + task::{BackgroundTaskBuilder, SyncTask}, + thread::ThreadPriority, +}; + +use super::{client::Client, ClientSender}; + +/// The event loop thread is actually a secondary thread that we spawn from the +/// _actual_ main thread. This secondary thread has a larger stack size +/// than some OS defaults (Windows, for example) and is also designated as +/// high-priority. +pub(crate) fn event_loop_thread( + func: impl FnOnce() -> crate::Result<()> + Send + 'static, +) -> crate::Result>> { + // Override OS defaults to avoid stack overflows on platforms with low stack size defaults. + const MAIN_THREAD_STACK_SIZE: usize = 2 * 1024 * 1024; + const MAIN_THREAD_NAME: &str = "ruff:main"; + Ok( + thread::Builder::new(thread::ThreadPriority::LatencySensitive) + .name(MAIN_THREAD_NAME.into()) + .stack_size(MAIN_THREAD_STACK_SIZE) + .spawn(func)?, + ) +} + +pub(crate) struct Scheduler<'s> { + session: &'s mut Session, + client: Client<'s>, + fmt_pool: thread::Pool, + background_pool: thread::Pool, +} + +impl<'s> Scheduler<'s> { + pub(super) fn new( + session: &'s mut Session, + worker_threads: NonZeroUsize, + sender: ClientSender, + ) -> Self { + const FMT_THREADS: usize = 1; + Self { + session, + fmt_pool: thread::Pool::new(NonZeroUsize::try_from(FMT_THREADS).unwrap()), + background_pool: thread::Pool::new(worker_threads), + client: Client::new(sender), + } + } + + /// Immediately sends a request of kind `R` to the client, with associated parameters. + /// The task provided by `response_handler` will be dispatched as soon as the response + /// comes back from the client. + pub(super) fn request( + &mut self, + params: R::Params, + response_handler: impl Fn(R::Result) -> Task<'s> + 'static, + ) -> crate::Result<()> + where + R: lsp_types::request::Request, + { + self.client.requester.request::(params, response_handler) + } + + /// Creates a task to handle a response from the client. + pub(super) fn response(&mut self, response: lsp_server::Response) -> Task<'s> { + self.client.requester.pop_response_task(response) + } + + /// Dispatches a `task` by either running it as a blocking function or + /// executing it on a background thread pool. + pub(super) fn dispatch(&mut self, task: task::Task<'s>) { + match task { + Task::Sync(SyncTask { func }) => { + let notifier = self.client.notifier(); + let responder = self.client.responder(); + func( + self.session, + notifier, + &mut self.client.requester, + responder, + ); + } + Task::Background(BackgroundTaskBuilder { + schedule, + builder: func, + }) => { + let static_func = func(self.session); + let notifier = self.client.notifier(); + let responder = self.client.responder(); + let task = move || static_func(notifier, responder); + match schedule { + BackgroundSchedule::Worker => { + self.background_pool.spawn(ThreadPriority::Worker, task); + } + BackgroundSchedule::LatencySensitive => self + .background_pool + .spawn(ThreadPriority::LatencySensitive, task), + BackgroundSchedule::Fmt => { + self.fmt_pool.spawn(ThreadPriority::LatencySensitive, task); + } + } + } + } + } +} diff --git a/crates/ruff_server/src/server/schedule/task.rs b/crates/ruff_server/src/server/schedule/task.rs new file mode 100644 index 00000000..f9e1905a --- /dev/null +++ b/crates/ruff_server/src/server/schedule/task.rs @@ -0,0 +1,103 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use lsp_server::RequestId; +use serde::Serialize; + +use crate::{ + server::client::{Notifier, Requester, Responder}, + session::Session, +}; + +type LocalFn<'s> = Box; + +type BackgroundFn = Box; + +type BackgroundFnBuilder<'s> = Box BackgroundFn + 's>; + +/// Describes how the task should be run. +#[derive(Clone, Copy, Debug, Default)] +pub(in crate::server) enum BackgroundSchedule { + /// The task should be run on the background thread designated + /// for formatting actions. This is a high priority thread. + Fmt, + /// The task should be run on the general high-priority background + /// thread. + // TODO: Remove once we have some latency sensitive background request + #[allow(dead_code)] + LatencySensitive, + /// The task should be run on a regular-priority background thread. + #[default] + Worker, +} + +/// A [`Task`] is a future that has not yet started, and it is the job of +/// the [`super::Scheduler`] to make that happen, via [`super::Scheduler::dispatch`]. +/// A task can either run on the main thread (in other words, the same thread as the +/// scheduler) or it can run in a background thread. The main difference between +/// the two is that background threads only have a read-only snapshot of the session, +/// while local tasks have exclusive access and can modify it as they please. Keep in mind that +/// local tasks will **block** the main event loop, so only use local tasks if you **need** +/// mutable state access or you need the absolute lowest latency possible. +pub(in crate::server) enum Task<'s> { + Background(BackgroundTaskBuilder<'s>), + Sync(SyncTask<'s>), +} + +// The reason why this isn't just a 'static background closure +// is because we need to take a snapshot of the session before sending +// this task to the background, and the inner closure can't take the session +// as an immutable reference since it's used mutably elsewhere. So instead, +// a background task is built using an outer closure that borrows the session to take a snapshot, +// that the inner closure can capture. This builder closure has a lifetime linked to the scheduler. +// When the task is dispatched, the scheduler runs the synchronous builder, which takes the session +// as a reference, to create the inner 'static closure. That closure is then moved to a background task pool. +pub(in crate::server) struct BackgroundTaskBuilder<'s> { + pub(super) schedule: BackgroundSchedule, + pub(super) builder: BackgroundFnBuilder<'s>, +} + +pub(in crate::server) struct SyncTask<'s> { + pub(super) func: LocalFn<'s>, +} + +impl<'s> Task<'s> { + /// Creates a new background task. + pub(crate) fn background( + schedule: BackgroundSchedule, + func: impl FnOnce(&Session) -> Box + 's, + ) -> Self { + Self::Background(BackgroundTaskBuilder { + schedule, + builder: Box::new(func), + }) + } + /// Creates a new local task. + pub(crate) fn local( + func: impl FnOnce(&mut Session, Notifier, &mut Requester, Responder) + 's, + ) -> Self { + Self::Sync(SyncTask { + func: Box::new(func), + }) + } + /// Creates a local task that immediately + /// responds with the provided `request`. + pub(crate) fn immediate(id: RequestId, result: crate::server::Result) -> Self + where + R: Serialize + Send + 'static, + { + Self::local(move |_, _, _, responder| { + if let Err(err) = responder.respond(id, result) { + tracing::error!("Unable to send immediate response: {err}"); + } + }) + } + + /// Creates a local task that does nothing. + pub(crate) fn nothing() -> Self { + Self::local(move |_, _, _, _| {}) + } +} diff --git a/crates/ruff_server/src/server/schedule/thread.rs b/crates/ruff_server/src/server/schedule/thread.rs new file mode 100644 index 00000000..da3ea8c2 --- /dev/null +++ b/crates/ruff_server/src/server/schedule/thread.rs @@ -0,0 +1,109 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/rust-lang/rust-analyzer.git | +// | File: `crates/stdx/src/thread.rs` | +// | Commit: 03b3cb6be9f21c082f4206b35c7fe7f291c94eaa | +// +------------------------------------------------------------+ +//! A utility module for working with threads that automatically joins threads upon drop +//! and abstracts over operating system quality of service (QoS) APIs +//! through the concept of a “thread priority”. +//! +//! The priority of a thread is frozen at thread creation time, +//! i.e. there is no API to change the priority of a thread once it has been spawned. +//! +//! As a system, rust-analyzer should have the property that +//! old manual scheduling APIs are replaced entirely by QoS. +//! To maintain this invariant, we panic when it is clear that +//! old scheduling APIs have been used. +//! +//! Moreover, we also want to ensure that every thread has an priority set explicitly +//! to force a decision about its importance to the system. +//! Thus, [`ThreadPriority`] has no default value +//! and every entry point to creating a thread requires a [`ThreadPriority`] upfront. + +// Keeps us from getting warnings about the word `QoS` +#![allow(clippy::doc_markdown)] + +use std::fmt; + +mod pool; +mod priority; + +pub(super) use pool::Pool; +pub(super) use priority::ThreadPriority; + +pub(super) struct Builder { + priority: ThreadPriority, + inner: jod_thread::Builder, +} + +impl Builder { + pub(super) fn new(priority: ThreadPriority) -> Builder { + Builder { + priority, + inner: jod_thread::Builder::new(), + } + } + + pub(super) fn name(self, name: String) -> Builder { + Builder { + inner: self.inner.name(name), + ..self + } + } + + pub(super) fn stack_size(self, size: usize) -> Builder { + Builder { + inner: self.inner.stack_size(size), + ..self + } + } + + pub(super) fn spawn(self, f: F) -> std::io::Result> + where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, + { + let inner_handle = self.inner.spawn(move || { + self.priority.apply_to_current_thread(); + f() + })?; + + Ok(JoinHandle { + inner: Some(inner_handle), + allow_leak: false, + }) + } +} + +pub(crate) struct JoinHandle { + // `inner` is an `Option` so that we can + // take ownership of the contained `JoinHandle`. + inner: Option>, + allow_leak: bool, +} + +impl JoinHandle { + pub(crate) fn join(mut self) -> T { + self.inner.take().unwrap().join() + } +} + +impl Drop for JoinHandle { + fn drop(&mut self) { + if !self.allow_leak { + return; + } + + if let Some(join_handle) = self.inner.take() { + join_handle.detach(); + } + } +} + +impl fmt::Debug for JoinHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("JoinHandle { .. }") + } +} diff --git a/crates/ruff_server/src/server/schedule/thread/pool.rs b/crates/ruff_server/src/server/schedule/thread/pool.rs new file mode 100644 index 00000000..ea654a11 --- /dev/null +++ b/crates/ruff_server/src/server/schedule/thread/pool.rs @@ -0,0 +1,113 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/rust-lang/rust-analyzer.git | +// | File: `crates/stdx/src/thread/pool.rs` | +// | Commit: 03b3cb6be9f21c082f4206b35c7fe7f291c94eaa | +// +------------------------------------------------------------+ +//! [`Pool`] implements a basic custom thread pool +//! inspired by the [`threadpool` crate](http://docs.rs/threadpool). +//! When you spawn a task you specify a thread priority +//! so the pool can schedule it to run on a thread with that priority. +//! rust-analyzer uses this to prioritize work based on latency requirements. +//! +//! The thread pool is implemented entirely using +//! the threading utilities in [`crate::server::schedule::thread`]. + +use std::{ + num::NonZeroUsize, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; + +use crossbeam::channel::{Receiver, Sender}; + +use super::{Builder, JoinHandle, ThreadPriority}; + +pub(crate) struct Pool { + // `_handles` is never read: the field is present + // only for its `Drop` impl. + + // The worker threads exit once the channel closes; + // make sure to keep `job_sender` above `handles` + // so that the channel is actually closed + // before we join the worker threads! + job_sender: Sender, + _handles: Vec, + extant_tasks: Arc, +} + +struct Job { + requested_priority: ThreadPriority, + f: Box, +} + +impl Pool { + pub(crate) fn new(threads: NonZeroUsize) -> Pool { + // Override OS defaults to avoid stack overflows on platforms with low stack size defaults. + const STACK_SIZE: usize = 2 * 1024 * 1024; + const INITIAL_PRIORITY: ThreadPriority = ThreadPriority::Worker; + + let threads = usize::from(threads); + + // Channel buffer capacity is between 2 and 4, depending on the pool size. + let (job_sender, job_receiver) = crossbeam::channel::bounded(std::cmp::min(threads * 2, 4)); + let extant_tasks = Arc::new(AtomicUsize::new(0)); + + let mut handles = Vec::with_capacity(threads); + for i in 0..threads { + let handle = Builder::new(INITIAL_PRIORITY) + .stack_size(STACK_SIZE) + .name(format!("ruff:worker:{i}")) + .spawn({ + let extant_tasks = Arc::clone(&extant_tasks); + let job_receiver: Receiver = job_receiver.clone(); + move || { + let mut current_priority = INITIAL_PRIORITY; + for job in job_receiver { + if job.requested_priority != current_priority { + job.requested_priority.apply_to_current_thread(); + current_priority = job.requested_priority; + } + extant_tasks.fetch_add(1, Ordering::SeqCst); + (job.f)(); + extant_tasks.fetch_sub(1, Ordering::SeqCst); + } + } + }) + .expect("failed to spawn thread"); + + handles.push(handle); + } + + Pool { + _handles: handles, + extant_tasks, + job_sender, + } + } + + pub(crate) fn spawn(&self, priority: ThreadPriority, f: F) + where + F: FnOnce() + Send + 'static, + { + let f = Box::new(move || { + if cfg!(debug_assertions) { + priority.assert_is_used_on_current_thread(); + } + f(); + }); + + let job = Job { + requested_priority: priority, + f, + }; + self.job_sender.send(job).unwrap(); + } + + #[allow(dead_code)] + pub(super) fn len(&self) -> usize { + self.extant_tasks.load(Ordering::SeqCst) + } +} diff --git a/crates/ruff_server/src/server/schedule/thread/priority.rs b/crates/ruff_server/src/server/schedule/thread/priority.rs new file mode 100644 index 00000000..e6a55524 --- /dev/null +++ b/crates/ruff_server/src/server/schedule/thread/priority.rs @@ -0,0 +1,297 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/rust-lang/rust-analyzer.git | +// | File: `crates/stdx/src/thread/intent.rs` | +// | Commit: 03b3cb6be9f21c082f4206b35c7fe7f291c94eaa | +// +------------------------------------------------------------+ +//! An opaque façade around platform-specific QoS APIs. + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +// Please maintain order from least to most priority for the derived `Ord` impl. +pub(crate) enum ThreadPriority { + /// Any thread which does work that isn't in a critical path. + Worker, + + /// Any thread which does work caused by the user typing, or + /// work that the editor may wait on. + LatencySensitive, +} + +impl ThreadPriority { + // These APIs must remain private; + // we only want consumers to set thread priority + // during thread creation. + + pub(crate) fn apply_to_current_thread(self) { + let class = thread_priority_to_qos_class(self); + set_current_thread_qos_class(class); + } + + pub(crate) fn assert_is_used_on_current_thread(self) { + if IS_QOS_AVAILABLE { + let class = thread_priority_to_qos_class(self); + assert_eq!(get_current_thread_qos_class(), Some(class)); + } + } +} + +use imp::QoSClass; + +const IS_QOS_AVAILABLE: bool = imp::IS_QOS_AVAILABLE; + +fn set_current_thread_qos_class(class: QoSClass) { + imp::set_current_thread_qos_class(class); +} + +fn get_current_thread_qos_class() -> Option { + imp::get_current_thread_qos_class() +} + +fn thread_priority_to_qos_class(priority: ThreadPriority) -> QoSClass { + imp::thread_priority_to_qos_class(priority) +} + +// All Apple platforms use XNU as their kernel +// and thus have the concept of QoS. +#[cfg(target_vendor = "apple")] +mod imp { + use super::ThreadPriority; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + // Please maintain order from least to most priority for the derived `Ord` impl. + pub(super) enum QoSClass { + // Documentation adapted from https://github.com/apple-oss-distributions/libpthread/blob/67e155c94093be9a204b69637d198eceff2c7c46/include/sys/qos.h#L55 + // + /// TLDR: invisible maintenance tasks + /// + /// Contract: + /// + /// * **You do not care about how long it takes for work to finish.** + /// * **You do not care about work being deferred temporarily.** + /// (e.g. if the device's battery is in a critical state) + /// + /// Examples: + /// + /// * in a video editor: + /// creating periodic backups of project files + /// * in a browser: + /// cleaning up cached sites which have not been accessed in a long time + /// * in a collaborative word processor: + /// creating a searchable index of all documents + /// + /// Use this QoS class for background tasks + /// which the user did not initiate themselves + /// and which are invisible to the user. + /// It is expected that this work will take significant time to complete: + /// minutes or even hours. + /// + /// This QoS class provides the most energy and thermally-efficient execution possible. + /// All other work is prioritized over background tasks. + Background, + + /// TLDR: tasks that don't block using your app + /// + /// Contract: + /// + /// * **Your app remains useful even as the task is executing.** + /// + /// Examples: + /// + /// * in a video editor: + /// exporting a video to disk - + /// the user can still work on the timeline + /// * in a browser: + /// automatically extracting a downloaded zip file - + /// the user can still switch tabs + /// * in a collaborative word processor: + /// downloading images embedded in a document - + /// the user can still make edits + /// + /// Use this QoS class for tasks which + /// may or may not be initiated by the user, + /// but whose result is visible. + /// It is expected that this work will take a few seconds to a few minutes. + /// Typically your app will include a progress bar + /// for tasks using this class. + /// + /// This QoS class provides a balance between + /// performance, responsiveness and efficiency. + Utility, + + /// TLDR: tasks that block using your app + /// + /// Contract: + /// + /// * **You need this work to complete + /// before the user can keep interacting with your app.** + /// * **Your work will not take more than a few seconds to complete.** + /// + /// Examples: + /// + /// * in a video editor: + /// opening a saved project + /// * in a browser: + /// loading a list of the user's bookmarks and top sites + /// when a new tab is created + /// * in a collaborative word processor: + /// running a search on the document's content + /// + /// Use this QoS class for tasks which were initiated by the user + /// and block the usage of your app while they are in progress. + /// It is expected that this work will take a few seconds or less to complete; + /// not long enough to cause the user to switch to something else. + /// Your app will likely indicate progress on these tasks + /// through the display of placeholder content or modals. + /// + /// This QoS class is not energy-efficient. + /// Rather, it provides responsiveness + /// by prioritizing work above other tasks on the system + /// except for critical user-interactive work. + UserInitiated, + + /// TLDR: render loops and nothing else + /// + /// Contract: + /// + /// * **You absolutely need this work to complete immediately + /// or your app will appear to freeze.** + /// * **Your work will always complete virtually instantaneously.** + /// + /// Examples: + /// + /// * the main thread in a GUI application + /// * the update & render loop in a game + /// * a secondary thread which progresses an animation + /// + /// Use this QoS class for any work which, if delayed, + /// will make your user interface unresponsive. + /// It is expected that this work will be virtually instantaneous. + /// + /// This QoS class is not energy-efficient. + /// Specifying this class is a request to run with + /// nearly all available system CPU and I/O bandwidth even under contention. + UserInteractive, + } + + pub(super) const IS_QOS_AVAILABLE: bool = true; + + pub(super) fn set_current_thread_qos_class(class: QoSClass) { + let c = match class { + QoSClass::UserInteractive => libc::qos_class_t::QOS_CLASS_USER_INTERACTIVE, + QoSClass::UserInitiated => libc::qos_class_t::QOS_CLASS_USER_INITIATED, + QoSClass::Utility => libc::qos_class_t::QOS_CLASS_UTILITY, + QoSClass::Background => libc::qos_class_t::QOS_CLASS_BACKGROUND, + }; + + #[allow(unsafe_code)] + let code = unsafe { libc::pthread_set_qos_class_self_np(c, 0) }; + + if code == 0 { + return; + } + + #[allow(unsafe_code)] + let errno = unsafe { *libc::__error() }; + + match errno { + libc::EPERM => { + // This thread has been excluded from the QoS system + // due to a previous call to a function such as `pthread_setschedparam` + // which is incompatible with QoS. + // + // Panic instead of returning an error + // to maintain the invariant that we only use QoS APIs. + panic!("tried to set QoS of thread which has opted out of QoS (os error {errno})") + } + + libc::EINVAL => { + // This is returned if we pass something other than a qos_class_t + // to `pthread_set_qos_class_self_np`. + // + // This is impossible, so again panic. + unreachable!( + "invalid qos_class_t value was passed to pthread_set_qos_class_self_np" + ) + } + + _ => { + // `pthread_set_qos_class_self_np`’s documentation + // does not mention any other errors. + unreachable!("`pthread_set_qos_class_self_np` returned unexpected error {errno}") + } + } + } + + pub(super) fn get_current_thread_qos_class() -> Option { + #[allow(unsafe_code)] + let current_thread = unsafe { libc::pthread_self() }; + let mut qos_class_raw = libc::qos_class_t::QOS_CLASS_UNSPECIFIED; + #[allow(unsafe_code)] + let code = unsafe { + libc::pthread_get_qos_class_np(current_thread, &mut qos_class_raw, std::ptr::null_mut()) + }; + + if code != 0 { + // `pthread_get_qos_class_np`’s documentation states that + // an error value is placed into errno if the return code is not zero. + // However, it never states what errors are possible. + // Inspecting the source[0] shows that, as of this writing, it always returns zero. + // + // Whatever errors the function could report in future are likely to be + // ones which we cannot handle anyway + // + // 0: https://github.com/apple-oss-distributions/libpthread/blob/67e155c94093be9a204b69637d198eceff2c7c46/src/qos.c#L171-L177 + #[allow(unsafe_code)] + let errno = unsafe { *libc::__error() }; + unreachable!("`pthread_get_qos_class_np` failed unexpectedly (os error {errno})"); + } + + match qos_class_raw { + libc::qos_class_t::QOS_CLASS_USER_INTERACTIVE => Some(QoSClass::UserInteractive), + libc::qos_class_t::QOS_CLASS_USER_INITIATED => Some(QoSClass::UserInitiated), + libc::qos_class_t::QOS_CLASS_DEFAULT => None, // QoS has never been set + libc::qos_class_t::QOS_CLASS_UTILITY => Some(QoSClass::Utility), + libc::qos_class_t::QOS_CLASS_BACKGROUND => Some(QoSClass::Background), + + libc::qos_class_t::QOS_CLASS_UNSPECIFIED => { + // Using manual scheduling APIs causes threads to “opt out” of QoS. + // At this point they become incompatible with QoS, + // and as such have the “unspecified” QoS class. + // + // Panic instead of returning an error + // to maintain the invariant that we only use QoS APIs. + panic!("tried to get QoS of thread which has opted out of QoS") + } + } + } + + pub(super) fn thread_priority_to_qos_class(priority: ThreadPriority) -> QoSClass { + match priority { + ThreadPriority::Worker => QoSClass::Utility, + ThreadPriority::LatencySensitive => QoSClass::UserInitiated, + } + } +} + +// FIXME: Windows has QoS APIs, we should use them! +#[cfg(not(target_vendor = "apple"))] +mod imp { + use super::ThreadPriority; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + pub(super) enum QoSClass { + Default, + } + + pub(super) const IS_QOS_AVAILABLE: bool = false; + + pub(super) fn set_current_thread_qos_class(_: QoSClass) {} + + pub(super) fn get_current_thread_qos_class() -> Option { + None + } + + pub(super) fn thread_priority_to_qos_class(_: ThreadPriority) -> QoSClass { + QoSClass::Default + } +} diff --git a/crates/ruff_server/src/session.rs b/crates/ruff_server/src/session.rs new file mode 100644 index 00000000..dd8e86ab --- /dev/null +++ b/crates/ruff_server/src/session.rs @@ -0,0 +1,152 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +//! Data model, state management, and configuration resolution. + +use std::sync::Arc; + +use lsp_types::Url; +use lsp_types::WorkspaceFolder; + +use crate::edit::{DocumentKey, DocumentVersion, PositionEncoding, TextDocument}; + +pub(crate) use self::capabilities::ResolvedClientCapabilities; +pub use self::index::DocumentQuery; + +mod capabilities; +mod index; +mod workspaces; + +/// The global state for the LSP +pub(crate) struct Session { + /// Used to retrieve information about open documents and settings. + index: index::Index, + /// The global position encoding, negotiated during LSP initialization. + position_encoding: PositionEncoding, + /// Tracks what LSP features the client supports and doesn't support. + resolved_client_capabilities: Arc, +} + +/// An immutable snapshot of `Session` that references +/// a specific document. +pub(crate) struct DocumentSnapshot { + #[allow(dead_code)] + resolved_client_capabilities: Arc, + document_ref: index::DocumentQuery, + position_encoding: PositionEncoding, +} + +impl Session { + pub(crate) fn new( + resolved_client_capabilities: ResolvedClientCapabilities, + position_encoding: PositionEncoding, + workspace_folders: Vec, + ) -> crate::Result { + Ok(Self { + position_encoding, + index: index::Index::new(workspace_folders)?, + resolved_client_capabilities: Arc::new(resolved_client_capabilities), + }) + } + + pub(crate) fn key_from_url(&self, url: Url) -> DocumentKey { + self.index.key_from_url(url) + } + + /// Creates a document snapshot with the URL referencing the document to snapshot. + pub(crate) fn take_snapshot(&self, url: Url) -> Option { + let key = self.key_from_url(url); + Some(DocumentSnapshot { + resolved_client_capabilities: self.resolved_client_capabilities.clone(), + document_ref: self.index.make_document_ref(key)?, + position_encoding: self.position_encoding, + }) + } + + /// Iterates over the LSP URLs for all open text documents. These URLs are valid file paths. + #[allow(dead_code)] + pub(crate) fn text_document_urls(&self) -> impl Iterator + '_ { + self.index.text_document_urls() + } + + /// Updates a text document at the associated `key`. + /// + /// The document key must point to a text document, or this will throw an error. + pub(crate) fn update_text_document( + &mut self, + key: &DocumentKey, + content_changes: Vec, + new_version: DocumentVersion, + ) -> crate::Result<()> { + let encoding = self.encoding(); + + self.index + .update_text_document(key, content_changes, new_version, encoding) + } + + /// Registers a text document at the provided `url`. + /// If a document is already open here, it will be overwritten. + pub(crate) fn open_text_document(&mut self, url: Url, document: TextDocument) { + self.index.open_text_document(url, document); + } + + /// De-registers a document, specified by its key. + /// Calling this multiple times for the same document is a logic error. + pub(crate) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { + self.index.close_document(key)?; + Ok(()) + } + + /// Reloads the settings index + pub(crate) fn reload_settings(&mut self, changed_url: &Url) { + self.index.reload_settings(changed_url); + } + + /// Open a workspace folder at the given `url`. + pub(crate) fn open_workspace_folder(&mut self, url: Url) -> crate::Result<()> { + self.index.open_workspace_folder(url) + } + + /// Close a workspace folder at the given `url`. + pub(crate) fn close_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { + self.index.close_workspace_folder(url)?; + Ok(()) + } + + #[allow(dead_code)] + pub(crate) fn num_documents(&self) -> usize { + self.index.num_documents() + } + + #[allow(dead_code)] + pub(crate) fn num_workspaces(&self) -> usize { + self.index.num_workspaces() + } + + #[allow(dead_code)] + pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities { + &self.resolved_client_capabilities + } + + pub(crate) fn encoding(&self) -> PositionEncoding { + self.position_encoding + } +} + +impl DocumentSnapshot { + #[allow(dead_code)] + pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities { + &self.resolved_client_capabilities + } + + pub fn query(&self) -> &index::DocumentQuery { + &self.document_ref + } + + pub(crate) fn encoding(&self) -> PositionEncoding { + self.position_encoding + } +} diff --git a/crates/ruff_server/src/session/capabilities.rs b/crates/ruff_server/src/session/capabilities.rs new file mode 100644 index 00000000..5b2309a2 --- /dev/null +++ b/crates/ruff_server/src/session/capabilities.rs @@ -0,0 +1,41 @@ +use lsp_types::ClientCapabilities; +use lsp_types::PositionEncodingKind; + +/// A resolved representation of the [ClientCapabilities] the Client sends over that we +/// actually do something with +#[derive(Debug, Default, Clone)] +pub(crate) struct ResolvedClientCapabilities { + pub(crate) position_encodings: Vec, + #[allow(dead_code)] + pub(crate) dynamic_registration_for_did_change_configuration: bool, + pub(crate) dynamic_registration_for_did_change_watched_files: bool, +} + +impl ResolvedClientCapabilities { + pub(crate) fn new(capabilities: ClientCapabilities) -> Self { + let position_encodings = capabilities + .general + .and_then(|general_client_capabilities| general_client_capabilities.position_encodings) + .unwrap_or(vec![PositionEncodingKind::UTF16]); + + let dynamic_registration_for_did_change_configuration = capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.did_change_configuration) + .and_then(|did_change_configuration| did_change_configuration.dynamic_registration) + .unwrap_or(false); + + let dynamic_registration_for_did_change_watched_files = capabilities + .workspace + .as_ref() + .and_then(|workspace| workspace.did_change_watched_files) + .and_then(|watched_files| watched_files.dynamic_registration) + .unwrap_or_default(); + + Self { + position_encodings, + dynamic_registration_for_did_change_configuration, + dynamic_registration_for_did_change_watched_files, + } + } +} diff --git a/crates/ruff_server/src/session/index.rs b/crates/ruff_server/src/session/index.rs new file mode 100644 index 00000000..c9e3a06e --- /dev/null +++ b/crates/ruff_server/src/session/index.rs @@ -0,0 +1,275 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use std::borrow::Cow; +use std::path::PathBuf; +use std::{path::Path, sync::Arc}; + +use lsp_types::Url; +use lsp_types::WorkspaceFolder; +use rustc_hash::FxHashMap; + +use workspace::settings::Settings; + +use crate::edit::LanguageId; +use crate::edit::{DocumentKey, DocumentVersion, PositionEncoding, TextDocument}; +use crate::session::workspaces::WorkspaceSettingsResolver; + +/// Stores and tracks all open documents in a session, along with their associated settings. +#[derive(Default)] +pub(crate) struct Index { + /// Maps all document file URLs to the associated document controller + documents: FxHashMap, + + /// Maps a workspace folder root to its settings. + settings: WorkspaceSettingsResolver, +} + +/// A mutable handler to an underlying document. +#[derive(Debug)] +enum DocumentController { + Text(Arc), +} + +/// A read-only query to an open document. +/// This query can 'select' a text document, full notebook, or a specific notebook cell. +/// It also includes document settings. +#[derive(Clone)] +pub enum DocumentQuery { + Text { + file_url: Url, + document: Arc, + // TODO: This should be `Arc` + settings: Settings, + }, +} + +impl Index { + pub(super) fn new(workspace_folders: Vec) -> crate::Result { + Ok(Self { + documents: FxHashMap::default(), + settings: WorkspaceSettingsResolver::from_workspace_folders(workspace_folders), + }) + } + + pub(super) fn text_document_urls(&self) -> impl Iterator + '_ { + self.documents + .iter() + .filter(|(_, doc)| doc.as_text().is_some()) + .map(|(url, _)| url) + } + + pub(super) fn update_text_document( + &mut self, + key: &DocumentKey, + content_changes: Vec, + new_version: DocumentVersion, + encoding: PositionEncoding, + ) -> crate::Result<()> { + let controller = self.document_controller_for_key(key)?; + let Some(document) = controller.as_text_mut() else { + anyhow::bail!("Text document URI does not point to a text document"); + }; + + if content_changes.is_empty() { + document.update_version(new_version); + return Ok(()); + } + + document.apply_changes(content_changes, new_version, encoding); + Ok(()) + } + + pub(super) fn key_from_url(&self, url: Url) -> DocumentKey { + DocumentKey::Text(url) + } + + pub(super) fn open_workspace_folder(&mut self, url: Url) -> crate::Result<()> { + self.settings.open_workspace_folder(&url) + } + + #[allow(dead_code)] + pub(super) fn num_documents(&self) -> usize { + self.documents.len() + } + + #[allow(dead_code)] + pub(super) fn num_workspaces(&self) -> usize { + self.settings.len() + } + + pub(super) fn close_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { + self.settings.close_workspace_folder(url)?; + Ok(()) + } + + pub(super) fn make_document_ref(&self, key: DocumentKey) -> Option { + let url = self.url_for_key(&key)?.clone(); + + let document_settings = self.settings_for_url(&url).clone(); + + let controller = self.documents.get(&url)?; + Some(controller.make_ref(url, document_settings)) + } + + /// Reloads relevant existing settings files based on a changed settings file path. + pub(super) fn reload_settings(&mut self, changed_url: &Url) { + self.settings.reload_workspaces_matched_by_url(changed_url); + } + + pub(super) fn open_text_document(&mut self, url: Url, document: TextDocument) { + self.documents + .insert(url, DocumentController::new_text(document)); + } + + pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { + let Some(url) = self.url_for_key(key).cloned() else { + anyhow::bail!("Tried to close unavailable document `{key}`"); + }; + + let Some(_) = self.documents.remove(&url) else { + anyhow::bail!("tried to close document that didn't exist at {}", url) + }; + Ok(()) + } + + // TODO: Index should manage per workspace client settings at some point once we have some + // pub(super) fn client_settings( + // &self, + // key: &DocumentKey, + // global_settings: &ClientSettings, + // ) -> ResolvedClientSettings { + // let Some(url) = self.url_for_key(key) else { + // return ResolvedClientSettings::global(global_settings); + // }; + // let Some(WorkspaceSettings { + // client_settings, .. + // }) = self.settings_for_url(url) + // else { + // return ResolvedClientSettings::global(global_settings); + // }; + // client_settings.clone() + // } + + fn document_controller_for_key( + &mut self, + key: &DocumentKey, + ) -> crate::Result<&mut DocumentController> { + let Some(url) = self.url_for_key(key).cloned() else { + anyhow::bail!("Tried to open unavailable document `{key}`"); + }; + let Some(controller) = self.documents.get_mut(&url) else { + anyhow::bail!("Document controller not available at `{}`", url); + }; + Ok(controller) + } + + fn url_for_key<'a>(&'a self, key: &'a DocumentKey) -> Option<&'a Url> { + match key { + DocumentKey::Text(path) => Some(path), + } + } + + fn settings_for_url(&self, url: &Url) -> &Settings { + self.settings.settings_for_url(url) + } +} + +impl DocumentController { + fn new_text(document: TextDocument) -> Self { + Self::Text(Arc::new(document)) + } + + fn make_ref(&self, file_url: Url, settings: Settings) -> DocumentQuery { + match &self { + Self::Text(document) => DocumentQuery::Text { + file_url, + document: document.clone(), + settings, + }, + } + } + + pub(crate) fn as_text(&self) -> Option<&TextDocument> { + match self { + Self::Text(document) => Some(document), + } + } + + pub(crate) fn as_text_mut(&mut self) -> Option<&mut TextDocument> { + Some(match self { + Self::Text(document) => Arc::make_mut(document), + }) + } +} + +impl DocumentQuery { + /// Retrieve the original key that describes this document query. + #[allow(dead_code)] + pub(crate) fn make_key(&self) -> DocumentKey { + match self { + Self::Text { file_url, .. } => DocumentKey::Text(file_url.clone()), + } + } + + /// Get the document settings associated with this query. + pub(crate) fn settings(&self) -> &Settings { + match self { + Self::Text { settings, .. } => settings, + } + } + + /// Get the version of document selected by this query. + #[allow(dead_code)] + pub(crate) fn version(&self) -> DocumentVersion { + match self { + Self::Text { document, .. } => document.version(), + } + } + + /// Get the URL for the document selected by this query. + pub(crate) fn file_url(&self) -> &Url { + match self { + Self::Text { file_url, .. } => file_url, + } + } + + /// Get the path for the document selected by this query. + /// + /// Returns `None` if this is an unsaved (untitled) document. + /// + /// The path isn't guaranteed to point to a real path on the filesystem. This is the case + /// for unsaved (untitled) documents. + #[allow(dead_code)] + pub(crate) fn file_path(&self) -> Option { + self.file_url().to_file_path().ok() + } + + /// Get the path for the document selected by this query, ignoring whether the file exists on disk. + /// + /// Returns the URL's path if this is an unsaved (untitled) document. + #[allow(dead_code)] + pub(crate) fn virtual_file_path(&self) -> Cow { + self.file_path().map_or_else( + || Cow::Borrowed(Path::new(self.file_url().path())), + Cow::Owned, + ) + } + + /// Attempt to access the single inner text document selected by the query. + pub(crate) fn as_single_document(&self) -> &TextDocument { + match self { + Self::Text { document, .. } => document, + } + } + + #[allow(dead_code)] + pub(crate) fn text_document_language_id(&self) -> Option { + // Optional because notebooks don't have a document language id + let DocumentQuery::Text { document, .. } = self; + document.language_id() + } +} diff --git a/crates/ruff_server/src/session/workspaces.rs b/crates/ruff_server/src/session/workspaces.rs new file mode 100644 index 00000000..f95736c2 --- /dev/null +++ b/crates/ruff_server/src/session/workspaces.rs @@ -0,0 +1,181 @@ +// +------------------------------------------------------------+ +// | Code adopted from: | +// | Repository: https://github.com/astral-sh/ruff.git | +// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | +// +------------------------------------------------------------+ + +use std::path::Path; +use std::path::PathBuf; + +use lsp_types::Url; +use lsp_types::WorkspaceFolder; +use workspace::resolve::PathResolver; +use workspace::resolve::SettingsResolver; +use workspace::settings::Settings; + +/// Resolver for retrieving [`Settings`] associated with a workspace specific [`Path`] +#[derive(Debug, Default)] +pub(crate) struct WorkspaceSettingsResolver { + /// Resolves a `path` to the closest workspace specific `SettingsResolver`. + /// That `SettingsResolver` can then return `Settings` for the `path`. + path_to_settings_resolver: PathResolver, +} + +impl WorkspaceSettingsResolver { + /// Construct a new workspace settings resolver from an initial set of workspace folders + pub(crate) fn from_workspace_folders(workspace_folders: Vec) -> Self { + // How to do better here? + let fallback = Settings::default(); + + let settings_resolver_fallback = SettingsResolver::new(fallback); + let path_to_settings_resolver = PathResolver::new(settings_resolver_fallback); + + let mut resolver = Self { + path_to_settings_resolver, + }; + + // Add each workspace folder's settings into the resolver. + // If we fail for any reason (i.e. parse failure of an `air.toml`) then + // we log an error and try to resolve the remaining workspace folders. We don't want + // to propagate an error here because we don't want to prevent the server from + // starting up entirely. + // TODO: This is one place it would be nice to show a toast notification back + // to the user, but we probably need to add support to the Aux thread for that? + for workspace_folder in workspace_folders { + if let Err(error) = resolver.open_workspace_folder(&workspace_folder.uri) { + tracing::error!( + "Failed to load workspace settings for '{uri}':\n{error}", + uri = workspace_folder.uri.as_str(), + error = error + ); + } + } + + resolver + } + + pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { + let path = match Self::url_to_path(url)? { + Some(path) => path, + None => { + tracing::warn!("Ignoring non-file workspace URL: {url}"); + return Ok(()); + } + }; + + // How to do better here? + let fallback = Settings::default(); + + let mut settings_resolver = SettingsResolver::new(fallback); + settings_resolver.load_from_paths(&[&path])?; + + tracing::trace!("Adding workspace settings: {}", path.display()); + self.path_to_settings_resolver.add(&path, settings_resolver); + + Ok(()) + } + + pub(crate) fn close_workspace_folder( + &mut self, + url: &Url, + ) -> anyhow::Result> { + match Self::url_to_path(url)? { + Some(path) => { + tracing::trace!("Removing workspace settings: {}", path.display()); + Ok(self.path_to_settings_resolver.remove(&path)) + } + None => { + tracing::warn!("Ignoring non-file workspace URL: {url}"); + Ok(None) + } + } + } + + pub(crate) fn len(&self) -> usize { + self.path_to_settings_resolver.len() + } + + /// Return the appropriate [`Settings`] for a given document [`Url`]. + pub(crate) fn settings_for_url(&self, url: &Url) -> &Settings { + if let Ok(Some(path)) = Self::url_to_path(url) { + return self.settings_for_path(&path); + } + + // For `untitled` schemes, we have special behavior. + // If there is exactly 1 workspace, we resolve using a path of + // `{workspace_path}/untitled` to provide relevant settings for this workspace. + if url.scheme() == "untitled" && self.path_to_settings_resolver.len() == 1 { + tracing::trace!("Using workspace settings for 'untitled' URL: {url}"); + let workspace_path = self.path_to_settings_resolver.keys().next().unwrap(); + let path = workspace_path.join("untitled"); + return self.settings_for_path(&path); + } + + tracing::trace!("Using default settings for non-file URL: {url}"); + self.path_to_settings_resolver.fallback().fallback() + } + + /// Reloads all workspaces matched by the [`Url`] + /// + /// This is utilized by the watched files handler to reload the settings + /// resolver whenever an `air.toml` is modified. + pub(crate) fn reload_workspaces_matched_by_url(&mut self, url: &Url) { + let path = match Self::url_to_path(url) { + Ok(Some(path)) => path, + Ok(None) => { + tracing::trace!("Ignoring non-`file` changed URL: {url}"); + return; + } + Err(error) => { + tracing::error!("Failed to reload workspaces associated with {url}:\n{error}"); + return; + } + }; + + if !path.ends_with("air.toml") { + // We could get called with a changed file that isn't an `air.toml` if we are + // watching more than `air.toml` files + tracing::trace!("Ignoring non-`air.toml` changed URL: {url}"); + return; + } + + for (workspace_path, settings_resolver) in self.path_to_settings_resolver.matches_mut(&path) + { + tracing::trace!("Reloading workspace settings: {}", workspace_path.display()); + + settings_resolver.clear(); + + if let Err(error) = settings_resolver.load_from_paths(&[workspace_path]) { + tracing::error!( + "Failed to reload workspace settings for {path}:\n{error}", + path = workspace_path.display(), + error = error + ); + } + } + } + + /// Return the appropriate [`Settings`] for a given [`Path`]. + /// + /// This actually performs a double resolution. It first resolves to the + /// workspace specific `SettingsResolver` that matches this path, and then uses that + /// resolver to actually resolve the `Settings` for this path. We do it this way + /// to ensure we can easily add and remove workspaces (including all of their + /// hierarchical paths). + fn settings_for_path(&self, path: &Path) -> &Settings { + let settings_resolver = self.path_to_settings_resolver.resolve_or_fallback(path); + settings_resolver.resolve_or_fallback(path) + } + + fn url_to_path(url: &Url) -> anyhow::Result> { + if url.scheme() != "file" { + return Ok(None); + } + + let path = url + .to_file_path() + .map_err(|()| anyhow::anyhow!("Failed to convert workspace URL to file path: {url}"))?; + + Ok(Some(path)) + } +} diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml new file mode 100644 index 00000000..521a37c7 --- /dev/null +++ b/crates/ruff_source_file/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "ruff_source_file" +version = "0.0.0" +publish = false +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[lib] + +[dependencies] +ruff_text_size = { workspace = true } + +memchr = { workspace = true } +serde = { workspace = true, optional = true } + +[dev-dependencies] + +[features] +serde = ["dep:serde", "ruff_text_size/serde"] + +[lints] +workspace = true diff --git a/crates/ruff_source_file/src/lib.rs b/crates/ruff_source_file/src/lib.rs new file mode 100644 index 00000000..5bf43e3a --- /dev/null +++ b/crates/ruff_source_file/src/lib.rs @@ -0,0 +1,278 @@ +use std::cmp::Ordering; +use std::fmt::{Debug, Display, Formatter}; +use std::sync::{Arc, OnceLock}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use ruff_text_size::{Ranged, TextRange, TextSize}; + +pub use crate::line_index::{LineIndex, OneIndexed}; +pub use crate::line_ranges::LineRanges; +pub use crate::newlines::{ + find_newline, Line, LineEnding, NewlineWithTrailingNewline, UniversalNewlineIterator, + UniversalNewlines, +}; + +mod line_index; +mod line_ranges; +mod newlines; + +/// Gives access to the source code of a file and allows mapping between [`TextSize`] and [`SourceLocation`]. +#[derive(Debug)] +pub struct SourceCode<'src, 'index> { + text: &'src str, + index: &'index LineIndex, +} + +impl<'src, 'index> SourceCode<'src, 'index> { + pub fn new(content: &'src str, index: &'index LineIndex) -> Self { + Self { + text: content, + index, + } + } + + /// Computes the one indexed row and column numbers for `offset`. + #[inline] + pub fn source_location(&self, offset: TextSize) -> SourceLocation { + self.index.source_location(offset, self.text) + } + + #[inline] + pub fn line_index(&self, offset: TextSize) -> OneIndexed { + self.index.line_index(offset) + } + + /// Take the source code up to the given [`TextSize`]. + #[inline] + pub fn up_to(&self, offset: TextSize) -> &'src str { + &self.text[TextRange::up_to(offset)] + } + + /// Take the source code after the given [`TextSize`]. + #[inline] + pub fn after(&self, offset: TextSize) -> &'src str { + &self.text[usize::from(offset)..] + } + + /// Take the source code between the given [`TextRange`]. + pub fn slice(&self, ranged: T) -> &'src str { + &self.text[ranged.range()] + } + + pub fn line_start(&self, line: OneIndexed) -> TextSize { + self.index.line_start(line, self.text) + } + + pub fn line_end(&self, line: OneIndexed) -> TextSize { + self.index.line_end(line, self.text) + } + + pub fn line_end_exclusive(&self, line: OneIndexed) -> TextSize { + self.index.line_end_exclusive(line, self.text) + } + + pub fn line_range(&self, line: OneIndexed) -> TextRange { + self.index.line_range(line, self.text) + } + + /// Returns the source text of the line with the given index + #[inline] + pub fn line_text(&self, index: OneIndexed) -> &'src str { + let range = self.index.line_range(index, self.text); + &self.text[range] + } + + /// Returns the source text + pub fn text(&self) -> &'src str { + self.text + } + + /// Returns the number of lines + #[inline] + pub fn line_count(&self) -> usize { + self.index.line_count() + } +} + +impl PartialEq for SourceCode<'_, '_> { + fn eq(&self, other: &Self) -> bool { + self.text == other.text + } +} + +impl Eq for SourceCode<'_, '_> {} + +/// A Builder for constructing a [`SourceFile`] +pub struct SourceFileBuilder { + name: Box, + code: Box, + index: Option, +} + +impl SourceFileBuilder { + /// Creates a new builder for a file named `name`. + pub fn new>, Code: Into>>(name: Name, code: Code) -> Self { + Self { + name: name.into(), + code: code.into(), + index: None, + } + } + + #[must_use] + pub fn line_index(mut self, index: LineIndex) -> Self { + self.index = Some(index); + self + } + + pub fn set_line_index(&mut self, index: LineIndex) { + self.index = Some(index); + } + + /// Consumes `self` and returns the [`SourceFile`]. + pub fn finish(self) -> SourceFile { + let index = if let Some(index) = self.index { + OnceLock::from(index) + } else { + OnceLock::new() + }; + + SourceFile { + inner: Arc::new(SourceFileInner { + name: self.name, + code: self.code, + line_index: index, + }), + } + } +} + +/// A source file that is identified by its name. Optionally stores the source code and [`LineIndex`]. +/// +/// Cloning a [`SourceFile`] is cheap, because it only requires bumping a reference count. +#[derive(Clone, Eq, PartialEq)] +pub struct SourceFile { + inner: Arc, +} + +impl Debug for SourceFile { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SourceFile") + .field("name", &self.name()) + .field("code", &self.source_text()) + .finish() + } +} + +impl SourceFile { + /// Returns the name of the source file (filename). + #[inline] + pub fn name(&self) -> &str { + &self.inner.name + } + + #[inline] + pub fn slice(&self, range: TextRange) -> &str { + &self.source_text()[range] + } + + pub fn to_source_code(&self) -> SourceCode { + SourceCode { + text: self.source_text(), + index: self.index(), + } + } + + fn index(&self) -> &LineIndex { + self.inner + .line_index + .get_or_init(|| LineIndex::from_source_text(self.source_text())) + } + + /// Returns the source code. + #[inline] + pub fn source_text(&self) -> &str { + &self.inner.code + } +} + +impl PartialOrd for SourceFile { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SourceFile { + fn cmp(&self, other: &Self) -> Ordering { + // Short circuit if these are the same source files + if Arc::ptr_eq(&self.inner, &other.inner) { + Ordering::Equal + } else { + self.inner.name.cmp(&other.inner.name) + } + } +} + +struct SourceFileInner { + name: Box, + code: Box, + line_index: OnceLock, +} + +impl PartialEq for SourceFileInner { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.code == other.code + } +} + +impl Eq for SourceFileInner {} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SourceLocation { + pub row: OneIndexed, + pub column: OneIndexed, +} + +impl Default for SourceLocation { + fn default() -> Self { + Self { + row: OneIndexed::MIN, + column: OneIndexed::MIN, + } + } +} + +impl Debug for SourceLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SourceLocation") + .field("row", &self.row.get()) + .field("column", &self.column.get()) + .finish() + } +} + +impl std::fmt::Display for SourceLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{row}:{column}", row = self.row, column = self.column) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum SourceRow { + /// A row within a cell in a Jupyter Notebook. + Notebook { cell: OneIndexed, line: OneIndexed }, + /// A row within a source file. + SourceFile { line: OneIndexed }, +} + +impl Display for SourceRow { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SourceRow::Notebook { cell, line } => write!(f, "cell {cell}, line {line}"), + SourceRow::SourceFile { line } => write!(f, "line {line}"), + } + } +} diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs new file mode 100644 index 00000000..e9d211ca --- /dev/null +++ b/crates/ruff_source_file/src/line_index.rs @@ -0,0 +1,714 @@ +use std::fmt; +use std::fmt::{Debug, Formatter}; +use std::num::{NonZeroUsize, ParseIntError}; +use std::ops::Deref; +use std::str::FromStr; +use std::sync::Arc; + +use ruff_text_size::{TextLen, TextRange, TextSize}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::SourceLocation; + +/// Index for fast [byte offset](TextSize) to [`SourceLocation`] conversions. +/// +/// Cloning a [`LineIndex`] is cheap because it only requires bumping a reference count. +#[derive(Clone, Eq, PartialEq)] +pub struct LineIndex { + inner: Arc, +} + +#[derive(Eq, PartialEq)] +struct LineIndexInner { + line_starts: Vec, + kind: IndexKind, +} + +impl LineIndex { + /// Builds the [`LineIndex`] from the source text of a file. + pub fn from_source_text(text: &str) -> Self { + let mut line_starts: Vec = Vec::with_capacity(text.len() / 88); + line_starts.push(TextSize::default()); + + let bytes = text.as_bytes(); + let mut utf8 = false; + + assert!(u32::try_from(bytes.len()).is_ok()); + + for (i, byte) in bytes.iter().enumerate() { + utf8 |= !byte.is_ascii(); + + match byte { + // Only track one line break for `\r\n`. + b'\r' if bytes.get(i + 1) == Some(&b'\n') => continue, + b'\n' | b'\r' => { + // SAFETY: Assertion above guarantees `i <= u32::MAX` + #[allow(clippy::cast_possible_truncation)] + line_starts.push(TextSize::from(i as u32) + TextSize::from(1)); + } + _ => {} + } + } + + let kind = if utf8 { + IndexKind::Utf8 + } else { + IndexKind::Ascii + }; + + Self { + inner: Arc::new(LineIndexInner { line_starts, kind }), + } + } + + fn kind(&self) -> IndexKind { + self.inner.kind + } + + /// Returns the row and column index for an offset. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::TextSize; + /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; + /// let source = "def a():\n pass"; + /// let index = LineIndex::from_source_text(source); + /// + /// assert_eq!( + /// index.source_location(TextSize::from(0), source), + /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } + /// ); + /// + /// assert_eq!( + /// index.source_location(TextSize::from(4), source), + /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } + /// ); + /// assert_eq!( + /// index.source_location(TextSize::from(13), source), + /// SourceLocation { row: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) } + /// ); + /// ``` + /// + /// ## Panics + /// + /// If the offset is out of bounds. + pub fn source_location(&self, offset: TextSize, content: &str) -> SourceLocation { + match self.line_starts().binary_search(&offset) { + // Offset is at the start of a line + Ok(row) => SourceLocation { + row: OneIndexed::from_zero_indexed(row), + column: OneIndexed::from_zero_indexed(0), + }, + Err(next_row) => { + // SAFETY: Safe because the index always contains an entry for the offset 0 + let row = next_row - 1; + let mut line_start = self.line_starts()[row]; + + let column = if self.kind().is_ascii() { + usize::from(offset) - usize::from(line_start) + } else { + // Don't count the BOM character as a column. + if line_start == TextSize::from(0) && content.starts_with('\u{feff}') { + line_start = '\u{feff}'.text_len(); + } + + content[TextRange::new(line_start, offset)].chars().count() + }; + + SourceLocation { + row: OneIndexed::from_zero_indexed(row), + column: OneIndexed::from_zero_indexed(column), + } + } + } + } + + /// Return the number of lines in the source code. + pub fn line_count(&self) -> usize { + self.line_starts().len() + } + + /// Returns `true` if the text only consists of ASCII characters + pub fn is_ascii(&self) -> bool { + self.kind().is_ascii() + } + + /// Returns the row number for a given offset. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::TextSize; + /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; + /// let source = "def a():\n pass"; + /// let index = LineIndex::from_source_text(source); + /// + /// assert_eq!(index.line_index(TextSize::from(0)), OneIndexed::from_zero_indexed(0)); + /// assert_eq!(index.line_index(TextSize::from(4)), OneIndexed::from_zero_indexed(0)); + /// assert_eq!(index.line_index(TextSize::from(13)), OneIndexed::from_zero_indexed(1)); + /// ``` + /// + /// ## Panics + /// + /// If the offset is out of bounds. + pub fn line_index(&self, offset: TextSize) -> OneIndexed { + match self.line_starts().binary_search(&offset) { + // Offset is at the start of a line + Ok(row) => OneIndexed::from_zero_indexed(row), + Err(row) => { + // SAFETY: Safe because the index always contains an entry for the offset 0 + OneIndexed::from_zero_indexed(row - 1) + } + } + } + + /// Returns the [byte offset](TextSize) for the `line` with the given index. + pub fn line_start(&self, line: OneIndexed, contents: &str) -> TextSize { + let row_index = line.to_zero_indexed(); + let starts = self.line_starts(); + + // If start-of-line position after last line + if row_index == starts.len() { + contents.text_len() + } else { + starts[row_index] + } + } + + /// Returns the [byte offset](TextSize) of the `line`'s end. + /// The offset is the end of the line, up to and including the newline character ending the line (if any). + pub fn line_end(&self, line: OneIndexed, contents: &str) -> TextSize { + let row_index = line.to_zero_indexed(); + let starts = self.line_starts(); + + // If start-of-line position after last line + if row_index.saturating_add(1) >= starts.len() { + contents.text_len() + } else { + starts[row_index + 1] + } + } + + /// Returns the [byte offset](TextSize) of the `line`'s end. + /// The offset is the end of the line, excluding the newline character ending the line (if any). + pub fn line_end_exclusive(&self, line: OneIndexed, contents: &str) -> TextSize { + let row_index = line.to_zero_indexed(); + let starts = self.line_starts(); + + // If start-of-line position after last line + if row_index.saturating_add(1) >= starts.len() { + contents.text_len() + } else { + starts[row_index + 1] - TextSize::new(1) + } + } + + /// Returns the [`TextRange`] of the `line` with the given index. + /// The start points to the first character's [byte offset](TextSize), the end up to, and including + /// the newline character ending the line (if any). + pub fn line_range(&self, line: OneIndexed, contents: &str) -> TextRange { + let starts = self.line_starts(); + + if starts.len() == line.to_zero_indexed() { + TextRange::empty(contents.text_len()) + } else { + TextRange::new( + self.line_start(line, contents), + self.line_start(line.saturating_add(1), contents), + ) + } + } + + /// Returns the [byte offset](TextSize) at `line` and `column`. + /// + /// ## Examples + /// + /// ### ASCII + /// + /// ``` + /// use ruff_source_file::{LineIndex, OneIndexed}; + /// use ruff_text_size::TextSize; + /// let source = r#"a = 4 + /// c = "some string" + /// x = b"#; + /// + /// let index = LineIndex::from_source_text(source); + /// + /// // First line, first column + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::new(0)); + /// + /// // Second line, 4th column + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(4), source), TextSize::new(10)); + /// + /// // Offset past the end of the first line + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(10), source), TextSize::new(6)); + /// + /// // Offset past the end of the file + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::new(29)); + /// ``` + /// + /// ### UTF8 + /// + /// ``` + /// use ruff_source_file::{LineIndex, OneIndexed}; + /// use ruff_text_size::TextSize; + /// let source = r#"a = 4 + /// c = "❤️" + /// x = b"#; + /// + /// let index = LineIndex::from_source_text(source); + /// + /// // First line, first column + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::new(0)); + /// + /// // Third line, 2nd column, after emoji + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(2), OneIndexed::from_zero_indexed(1), source), TextSize::new(20)); + /// + /// // Offset past the end of the second line + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(10), source), TextSize::new(19)); + /// + /// // Offset past the end of the file + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::new(24)); + /// ``` + /// + pub fn offset(&self, line: OneIndexed, column: OneIndexed, contents: &str) -> TextSize { + // If start-of-line position after last line + if line.to_zero_indexed() > self.line_starts().len() { + return contents.text_len(); + } + + let line_range = self.line_range(line, contents); + + match self.kind() { + IndexKind::Ascii => { + line_range.start() + + TextSize::try_from(column.to_zero_indexed()) + .unwrap_or(line_range.len()) + .clamp(TextSize::new(0), line_range.len()) + } + IndexKind::Utf8 => { + let rest = &contents[line_range]; + let column_offset: TextSize = rest + .chars() + .take(column.to_zero_indexed()) + .map(ruff_text_size::TextLen::text_len) + .sum(); + line_range.start() + column_offset + } + } + } + + /// Returns the [byte offsets](TextSize) for every line + pub fn line_starts(&self) -> &[TextSize] { + &self.inner.line_starts + } +} + +impl Deref for LineIndex { + type Target = [TextSize]; + + fn deref(&self) -> &Self::Target { + self.line_starts() + } +} + +impl Debug for LineIndex { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.line_starts()).finish() + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum IndexKind { + /// Optimized index for an ASCII only document + Ascii, + + /// Index for UTF8 documents + Utf8, +} + +impl IndexKind { + const fn is_ascii(self) -> bool { + matches!(self, IndexKind::Ascii) + } +} + +/// Type-safe wrapper for a value whose logical range starts at `1`, for +/// instance the line or column numbers in a file +/// +/// Internally this is represented as a [`NonZeroUsize`], this enables some +/// memory optimizations +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct OneIndexed(NonZeroUsize); + +impl OneIndexed { + /// The largest value that can be represented by this integer type + pub const MAX: Self = unwrap(Self::new(usize::MAX)); + // SAFETY: These constants are being initialized with non-zero values + /// The smallest value that can be represented by this integer type. + pub const MIN: Self = unwrap(Self::new(1)); + pub const ONE: NonZeroUsize = unwrap(NonZeroUsize::new(1)); + + /// Creates a non-zero if the given value is not zero. + pub const fn new(value: usize) -> Option { + match NonZeroUsize::new(value) { + Some(value) => Some(Self(value)), + None => None, + } + } + + /// Construct a new [`OneIndexed`] from a zero-indexed value + pub const fn from_zero_indexed(value: usize) -> Self { + Self(Self::ONE.saturating_add(value)) + } + + /// Returns the value as a primitive type. + pub const fn get(self) -> usize { + self.0.get() + } + + /// Return the zero-indexed primitive value for this [`OneIndexed`] + pub const fn to_zero_indexed(self) -> usize { + self.0.get() - 1 + } + + /// Saturating integer addition. Computes `self + rhs`, saturating at + /// the numeric bounds instead of overflowing. + #[must_use] + pub const fn saturating_add(self, rhs: usize) -> Self { + match NonZeroUsize::new(self.0.get().saturating_add(rhs)) { + Some(value) => Self(value), + None => Self::MAX, + } + } + + /// Saturating integer subtraction. Computes `self - rhs`, saturating + /// at the numeric bounds instead of overflowing. + #[must_use] + pub const fn saturating_sub(self, rhs: usize) -> Self { + match NonZeroUsize::new(self.0.get().saturating_sub(rhs)) { + Some(value) => Self(value), + None => Self::MIN, + } + } + + /// Checked addition. Returns `None` if overflow occurred. + #[must_use] + pub fn checked_add(self, rhs: Self) -> Option { + self.0.checked_add(rhs.0.get()).map(Self) + } + + /// Checked subtraction. Returns `None` if overflow occurred. + #[must_use] + pub fn checked_sub(self, rhs: Self) -> Option { + self.0.get().checked_sub(rhs.get()).and_then(Self::new) + } +} + +impl fmt::Display for OneIndexed { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + std::fmt::Debug::fmt(&self.0.get(), f) + } +} + +/// A const `Option::unwrap` without nightly features: +/// [Tracking issue](https://github.com/rust-lang/rust/issues/67441) +const fn unwrap(option: Option) -> T { + match option { + Some(value) => value, + None => panic!("unwrapping None"), + } +} + +impl FromStr for OneIndexed { + type Err = ParseIntError; + fn from_str(s: &str) -> Result { + Ok(OneIndexed(NonZeroUsize::from_str(s)?)) + } +} + +#[cfg(test)] +mod tests { + use ruff_text_size::TextSize; + + use crate::line_index::LineIndex; + use crate::{OneIndexed, SourceLocation}; + + #[test] + fn ascii_index() { + let index = LineIndex::from_source_text(""); + assert_eq!(index.line_starts(), &[TextSize::from(0)]); + + let index = LineIndex::from_source_text("x = 1"); + assert_eq!(index.line_starts(), &[TextSize::from(0)]); + + let index = LineIndex::from_source_text("x = 1\n"); + assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]); + + let index = LineIndex::from_source_text("x = 1\ny = 2\nz = x + y\n"); + assert_eq!( + index.line_starts(), + &[ + TextSize::from(0), + TextSize::from(6), + TextSize::from(12), + TextSize::from(22) + ] + ); + } + + #[test] + fn ascii_source_location() { + let contents = "x = 1\ny = 2"; + let index = LineIndex::from_source_text(contents); + + // First row. + let loc = index.source_location(TextSize::from(2), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(2) + } + ); + + // Second row. + let loc = index.source_location(TextSize::from(6), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } + ); + + let loc = index.source_location(TextSize::from(11), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(5) + } + ); + } + + #[test] + fn ascii_carriage_return() { + let contents = "x = 4\ry = 3"; + let index = LineIndex::from_source_text(contents); + assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]); + + assert_eq!( + index.source_location(TextSize::from(4), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(4) + } + ); + assert_eq!( + index.source_location(TextSize::from(6), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } + ); + assert_eq!( + index.source_location(TextSize::from(7), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(1) + } + ); + } + + #[test] + fn ascii_carriage_return_newline() { + let contents = "x = 4\r\ny = 3"; + let index = LineIndex::from_source_text(contents); + assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(7)]); + + assert_eq!( + index.source_location(TextSize::from(4), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(4) + } + ); + assert_eq!( + index.source_location(TextSize::from(7), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } + ); + assert_eq!( + index.source_location(TextSize::from(8), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(1) + } + ); + } + + #[test] + fn utf8_index() { + let index = LineIndex::from_source_text("x = '🫣'"); + assert_eq!(index.line_count(), 1); + assert_eq!(index.line_starts(), &[TextSize::from(0)]); + + let index = LineIndex::from_source_text("x = '🫣'\n"); + assert_eq!(index.line_count(), 2); + assert_eq!( + index.line_starts(), + &[TextSize::from(0), TextSize::from(11)] + ); + + let index = LineIndex::from_source_text("x = '🫣'\ny = 2\nz = x + y\n"); + assert_eq!(index.line_count(), 4); + assert_eq!( + index.line_starts(), + &[ + TextSize::from(0), + TextSize::from(11), + TextSize::from(17), + TextSize::from(27) + ] + ); + + let index = LineIndex::from_source_text("# 🫣\nclass Foo:\n \"\"\".\"\"\""); + assert_eq!(index.line_count(), 3); + assert_eq!( + index.line_starts(), + &[TextSize::from(0), TextSize::from(7), TextSize::from(18)] + ); + } + + #[test] + fn utf8_carriage_return() { + let contents = "x = '🫣'\ry = 3"; + let index = LineIndex::from_source_text(contents); + assert_eq!(index.line_count(), 2); + assert_eq!( + index.line_starts(), + &[TextSize::from(0), TextSize::from(11)] + ); + + // Second ' + assert_eq!( + index.source_location(TextSize::from(9), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(6) + } + ); + assert_eq!( + index.source_location(TextSize::from(11), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } + ); + assert_eq!( + index.source_location(TextSize::from(12), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(1) + } + ); + } + + #[test] + fn utf8_carriage_return_newline() { + let contents = "x = '🫣'\r\ny = 3"; + let index = LineIndex::from_source_text(contents); + assert_eq!(index.line_count(), 2); + assert_eq!( + index.line_starts(), + &[TextSize::from(0), TextSize::from(12)] + ); + + // Second ' + assert_eq!( + index.source_location(TextSize::from(9), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(6) + } + ); + assert_eq!( + index.source_location(TextSize::from(12), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } + ); + assert_eq!( + index.source_location(TextSize::from(13), contents), + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(1) + } + ); + } + + #[test] + fn utf8_byte_offset() { + let contents = "x = '☃'\ny = 2"; + let index = LineIndex::from_source_text(contents); + assert_eq!( + index.line_starts(), + &[TextSize::from(0), TextSize::from(10)] + ); + + // First row. + let loc = index.source_location(TextSize::from(0), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(0) + } + ); + + let loc = index.source_location(TextSize::from(5), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(5) + } + ); + + let loc = index.source_location(TextSize::from(8), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(0), + column: OneIndexed::from_zero_indexed(6) + } + ); + + // Second row. + let loc = index.source_location(TextSize::from(10), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(0) + } + ); + + // One-past-the-end. + let loc = index.source_location(TextSize::from(15), contents); + assert_eq!( + loc, + SourceLocation { + row: OneIndexed::from_zero_indexed(1), + column: OneIndexed::from_zero_indexed(5) + } + ); + } +} diff --git a/crates/ruff_source_file/src/line_ranges.rs b/crates/ruff_source_file/src/line_ranges.rs new file mode 100644 index 00000000..77c19837 --- /dev/null +++ b/crates/ruff_source_file/src/line_ranges.rs @@ -0,0 +1,396 @@ +use crate::find_newline; +use memchr::{memchr2, memrchr2}; +use ruff_text_size::{TextLen, TextRange, TextSize}; +use std::ops::Add; + +/// Extension trait for [`str`] that provides methods for working with ranges of lines. +pub trait LineRanges { + /// Computes the start position of the line of `offset`. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::TextSize; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\rthird line"; + /// + /// assert_eq!(text.line_start(TextSize::from(0)), TextSize::from(0)); + /// assert_eq!(text.line_start(TextSize::from(4)), TextSize::from(0)); + /// + /// assert_eq!(text.line_start(TextSize::from(14)), TextSize::from(11)); + /// assert_eq!(text.line_start(TextSize::from(28)), TextSize::from(23)); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + fn line_start(&self, offset: TextSize) -> TextSize; + + /// Computes the start position of the file contents: either the first byte, or the byte after + /// the BOM. + fn bom_start_offset(&self) -> TextSize; + + /// Returns `true` if `offset` is at the start of a line. + fn is_at_start_of_line(&self, offset: TextSize) -> bool { + self.line_start(offset) == offset + } + + /// Computes the offset that is right after the newline character that ends `offset`'s line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!(text.full_line_end(TextSize::from(3)), TextSize::from(11)); + /// assert_eq!(text.full_line_end(TextSize::from(14)), TextSize::from(24)); + /// assert_eq!(text.full_line_end(TextSize::from(28)), TextSize::from(34)); + /// ``` + /// + /// ## Panics + /// + /// If `offset` is passed the end of the content. + fn full_line_end(&self, offset: TextSize) -> TextSize; + + /// Computes the offset that is right before the newline character that ends `offset`'s line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!(text.line_end(TextSize::from(3)), TextSize::from(10)); + /// assert_eq!(text.line_end(TextSize::from(14)), TextSize::from(22)); + /// assert_eq!(text.line_end(TextSize::from(28)), TextSize::from(34)); + /// ``` + /// + /// ## Panics + /// + /// If `offset` is passed the end of the content. + fn line_end(&self, offset: TextSize) -> TextSize; + + /// Computes the range of this `offset`s line. + /// + /// The range starts at the beginning of the line and goes up to, and including, the new line character + /// at the end of the line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!(text.full_line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(11))); + /// assert_eq!(text.full_line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(24))); + /// assert_eq!(text.full_line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34))); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + fn full_line_range(&self, offset: TextSize) -> TextRange { + TextRange::new(self.line_start(offset), self.full_line_end(offset)) + } + + /// Computes the range of this `offset`s line ending before the newline character. + /// + /// The range starts at the beginning of the line and goes up to, but excluding, the new line character + /// at the end of the line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!(text.line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(10))); + /// assert_eq!(text.line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(22))); + /// assert_eq!(text.line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34))); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + fn line_range(&self, offset: TextSize) -> TextRange { + TextRange::new(self.line_start(offset), self.line_end(offset)) + } + + /// Returns the text of the `offset`'s line. + /// + /// The line includes the newline characters at the end of the line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!(text.full_line_str(TextSize::from(3)), "First line\n"); + /// assert_eq!(text.full_line_str(TextSize::from(14)), "second line\r\n"); + /// assert_eq!(text.full_line_str(TextSize::from(28)), "third line"); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + fn full_line_str(&self, offset: TextSize) -> &str; + + /// Returns the text of the `offset`'s line. + /// + /// Excludes the newline characters at the end of the line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!(text.line_str(TextSize::from(3)), "First line"); + /// assert_eq!(text.line_str(TextSize::from(14)), "second line"); + /// assert_eq!(text.line_str(TextSize::from(28)), "third line"); + /// ``` + /// + /// ## Panics + /// If `offset` is out of bounds. + fn line_str(&self, offset: TextSize) -> &str; + + /// Computes the range of all lines that this `range` covers. + /// + /// The range starts at the beginning of the line at `range.start()` and goes up to, and including, the new line character + /// at the end of `range.ends()`'s line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!( + /// text.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// TextRange::new(TextSize::from(0), TextSize::from(11)) + /// ); + /// assert_eq!( + /// text.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// TextRange::new(TextSize::from(0), TextSize::from(24)) + /// ); + /// ``` + /// + /// ## Panics + /// If the start or end of `range` is out of bounds. + fn full_lines_range(&self, range: TextRange) -> TextRange { + TextRange::new( + self.line_start(range.start()), + self.full_line_end(range.end()), + ) + } + + /// Computes the range of all lines that this `range` covers. + /// + /// The range starts at the beginning of the line at `range.start()` and goes up to, but excluding, the new line character + /// at the end of `range.end()`'s line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!( + /// text.lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// TextRange::new(TextSize::from(0), TextSize::from(10)) + /// ); + /// assert_eq!( + /// text.lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// TextRange::new(TextSize::from(0), TextSize::from(22)) + /// ); + /// ``` + /// + /// ## Panics + /// If the start or end of `range` is out of bounds. + fn lines_range(&self, range: TextRange) -> TextRange { + TextRange::new(self.line_start(range.start()), self.line_end(range.end())) + } + + /// Returns true if the text of `range` contains any line break. + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert!( + /// !text.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// ); + /// assert!( + /// text.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// ); + /// ``` + /// + /// ## Panics + /// If the `range` is out of bounds. + fn contains_line_break(&self, range: TextRange) -> bool; + + /// Returns the text of all lines that include `range`. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!( + /// text.lines_str(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// "First line" + /// ); + /// assert_eq!( + /// text.lines_str(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// "First line\nsecond line" + /// ); + /// ``` + /// + /// ## Panics + /// If the start or end of `range` is out of bounds. + fn lines_str(&self, range: TextRange) -> &str; + + /// Returns the text of all lines that include `range`. + /// + /// Includes the newline characters of the last line. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange, TextSize}; + /// # use ruff_source_file::LineRanges; + /// + /// let text = "First line\nsecond line\r\nthird line"; + /// + /// assert_eq!( + /// text.full_lines_str(TextRange::new(TextSize::from(3), TextSize::from(5))), + /// "First line\n" + /// ); + /// assert_eq!( + /// text.full_lines_str(TextRange::new(TextSize::from(3), TextSize::from(14))), + /// "First line\nsecond line\r\n" + /// ); + /// ``` + /// + /// ## Panics + /// If the start or end of `range` is out of bounds. + fn full_lines_str(&self, range: TextRange) -> &str; + + /// The number of lines `range` spans. + /// + /// ## Examples + /// + /// ``` + /// # use ruff_text_size::{Ranged, TextRange}; + /// # use ruff_source_file::LineRanges; + /// + /// assert_eq!("a\nb".count_lines(TextRange::up_to(1.into())), 0); + /// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(3.into())), 1, "Up to the end of the second line"); + /// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(4.into())), 2, "In between the line break characters"); + /// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(5.into())), 2); + /// assert_eq!("Single line".count_lines(TextRange::up_to(13.into())), 0); + /// assert_eq!("out\nof\nbounds end".count_lines(TextRange::up_to(55.into())), 2); + /// ``` + fn count_lines(&self, range: TextRange) -> u32 { + let mut count = 0; + let mut line_end = self.line_end(range.start()); + + loop { + let next_line_start = self.full_line_end(line_end); + + // Reached the end of the string + if next_line_start == line_end { + break count; + } + + // Range ends at the line boundary + if line_end >= range.end() { + break count; + } + + count += 1; + + line_end = self.line_end(next_line_start); + } + } +} + +impl LineRanges for str { + fn line_start(&self, offset: TextSize) -> TextSize { + let bytes = self[TextRange::up_to(offset)].as_bytes(); + if let Some(index) = memrchr2(b'\n', b'\r', bytes) { + // SAFETY: Safe because `index < offset` + TextSize::try_from(index).unwrap().add(TextSize::from(1)) + } else { + self.bom_start_offset() + } + } + + fn bom_start_offset(&self) -> TextSize { + if self.starts_with('\u{feff}') { + // Skip the BOM. + '\u{feff}'.text_len() + } else { + // Start of file. + TextSize::default() + } + } + + fn full_line_end(&self, offset: TextSize) -> TextSize { + let slice = &self[usize::from(offset)..]; + if let Some((index, line_ending)) = find_newline(slice) { + offset + TextSize::try_from(index).unwrap() + line_ending.text_len() + } else { + self.text_len() + } + } + + fn line_end(&self, offset: TextSize) -> TextSize { + let slice = &self[offset.to_usize()..]; + if let Some(index) = memchr2(b'\n', b'\r', slice.as_bytes()) { + offset + TextSize::try_from(index).unwrap() + } else { + self.text_len() + } + } + + fn full_line_str(&self, offset: TextSize) -> &str { + &self[self.full_line_range(offset)] + } + + fn line_str(&self, offset: TextSize) -> &str { + &self[self.line_range(offset)] + } + + fn contains_line_break(&self, range: TextRange) -> bool { + memchr2(b'\n', b'\r', self[range].as_bytes()).is_some() + } + + fn lines_str(&self, range: TextRange) -> &str { + &self[self.lines_range(range)] + } + + fn full_lines_str(&self, range: TextRange) -> &str { + &self[self.full_lines_range(range)] + } +} diff --git a/crates/ruff_source_file/src/newlines.rs b/crates/ruff_source_file/src/newlines.rs new file mode 100644 index 00000000..deb6d846 --- /dev/null +++ b/crates/ruff_source_file/src/newlines.rs @@ -0,0 +1,457 @@ +use std::iter::FusedIterator; +use std::ops::Deref; + +use memchr::{memchr2, memrchr2}; +use ruff_text_size::{TextLen, TextRange, TextSize}; + +/// Extension trait for [`str`] that provides a [`UniversalNewlineIterator`]. +pub trait UniversalNewlines { + fn universal_newlines(&self) -> UniversalNewlineIterator<'_>; +} + +impl UniversalNewlines for str { + fn universal_newlines(&self) -> UniversalNewlineIterator<'_> { + UniversalNewlineIterator::from(self) + } +} + +/// Like [`str::lines`], but accommodates LF, CRLF, and CR line endings, +/// the latter of which are not supported by [`str::lines`]. +/// +/// ## Examples +/// +/// ```rust +/// # use ruff_text_size::TextSize; +/// # use ruff_source_file::{Line, UniversalNewlineIterator}; +/// let mut lines = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop"); +/// +/// assert_eq!(lines.next_back(), Some(Line::new("bop", TextSize::from(14)))); +/// assert_eq!(lines.next(), Some(Line::new("foo\n", TextSize::from(0)))); +/// assert_eq!(lines.next_back(), Some(Line::new("baz\r", TextSize::from(10)))); +/// assert_eq!(lines.next(), Some(Line::new("bar\n", TextSize::from(4)))); +/// assert_eq!(lines.next_back(), Some(Line::new("\r\n", TextSize::from(8)))); +/// assert_eq!(lines.next(), None); +/// ``` +#[derive(Clone)] +pub struct UniversalNewlineIterator<'a> { + text: &'a str, + offset: TextSize, + offset_back: TextSize, +} + +impl<'a> UniversalNewlineIterator<'a> { + pub fn with_offset(text: &'a str, offset: TextSize) -> UniversalNewlineIterator<'a> { + UniversalNewlineIterator { + text, + offset, + offset_back: offset + text.text_len(), + } + } + + pub fn from(text: &'a str) -> UniversalNewlineIterator<'a> { + Self::with_offset(text, TextSize::default()) + } +} + +/// Finds the next newline character. Returns its position and the [`LineEnding`]. +#[inline] +pub fn find_newline(text: &str) -> Option<(usize, LineEnding)> { + let bytes = text.as_bytes(); + if let Some(position) = memchr2(b'\n', b'\r', bytes) { + let line_ending = match bytes[position] { + // Explicit branch for `\n` as this is the most likely path + b'\n' => LineEnding::Lf, + // '\r\n' + b'\r' if bytes.get(position.saturating_add(1)) == Some(&b'\n') => LineEnding::CrLf, + // '\r' + _ => LineEnding::Cr, + }; + + Some((position, line_ending)) + } else { + None + } +} + +impl<'a> Iterator for UniversalNewlineIterator<'a> { + type Item = Line<'a>; + + #[inline] + fn next(&mut self) -> Option> { + if self.text.is_empty() { + return None; + } + + let line = if let Some((newline_position, line_ending)) = find_newline(self.text) { + let (text, remainder) = self.text.split_at(newline_position + line_ending.len()); + + let line = Line { + offset: self.offset, + text, + }; + + self.text = remainder; + self.offset += text.text_len(); + + line + } + // Last line + else { + Line { + offset: self.offset, + text: std::mem::take(&mut self.text), + } + }; + + Some(line) + } + + fn last(mut self) -> Option { + self.next_back() + } +} + +impl DoubleEndedIterator for UniversalNewlineIterator<'_> { + #[inline] + fn next_back(&mut self) -> Option { + if self.text.is_empty() { + return None; + } + + let len = self.text.len(); + + // Trim any trailing newlines. + let haystack = match self.text.as_bytes()[len - 1] { + b'\n' if len > 1 && self.text.as_bytes()[len - 2] == b'\r' => &self.text[..len - 2], + b'\n' | b'\r' => &self.text[..len - 1], + _ => self.text, + }; + + // Find the end of the previous line. The previous line is the text up to, but not including + // the newline character. + let line = if let Some(line_end) = memrchr2(b'\n', b'\r', haystack.as_bytes()) { + // '\n' or '\r' or '\r\n' + let (remainder, line) = self.text.split_at(line_end + 1); + self.text = remainder; + self.offset_back -= line.text_len(); + + Line { + text: line, + offset: self.offset_back, + } + } else { + // Last line + let offset = self.offset_back - self.text.text_len(); + Line { + text: std::mem::take(&mut self.text), + offset, + } + }; + + Some(line) + } +} + +impl FusedIterator for UniversalNewlineIterator<'_> {} + +/// Like [`UniversalNewlineIterator`], but includes a trailing newline as an empty line. +pub struct NewlineWithTrailingNewline<'a> { + trailing: Option>, + underlying: UniversalNewlineIterator<'a>, +} + +impl<'a> NewlineWithTrailingNewline<'a> { + pub fn from(input: &'a str) -> NewlineWithTrailingNewline<'a> { + Self::with_offset(input, TextSize::default()) + } + + pub fn with_offset(input: &'a str, offset: TextSize) -> Self { + NewlineWithTrailingNewline { + underlying: UniversalNewlineIterator::with_offset(input, offset), + trailing: if input.ends_with(['\r', '\n']) { + Some(Line { + text: "", + offset: offset + input.text_len(), + }) + } else { + None + }, + } + } +} + +impl<'a> Iterator for NewlineWithTrailingNewline<'a> { + type Item = Line<'a>; + + #[inline] + fn next(&mut self) -> Option { + self.underlying.next().or_else(|| self.trailing.take()) + } +} + +impl DoubleEndedIterator for NewlineWithTrailingNewline<'_> { + #[inline] + fn next_back(&mut self) -> Option { + self.trailing.take().or_else(|| self.underlying.next_back()) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Line<'a> { + text: &'a str, + offset: TextSize, +} + +impl<'a> Line<'a> { + pub fn new(text: &'a str, offset: TextSize) -> Self { + Self { text, offset } + } + + #[inline] + pub const fn start(&self) -> TextSize { + self.offset + } + + /// Returns the byte offset where the line ends, including its terminating new line character. + #[inline] + pub fn full_end(&self) -> TextSize { + self.offset + self.full_text_len() + } + + /// Returns the byte offset where the line ends, excluding its new line character + #[inline] + pub fn end(&self) -> TextSize { + self.offset + self.as_str().text_len() + } + + /// Returns the range of the line, including its terminating new line character. + #[inline] + pub fn full_range(&self) -> TextRange { + TextRange::at(self.offset, self.text.text_len()) + } + + /// Returns the range of the line, excluding its terminating new line character + #[inline] + pub fn range(&self) -> TextRange { + TextRange::new(self.start(), self.end()) + } + + /// Returns the line's new line character, if any. + #[inline] + pub fn line_ending(&self) -> Option { + let mut bytes = self.text.bytes().rev(); + match bytes.next() { + Some(b'\n') => { + if bytes.next() == Some(b'\r') { + Some(LineEnding::CrLf) + } else { + Some(LineEnding::Lf) + } + } + Some(b'\r') => Some(LineEnding::Cr), + _ => None, + } + } + + /// Returns the text of the line, excluding the terminating new line character. + #[inline] + pub fn as_str(&self) -> &'a str { + let newline_len = self + .line_ending() + .map_or(0, |line_ending| line_ending.len()); + &self.text[..self.text.len() - newline_len] + } + + /// Returns the line's text, including the terminating new line character. + #[inline] + pub fn as_full_str(&self) -> &'a str { + self.text + } + + #[inline] + pub fn full_text_len(&self) -> TextSize { + self.text.text_len() + } +} + +impl Deref for Line<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl PartialEq<&str> for Line<'_> { + fn eq(&self, other: &&str) -> bool { + self.as_str() == *other + } +} + +impl PartialEq> for &str { + fn eq(&self, other: &Line<'_>) -> bool { + *self == other.as_str() + } +} + +/// The line ending style used in Python source code. +/// See +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum LineEnding { + Lf, + Cr, + CrLf, +} + +impl Default for LineEnding { + fn default() -> Self { + if cfg!(windows) { + LineEnding::CrLf + } else { + LineEnding::Lf + } + } +} + +impl LineEnding { + pub const fn as_str(&self) -> &'static str { + match self { + LineEnding::Lf => "\n", + LineEnding::CrLf => "\r\n", + LineEnding::Cr => "\r", + } + } + + #[allow(clippy::len_without_is_empty)] + pub const fn len(&self) -> usize { + match self { + LineEnding::Lf | LineEnding::Cr => 1, + LineEnding::CrLf => 2, + } + } + + pub const fn text_len(&self) -> TextSize { + match self { + LineEnding::Lf | LineEnding::Cr => TextSize::new(1), + LineEnding::CrLf => TextSize::new(2), + } + } +} + +impl Deref for LineEnding { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +#[cfg(test)] +mod tests { + use ruff_text_size::TextSize; + + use super::{Line, UniversalNewlineIterator}; + + #[test] + fn universal_newlines_empty_str() { + let lines: Vec<_> = UniversalNewlineIterator::from("").collect(); + assert_eq!(lines, Vec::::new()); + + let lines: Vec<_> = UniversalNewlineIterator::from("").rev().collect(); + assert_eq!(lines, Vec::::new()); + } + + #[test] + fn universal_newlines_forward() { + let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop").collect(); + assert_eq!( + lines, + vec![ + Line::new("foo\n", TextSize::from(0)), + Line::new("bar\n", TextSize::from(4)), + Line::new("\r\n", TextSize::from(8)), + Line::new("baz\r", TextSize::from(10)), + Line::new("bop", TextSize::from(14)), + ] + ); + + let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop\n").collect(); + assert_eq!( + lines, + vec![ + Line::new("foo\n", TextSize::from(0)), + Line::new("bar\n", TextSize::from(4)), + Line::new("\r\n", TextSize::from(8)), + Line::new("baz\r", TextSize::from(10)), + Line::new("bop\n", TextSize::from(14)), + ] + ); + + let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop\n\n").collect(); + assert_eq!( + lines, + vec![ + Line::new("foo\n", TextSize::from(0)), + Line::new("bar\n", TextSize::from(4)), + Line::new("\r\n", TextSize::from(8)), + Line::new("baz\r", TextSize::from(10)), + Line::new("bop\n", TextSize::from(14)), + Line::new("\n", TextSize::from(18)), + ] + ); + } + + #[test] + fn universal_newlines_backwards() { + let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop") + .rev() + .collect(); + assert_eq!( + lines, + vec![ + Line::new("bop", TextSize::from(14)), + Line::new("baz\r", TextSize::from(10)), + Line::new("\r\n", TextSize::from(8)), + Line::new("bar\n", TextSize::from(4)), + Line::new("foo\n", TextSize::from(0)), + ] + ); + + let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\nbaz\rbop\n") + .rev() + .map(|line| line.as_str()) + .collect(); + + assert_eq!( + lines, + vec![ + Line::new("bop\n", TextSize::from(13)), + Line::new("baz\r", TextSize::from(9)), + Line::new("\n", TextSize::from(8)), + Line::new("bar\n", TextSize::from(4)), + Line::new("foo\n", TextSize::from(0)), + ] + ); + } + + #[test] + fn universal_newlines_mixed() { + let mut lines = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop"); + + assert_eq!( + lines.next_back(), + Some(Line::new("bop", TextSize::from(14))) + ); + assert_eq!(lines.next(), Some(Line::new("foo\n", TextSize::from(0)))); + assert_eq!( + lines.next_back(), + Some(Line::new("baz\r", TextSize::from(10))) + ); + assert_eq!(lines.next(), Some(Line::new("bar\n", TextSize::from(4)))); + assert_eq!( + lines.next_back(), + Some(Line::new("\r\n", TextSize::from(8))) + ); + assert_eq!(lines.next(), None); + } +} diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml new file mode 100644 index 00000000..d5195450 --- /dev/null +++ b/crates/ruff_text_size/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ruff_text_size" +version = "0.0.0" +publish = false +edition = "2021" +rust-version = "1.67.1" + +[dependencies] +serde = { workspace = true, optional = true } +schemars = { workspace = true, optional = true } + +[dev-dependencies] +serde_test = { workspace = true } +static_assertions = { workspace = true } + +[features] +serde = ["dep:serde"] + +[lints] +workspace = true + +[[test]] +name = "serde" +path = "tests/serde.rs" +required-features = ["serde"] diff --git a/crates/ruff_text_size/src/lib.rs b/crates/ruff_text_size/src/lib.rs new file mode 100644 index 00000000..0276e0b3 --- /dev/null +++ b/crates/ruff_text_size/src/lib.rs @@ -0,0 +1,36 @@ +//! Newtypes for working with text sizes/ranges in a more type-safe manner. +//! +//! This library can help with two things: +//! * Reducing storage requirements for offsets and ranges, under the +//! assumption that 32 bits is enough. +//! * Providing standard vocabulary types for applications where text ranges +//! are pervasive. +//! +//! However, you should not use this library simply because you work with +//! strings. In the overwhelming majority of cases, using `usize` and +//! `std::ops::Range` is better. In particular, if you are publishing a +//! library, using only std types in the interface would make it more +//! interoperable. Similarly, if you are writing something like a lexer, which +//! produces, but does not *store* text ranges, then sticking to `usize` would +//! be better. +//! +//! Minimal Supported Rust Version: latest stable. + +#![forbid(unsafe_code)] +#![warn(missing_debug_implementations, missing_docs)] + +mod range; +mod size; +mod traits; + +#[cfg(feature = "schemars")] +mod schemars_impls; +#[cfg(feature = "serde")] +mod serde_impls; + +pub use crate::{ + range::TextRange, size::TextSize, traits::Ranged, traits::TextLen, traits::TextSlice, +}; + +#[cfg(target_pointer_width = "16")] +compile_error!("text-size assumes usize >= u32 and does not work on 16-bit targets"); diff --git a/crates/ruff_text_size/src/range.rs b/crates/ruff_text_size/src/range.rs new file mode 100644 index 00000000..aa517345 --- /dev/null +++ b/crates/ruff_text_size/src/range.rs @@ -0,0 +1,544 @@ +use cmp::Ordering; + +use { + crate::TextSize, + std::{ + cmp, fmt, + ops::{Add, AddAssign, Bound, Index, IndexMut, Range, RangeBounds, Sub, SubAssign}, + }, +}; + +/// A range in text, represented as a pair of [`TextSize`][struct@TextSize]. +/// +/// It is a logic error for `start` to be greater than `end`. +#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct TextRange { + // Invariant: start <= end + start: TextSize, + end: TextSize, +} + +impl fmt::Debug for TextRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}..{}", self.start().raw, self.end().raw) + } +} + +impl TextRange { + /// Creates a new `TextRange` with the given `start` and `end` (`start..end`). + /// + /// # Panics + /// + /// Panics if `end < start`. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// let start = TextSize::from(5); + /// let end = TextSize::from(10); + /// let range = TextRange::new(start, end); + /// + /// assert_eq!(range.start(), start); + /// assert_eq!(range.end(), end); + /// assert_eq!(range.len(), end - start); + /// ``` + #[inline] + pub const fn new(start: TextSize, end: TextSize) -> TextRange { + assert!(start.raw <= end.raw); + TextRange { start, end } + } + + /// Create a new `TextRange` with the given `offset` and `len` (`offset..offset + len`). + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// let text = "0123456789"; + /// + /// let offset = TextSize::from(2); + /// let length = TextSize::from(5); + /// let range = TextRange::at(offset, length); + /// + /// assert_eq!(range, TextRange::new(offset, offset + length)); + /// assert_eq!(&text[range], "23456") + /// ``` + #[inline] + pub fn at(offset: TextSize, len: TextSize) -> TextRange { + TextRange::new(offset, offset + len) + } + + /// Create a zero-length range at the specified offset (`offset..offset`). + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// let point: TextSize; + /// # point = TextSize::from(3); + /// let range = TextRange::empty(point); + /// assert!(range.is_empty()); + /// assert_eq!(range, TextRange::new(point, point)); + /// ``` + #[inline] + pub fn empty(offset: TextSize) -> TextRange { + TextRange { + start: offset, + end: offset, + } + } + + /// Create a range up to the given end (`..end`). + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// let point: TextSize; + /// # point = TextSize::from(12); + /// let range = TextRange::up_to(point); + /// + /// assert_eq!(range.len(), point); + /// assert_eq!(range, TextRange::new(0.into(), point)); + /// assert_eq!(range, TextRange::at(0.into(), point)); + /// ``` + #[inline] + pub fn up_to(end: TextSize) -> TextRange { + TextRange { + start: 0.into(), + end, + } + } +} + +/// Identity methods. +impl TextRange { + /// The start point of this range. + #[inline] + pub const fn start(self) -> TextSize { + self.start + } + + /// The end point of this range. + #[inline] + pub const fn end(self) -> TextSize { + self.end + } + + /// The size of this range. + #[inline] + pub const fn len(self) -> TextSize { + // HACK for const fn: math on primitives only + TextSize { + raw: self.end().raw - self.start().raw, + } + } + + /// Check if this range is empty. + #[inline] + pub const fn is_empty(self) -> bool { + // HACK for const fn: math on primitives only + self.start().raw == self.end().raw + } +} + +/// Manipulation methods. +impl TextRange { + /// Check if this range contains an offset. + /// + /// The end index is considered excluded. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// let (start, end): (TextSize, TextSize); + /// # start = 10.into(); end = 20.into(); + /// let range = TextRange::new(start, end); + /// assert!(range.contains(start)); + /// assert!(!range.contains(end)); + /// ``` + #[inline] + pub fn contains(self, offset: TextSize) -> bool { + self.start() <= offset && offset < self.end() + } + + /// Check if this range contains an offset. + /// + /// The end index is considered included. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// let (start, end): (TextSize, TextSize); + /// # start = 10.into(); end = 20.into(); + /// let range = TextRange::new(start, end); + /// assert!(range.contains_inclusive(start)); + /// assert!(range.contains_inclusive(end)); + /// ``` + #[inline] + pub fn contains_inclusive(self, offset: TextSize) -> bool { + self.start() <= offset && offset <= self.end() + } + + /// Check if this range completely contains another range. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// let larger = TextRange::new(0.into(), 20.into()); + /// let smaller = TextRange::new(5.into(), 15.into()); + /// assert!(larger.contains_range(smaller)); + /// assert!(!smaller.contains_range(larger)); + /// + /// // a range always contains itself + /// assert!(larger.contains_range(larger)); + /// assert!(smaller.contains_range(smaller)); + /// ``` + #[inline] + pub fn contains_range(self, other: TextRange) -> bool { + self.start() <= other.start() && other.end() <= self.end() + } + + /// The range covered by both ranges, if it exists. + /// If the ranges touch but do not overlap, the output range is empty. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// assert_eq!( + /// TextRange::intersect( + /// TextRange::new(0.into(), 10.into()), + /// TextRange::new(5.into(), 15.into()), + /// ), + /// Some(TextRange::new(5.into(), 10.into())), + /// ); + /// ``` + #[inline] + pub fn intersect(self, other: TextRange) -> Option { + let start = cmp::max(self.start(), other.start()); + let end = cmp::min(self.end(), other.end()); + if end < start { + return None; + } + Some(TextRange::new(start, end)) + } + + /// Extends the range to cover `other` as well. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// assert_eq!( + /// TextRange::cover( + /// TextRange::new(0.into(), 5.into()), + /// TextRange::new(15.into(), 20.into()), + /// ), + /// TextRange::new(0.into(), 20.into()), + /// ); + /// ``` + #[inline] + #[must_use] + pub fn cover(self, other: TextRange) -> TextRange { + let start = cmp::min(self.start(), other.start()); + let end = cmp::max(self.end(), other.end()); + TextRange::new(start, end) + } + + /// Extends the range to cover `other` offsets as well. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// assert_eq!( + /// TextRange::empty(0.into()).cover_offset(20.into()), + /// TextRange::new(0.into(), 20.into()), + /// ) + /// ``` + #[inline] + #[must_use] + pub fn cover_offset(self, offset: TextSize) -> TextRange { + self.cover(TextRange::empty(offset)) + } + + /// Add an offset to this range. + /// + /// Note that this is not appropriate for changing where a `TextRange` is + /// within some string; rather, it is for changing the reference anchor + /// that the `TextRange` is measured against. + /// + /// The unchecked version (`Add::add`) will _always_ panic on overflow, + /// in contrast to primitive integers, which check in debug mode only. + #[inline] + pub fn checked_add(self, offset: TextSize) -> Option { + Some(TextRange { + start: self.start.checked_add(offset)?, + end: self.end.checked_add(offset)?, + }) + } + + /// Subtract an offset from this range. + /// + /// Note that this is not appropriate for changing where a `TextRange` is + /// within some string; rather, it is for changing the reference anchor + /// that the `TextRange` is measured against. + /// + /// The unchecked version (`Sub::sub`) will _always_ panic on overflow, + /// in contrast to primitive integers, which check in debug mode only. + #[inline] + pub fn checked_sub(self, offset: TextSize) -> Option { + Some(TextRange { + start: self.start.checked_sub(offset)?, + end: self.end.checked_sub(offset)?, + }) + } + + /// Relative order of the two ranges (overlapping ranges are considered + /// equal). + /// + /// + /// This is useful when, for example, binary searching an array of disjoint + /// ranges. + /// + /// # Examples + /// + /// ``` + /// # use ruff_text_size::*; + /// # use std::cmp::Ordering; + /// + /// let a = TextRange::new(0.into(), 3.into()); + /// let b = TextRange::new(4.into(), 5.into()); + /// assert_eq!(a.ordering(b), Ordering::Less); + /// + /// let a = TextRange::new(0.into(), 3.into()); + /// let b = TextRange::new(3.into(), 5.into()); + /// assert_eq!(a.ordering(b), Ordering::Less); + /// + /// let a = TextRange::new(0.into(), 3.into()); + /// let b = TextRange::new(2.into(), 5.into()); + /// assert_eq!(a.ordering(b), Ordering::Equal); + /// + /// let a = TextRange::new(0.into(), 3.into()); + /// let b = TextRange::new(2.into(), 2.into()); + /// assert_eq!(a.ordering(b), Ordering::Equal); + /// + /// let a = TextRange::new(2.into(), 3.into()); + /// let b = TextRange::new(2.into(), 2.into()); + /// assert_eq!(a.ordering(b), Ordering::Greater); + /// ``` + #[inline] + pub fn ordering(self, other: TextRange) -> Ordering { + if self.end() <= other.start() { + Ordering::Less + } else if other.end() <= self.start() { + Ordering::Greater + } else { + Ordering::Equal + } + } + + /// Subtracts an offset from the start position. + /// + /// + /// ## Panics + /// If `start - amount` is less than zero. + /// + /// ## Examples + /// + /// ``` + /// use ruff_text_size::{Ranged, TextRange, TextSize}; + /// + /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); + /// assert_eq!(range.sub_start(TextSize::from(2)), TextRange::new(TextSize::from(3), TextSize::from(10))); + /// ``` + #[inline] + #[must_use] + pub fn sub_start(&self, amount: TextSize) -> TextRange { + TextRange::new(self.start() - amount, self.end()) + } + + /// Adds an offset to the start position. + /// + /// ## Panics + /// If `start + amount > end` + /// + /// ## Examples + /// + /// ``` + /// use ruff_text_size::{Ranged, TextRange, TextSize}; + /// + /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); + /// assert_eq!(range.add_start(TextSize::from(3)), TextRange::new(TextSize::from(8), TextSize::from(10))); + /// ``` + #[inline] + #[must_use] + pub fn add_start(&self, amount: TextSize) -> TextRange { + TextRange::new(self.start() + amount, self.end()) + } + + /// Subtracts an offset from the end position. + /// + /// + /// ## Panics + /// If `end - amount < 0` or `end - amount < start` + /// + /// ## Examples + /// + /// ``` + /// use ruff_text_size::{Ranged, TextRange, TextSize}; + /// + /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); + /// assert_eq!(range.sub_end(TextSize::from(2)), TextRange::new(TextSize::from(5), TextSize::from(8))); + /// ``` + #[inline] + #[must_use] + pub fn sub_end(&self, amount: TextSize) -> TextRange { + TextRange::new(self.start(), self.end() - amount) + } + + /// Adds an offset to the end position. + /// + /// + /// ## Panics + /// If `end + amount > u32::MAX` + /// + /// ## Examples + /// + /// ``` + /// use ruff_text_size::{Ranged, TextRange, TextSize}; + /// + /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); + /// assert_eq!(range.add_end(TextSize::from(2)), TextRange::new(TextSize::from(5), TextSize::from(12))); + /// ``` + #[inline] + #[must_use] + pub fn add_end(&self, amount: TextSize) -> TextRange { + TextRange::new(self.start(), self.end() + amount) + } +} + +impl Index for str { + type Output = str; + #[inline] + fn index(&self, index: TextRange) -> &str { + &self[Range::::from(index)] + } +} + +impl Index for String { + type Output = str; + #[inline] + fn index(&self, index: TextRange) -> &str { + &self[Range::::from(index)] + } +} + +impl IndexMut for str { + #[inline] + fn index_mut(&mut self, index: TextRange) -> &mut str { + &mut self[Range::::from(index)] + } +} + +impl IndexMut for String { + #[inline] + fn index_mut(&mut self, index: TextRange) -> &mut str { + &mut self[Range::::from(index)] + } +} + +impl RangeBounds for TextRange { + fn start_bound(&self) -> Bound<&TextSize> { + Bound::Included(&self.start) + } + + fn end_bound(&self) -> Bound<&TextSize> { + Bound::Excluded(&self.end) + } +} + +impl From> for TextRange { + #[inline] + fn from(r: Range) -> Self { + TextRange::new(r.start, r.end) + } +} + +impl From for Range +where + T: From, +{ + #[inline] + fn from(r: TextRange) -> Self { + r.start().into()..r.end().into() + } +} + +macro_rules! ops { + (impl $Op:ident for TextRange by fn $f:ident = $op:tt) => { + impl $Op<&TextSize> for TextRange { + type Output = TextRange; + #[inline] + fn $f(self, other: &TextSize) -> TextRange { + self $op *other + } + } + impl $Op for &TextRange + where + TextRange: $Op, + { + type Output = TextRange; + #[inline] + fn $f(self, other: T) -> TextRange { + *self $op other + } + } + }; +} + +impl Add for TextRange { + type Output = TextRange; + #[inline] + fn add(self, offset: TextSize) -> TextRange { + self.checked_add(offset) + .expect("TextRange +offset overflowed") + } +} + +impl Sub for TextRange { + type Output = TextRange; + #[inline] + fn sub(self, offset: TextSize) -> TextRange { + self.checked_sub(offset) + .expect("TextRange -offset overflowed") + } +} + +ops!(impl Add for TextRange by fn add = +); +ops!(impl Sub for TextRange by fn sub = -); + +impl AddAssign for TextRange +where + TextRange: Add, +{ + #[inline] + fn add_assign(&mut self, rhs: A) { + *self = *self + rhs; + } +} + +impl SubAssign for TextRange +where + TextRange: Sub, +{ + #[inline] + fn sub_assign(&mut self, rhs: S) { + *self = *self - rhs; + } +} diff --git a/crates/ruff_text_size/src/schemars_impls.rs b/crates/ruff_text_size/src/schemars_impls.rs new file mode 100644 index 00000000..a1c7fa36 --- /dev/null +++ b/crates/ruff_text_size/src/schemars_impls.rs @@ -0,0 +1,33 @@ +//! This module implements the [`JsonSchema`] trait from the `schemars` crate for +//! [`TextSize`] and [`TextRange`] if the `schemars` feature is enabled. This trait +//! exposes meta-information on how a given type is serialized and deserialized +//! using `serde`, and is currently used to generate autocomplete information +//! for the `rome.json` configuration file and TypeScript types for the node.js +//! bindings to the Workspace API + +use crate::{TextRange, TextSize}; +use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; + +impl JsonSchema for TextSize { + fn schema_name() -> String { + String::from("TextSize") + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + // TextSize is represented as a raw u32, see serde_impls.rs for the + // actual implementation + ::json_schema(gen) + } +} + +impl JsonSchema for TextRange { + fn schema_name() -> String { + String::from("TextRange") + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + // TextSize is represented as (TextSize, TextSize), see serde_impls.rs + // for the actual implementation + <(TextSize, TextSize)>::json_schema(gen) + } +} diff --git a/crates/ruff_text_size/src/serde_impls.rs b/crates/ruff_text_size/src/serde_impls.rs new file mode 100644 index 00000000..b6885d67 --- /dev/null +++ b/crates/ruff_text_size/src/serde_impls.rs @@ -0,0 +1,47 @@ +use { + crate::{TextRange, TextSize}, + serde::{de, Deserialize, Deserializer, Serialize, Serializer}, +}; + +impl Serialize for TextSize { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.raw.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for TextSize { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + u32::deserialize(deserializer).map(TextSize::from) + } +} + +impl Serialize for TextRange { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + (self.start(), self.end()).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for TextRange { + #[allow(clippy::nonminimal_bool)] + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (start, end) = Deserialize::deserialize(deserializer)?; + if !(start <= end) { + return Err(de::Error::custom(format!( + "invalid range: {start:?}..{end:?}" + ))); + } + Ok(TextRange::new(start, end)) + } +} diff --git a/crates/ruff_text_size/src/size.rs b/crates/ruff_text_size/src/size.rs new file mode 100644 index 00000000..1b597698 --- /dev/null +++ b/crates/ruff_text_size/src/size.rs @@ -0,0 +1,196 @@ +use { + crate::TextLen, + std::{ + convert::TryFrom, + fmt, iter, + num::TryFromIntError, + ops::{Add, AddAssign, Sub, SubAssign}, + }, +}; + +/// A measure of text length. Also, equivalently, an index into text. +/// +/// This is a UTF-8 bytes offset stored as `u32`, but +/// most clients should treat it as an opaque measure. +/// +/// For cases that need to escape `TextSize` and return to working directly +/// with primitive integers, `TextSize` can be converted losslessly to/from +/// `u32` via [`From`] conversions as well as losslessly be converted [`Into`] +/// `usize`. The `usize -> TextSize` direction can be done via [`TryFrom`]. +/// +/// These escape hatches are primarily required for unit testing and when +/// converting from UTF-8 size to another coordinate space, such as UTF-16. +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TextSize { + pub(crate) raw: u32, +} + +impl fmt::Debug for TextSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.raw) + } +} + +impl TextSize { + /// Creates a new `TextSize` at the given `offset`. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// assert_eq!(TextSize::from(4), TextSize::new(4)); + /// ``` + pub const fn new(offset: u32) -> Self { + Self { raw: offset } + } + + /// The text size of some primitive text-like object. + /// + /// Accepts `char`, `&str`, and `&String`. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// let char_size = TextSize::of('🦀'); + /// assert_eq!(char_size, TextSize::from(4)); + /// + /// let str_size = TextSize::of("rust-analyzer"); + /// assert_eq!(str_size, TextSize::from(13)); + /// ``` + #[inline] + pub fn of(text: T) -> TextSize { + text.text_len() + } + + /// Returns current raw `offset` as u32. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// assert_eq!(TextSize::from(4).to_u32(), 4); + /// ``` + pub const fn to_u32(&self) -> u32 { + self.raw + } + + /// Returns current raw `offset` as usize. + /// + /// # Examples + /// + /// ```rust + /// # use ruff_text_size::*; + /// assert_eq!(TextSize::from(4).to_usize(), 4); + /// ``` + pub const fn to_usize(&self) -> usize { + self.raw as usize + } +} + +/// Methods to act like a primitive integer type, where reasonably applicable. +// Last updated for parity with Rust 1.42.0. +impl TextSize { + /// Checked addition. Returns `None` if overflow occurred. + #[inline] + pub fn checked_add(self, rhs: TextSize) -> Option { + self.raw.checked_add(rhs.raw).map(|raw| TextSize { raw }) + } + + /// Checked subtraction. Returns `None` if overflow occurred. + #[inline] + pub fn checked_sub(self, rhs: TextSize) -> Option { + self.raw.checked_sub(rhs.raw).map(|raw| TextSize { raw }) + } +} + +impl From for TextSize { + #[inline] + fn from(raw: u32) -> Self { + TextSize::new(raw) + } +} + +impl From for u32 { + #[inline] + fn from(value: TextSize) -> Self { + value.to_u32() + } +} + +impl TryFrom for TextSize { + type Error = TryFromIntError; + #[inline] + fn try_from(value: usize) -> Result { + Ok(u32::try_from(value)?.into()) + } +} + +impl From for usize { + #[inline] + fn from(value: TextSize) -> Self { + value.to_usize() + } +} + +macro_rules! ops { + (impl $Op:ident for TextSize by fn $f:ident = $op:tt) => { + impl $Op for TextSize { + type Output = TextSize; + #[inline] + fn $f(self, other: TextSize) -> TextSize { + TextSize { raw: self.raw $op other.raw } + } + } + impl $Op<&TextSize> for TextSize { + type Output = TextSize; + #[inline] + fn $f(self, other: &TextSize) -> TextSize { + self $op *other + } + } + impl $Op for &TextSize + where + TextSize: $Op, + { + type Output = TextSize; + #[inline] + fn $f(self, other: T) -> TextSize { + *self $op other + } + } + }; +} + +ops!(impl Add for TextSize by fn add = +); +ops!(impl Sub for TextSize by fn sub = -); + +impl AddAssign for TextSize +where + TextSize: Add, +{ + #[inline] + fn add_assign(&mut self, rhs: A) { + *self = *self + rhs; + } +} + +impl SubAssign for TextSize +where + TextSize: Sub, +{ + #[inline] + fn sub_assign(&mut self, rhs: S) { + *self = *self - rhs; + } +} + +impl iter::Sum for TextSize +where + TextSize: Add, +{ + #[inline] + fn sum>(iter: I) -> TextSize { + iter.fold(0.into(), Add::add) + } +} diff --git a/crates/ruff_text_size/src/traits.rs b/crates/ruff_text_size/src/traits.rs new file mode 100644 index 00000000..0ea01513 --- /dev/null +++ b/crates/ruff_text_size/src/traits.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; +use {crate::TextRange, crate::TextSize, std::convert::TryInto}; + +use priv_in_pub::Sealed; +mod priv_in_pub { + pub trait Sealed {} +} + +/// Primitives with a textual length that can be passed to [`TextSize::of`]. +pub trait TextLen: Copy + Sealed { + /// The textual length of this primitive. + fn text_len(self) -> TextSize; +} + +impl Sealed for &'_ str {} +impl TextLen for &'_ str { + #[inline] + fn text_len(self) -> TextSize { + self.len().try_into().unwrap() + } +} + +impl Sealed for &'_ String {} +impl TextLen for &'_ String { + #[inline] + fn text_len(self) -> TextSize { + self.as_str().text_len() + } +} + +impl Sealed for char {} +impl TextLen for char { + #[inline] + #[allow(clippy::cast_possible_truncation)] + fn text_len(self) -> TextSize { + (self.len_utf8() as u32).into() + } +} + +/// A ranged item in the source text. +pub trait Ranged { + /// The range of this item in the source text. + fn range(&self) -> TextRange; + + /// The start offset of this item in the source text. + fn start(&self) -> TextSize { + self.range().start() + } + + /// The end offset of this item in the source text. + fn end(&self) -> TextSize { + self.range().end() + } +} + +impl Ranged for TextRange { + fn range(&self) -> TextRange { + *self + } +} + +impl Ranged for &T +where + T: Ranged, +{ + fn range(&self) -> TextRange { + T::range(self) + } +} + +impl Ranged for Arc +where + T: Ranged, +{ + fn range(&self) -> TextRange { + T::range(self) + } +} + +/// A slice of the source text. +pub trait TextSlice: Sealed { + /// Returns the slice of the text within the given `range`. + /// + /// ## Note + /// + /// This is the same as `&self[range]` if `self` is a `str` and `range` a `TextRange`. + /// + /// ## Panics + /// If the range is out of bounds. + fn slice(&self, range: impl Ranged) -> &str; +} + +impl Sealed for str {} + +impl TextSlice for str { + fn slice(&self, ranged: impl Ranged) -> &str { + &self[ranged.range()] + } +} diff --git a/crates/ruff_text_size/tests/auto_traits.rs b/crates/ruff_text_size/tests/auto_traits.rs new file mode 100644 index 00000000..6adf8bd2 --- /dev/null +++ b/crates/ruff_text_size/tests/auto_traits.rs @@ -0,0 +1,18 @@ +use { + ruff_text_size::{TextRange, TextSize}, + static_assertions::assert_impl_all, + std::{ + fmt::Debug, + hash::Hash, + marker::{Send, Sync}, + panic::{RefUnwindSafe, UnwindSafe}, + }, +}; + +// auto traits +assert_impl_all!(TextSize: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); +assert_impl_all!(TextRange: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); + +// common traits +assert_impl_all!(TextSize: Copy, Debug, Default, Hash, Ord); +assert_impl_all!(TextRange: Copy, Debug, Default, Hash, Eq); diff --git a/crates/ruff_text_size/tests/constructors.rs b/crates/ruff_text_size/tests/constructors.rs new file mode 100644 index 00000000..8ee2fde9 --- /dev/null +++ b/crates/ruff_text_size/tests/constructors.rs @@ -0,0 +1,24 @@ +use ruff_text_size::TextSize; + +#[derive(Copy, Clone)] +struct BadRope<'a>(&'a [&'a str]); + +impl BadRope<'_> { + fn text_len(self) -> TextSize { + self.0.iter().copied().map(TextSize::of).sum() + } +} + +#[test] +fn main() { + let x: char = 'c'; + let _ = TextSize::of(x); + + let x: &str = "hello"; + let _ = TextSize::of(x); + + let x: &String = &"hello".into(); + let _ = TextSize::of(x); + + let _ = BadRope(&[""]).text_len(); +} diff --git a/crates/ruff_text_size/tests/indexing.rs b/crates/ruff_text_size/tests/indexing.rs new file mode 100644 index 00000000..e7205fde --- /dev/null +++ b/crates/ruff_text_size/tests/indexing.rs @@ -0,0 +1,8 @@ +use ruff_text_size::TextRange; + +#[test] +fn main() { + let range = TextRange::default(); + let _ = &""[range]; + let _ = &String::new()[range]; +} diff --git a/crates/ruff_text_size/tests/main.rs b/crates/ruff_text_size/tests/main.rs new file mode 100644 index 00000000..8d8dd091 --- /dev/null +++ b/crates/ruff_text_size/tests/main.rs @@ -0,0 +1,79 @@ +use { + ruff_text_size::{TextRange, TextSize}, + std::ops, +}; + +fn size(x: u32) -> TextSize { + TextSize::from(x) +} + +fn range(x: ops::Range) -> TextRange { + TextRange::new(x.start.into(), x.end.into()) +} + +#[test] +fn sum() { + let xs: Vec = vec![size(0), size(1), size(2)]; + assert_eq!(xs.iter().sum::(), size(3)); + assert_eq!(xs.into_iter().sum::(), size(3)); +} + +#[test] +fn math() { + assert_eq!(size(10) + size(5), size(15)); + assert_eq!(size(10) - size(5), size(5)); +} + +#[test] +fn checked_math() { + assert_eq!(size(1).checked_add(size(1)), Some(size(2))); + assert_eq!(size(1).checked_sub(size(1)), Some(size(0))); + assert_eq!(size(1).checked_sub(size(2)), None); + assert_eq!(size(!0).checked_add(size(1)), None); +} + +#[test] +#[rustfmt::skip] +fn contains() { + assert!( range(2..4).contains_range(range(2..3))); + assert!( ! range(2..4).contains_range(range(1..3))); +} + +#[test] +fn intersect() { + assert_eq!(range(1..2).intersect(range(2..3)), Some(range(2..2))); + assert_eq!(range(1..5).intersect(range(2..3)), Some(range(2..3))); + assert_eq!(range(1..2).intersect(range(3..4)), None); +} + +#[test] +fn cover() { + assert_eq!(range(1..2).cover(range(2..3)), range(1..3)); + assert_eq!(range(1..5).cover(range(2..3)), range(1..5)); + assert_eq!(range(1..2).cover(range(4..5)), range(1..5)); +} + +#[test] +fn cover_offset() { + assert_eq!(range(1..3).cover_offset(size(0)), range(0..3)); + assert_eq!(range(1..3).cover_offset(size(1)), range(1..3)); + assert_eq!(range(1..3).cover_offset(size(2)), range(1..3)); + assert_eq!(range(1..3).cover_offset(size(3)), range(1..3)); + assert_eq!(range(1..3).cover_offset(size(4)), range(1..4)); +} + +#[test] +#[rustfmt::skip] +fn contains_point() { + assert!( ! range(1..3).contains(size(0))); + assert!( range(1..3).contains(size(1))); + assert!( range(1..3).contains(size(2))); + assert!( ! range(1..3).contains(size(3))); + assert!( ! range(1..3).contains(size(4))); + + assert!( ! range(1..3).contains_inclusive(size(0))); + assert!( range(1..3).contains_inclusive(size(1))); + assert!( range(1..3).contains_inclusive(size(2))); + assert!( range(1..3).contains_inclusive(size(3))); + assert!( ! range(1..3).contains_inclusive(size(4))); +} diff --git a/crates/ruff_text_size/tests/serde.rs b/crates/ruff_text_size/tests/serde.rs new file mode 100644 index 00000000..0d8f9d4a --- /dev/null +++ b/crates/ruff_text_size/tests/serde.rs @@ -0,0 +1,83 @@ +use { + ruff_text_size::{TextRange, TextSize}, + serde_test::{assert_de_tokens_error, assert_tokens, Token}, + std::ops, +}; + +fn size(x: u32) -> TextSize { + TextSize::from(x) +} + +fn range(x: ops::Range) -> TextRange { + TextRange::new(x.start.into(), x.end.into()) +} + +#[test] +fn size_serialization() { + assert_tokens(&size(00), &[Token::U32(00)]); + assert_tokens(&size(10), &[Token::U32(10)]); + assert_tokens(&size(20), &[Token::U32(20)]); + assert_tokens(&size(30), &[Token::U32(30)]); +} + +#[test] +fn range_serialization() { + assert_tokens( + &range(00..10), + &[ + Token::Tuple { len: 2 }, + Token::U32(00), + Token::U32(10), + Token::TupleEnd, + ], + ); + assert_tokens( + &range(10..20), + &[ + Token::Tuple { len: 2 }, + Token::U32(10), + Token::U32(20), + Token::TupleEnd, + ], + ); + assert_tokens( + &range(20..30), + &[ + Token::Tuple { len: 2 }, + Token::U32(20), + Token::U32(30), + Token::TupleEnd, + ], + ); + assert_tokens( + &range(30..40), + &[ + Token::Tuple { len: 2 }, + Token::U32(30), + Token::U32(40), + Token::TupleEnd, + ], + ); +} + +#[test] +fn invalid_range_deserialization() { + assert_tokens::( + &range(62..92), + &[ + Token::Tuple { len: 2 }, + Token::U32(62), + Token::U32(92), + Token::TupleEnd, + ], + ); + assert_de_tokens_error::( + &[ + Token::Tuple { len: 2 }, + Token::U32(92), + Token::U32(62), + Token::TupleEnd, + ], + "invalid range: 92..62", + ); +} From cfc19e90b2e954c798e055853446f09c16e48aaa Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 26 Dec 2024 12:05:35 -0500 Subject: [PATCH 03/44] Review `ruff_source_file`, merge in our line ending tools --- Cargo.lock | 10 +- Cargo.toml | 1 - crates/air/Cargo.toml | 3 +- crates/air/src/commands/format.rs | 2 +- crates/air_formatter_test/Cargo.toml | 2 +- crates/air_formatter_test/src/spec.rs | 2 +- crates/air_r_formatter/Cargo.toml | 1 - crates/air_r_parser/Cargo.toml | 2 +- crates/air_r_parser/tests/spec_test.rs | 2 +- crates/ruff_source_file/src/lib.rs | 278 +--------- crates/ruff_source_file/src/line_index.rs | 98 +--- crates/ruff_source_file/src/line_ranges.rs | 396 -------------- crates/ruff_source_file/src/newlines.rs | 488 ++++-------------- crates/ruff_source_file/src/one_indexed.rs | 100 ++++ .../ruff_source_file/src/source_location.rs | 34 ++ crates/workspace/Cargo.toml | 2 +- crates/workspace/src/settings.rs | 15 +- 17 files changed, 246 insertions(+), 1190 deletions(-) delete mode 100644 crates/ruff_source_file/src/line_ranges.rs create mode 100644 crates/ruff_source_file/src/one_indexed.rs create mode 100644 crates/ruff_source_file/src/source_location.rs diff --git a/Cargo.lock b/Cargo.lock index 1d25a354..8ca7f2f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,9 +41,8 @@ dependencies = [ "fs", "ignore", "itertools", - "line_ending", - "lsp", "ruff_server", + "ruff_source_file", "tempfile", "thiserror 2.0.5", "tracing", @@ -60,7 +59,7 @@ dependencies = [ "biome_parser", "biome_rowan", "insta", - "line_ending", + "ruff_source_file", "serde", "similar", "similar-asserts", @@ -85,7 +84,6 @@ dependencies = [ "biome_parser", "biome_rowan", "itertools", - "line_ending", "tests_macros", "tracing", ] @@ -102,7 +100,7 @@ dependencies = [ "biome_rowan", "biome_unicode_table", "insta", - "line_ending", + "ruff_source_file", "serde", "tests_macros", "tracing", @@ -2796,7 +2794,7 @@ dependencies = [ "fs", "ignore", "insta", - "line_ending", + "ruff_source_file", "rustc-hash", "serde", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 840ae1f2..e1e82142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ air_r_syntax = { path = "./crates/air_r_syntax" } biome_ungrammar = { path = "./crates/biome_ungrammar" } fs = { path = "./crates/fs" } line_ending = { path = "./crates/line_ending" } -lsp = { path = "./crates/lsp" } lsp_test = { path = "./crates/lsp_test" } tests_macros = { path = "./crates/tests_macros" } ruff_server = { path = "./crates/ruff_server" } diff --git a/crates/air/Cargo.toml b/crates/air/Cargo.toml index 94367be8..120bce02 100644 --- a/crates/air/Cargo.toml +++ b/crates/air/Cargo.toml @@ -23,9 +23,8 @@ clap = { workspace = true, features = ["wrap_help"] } fs = { workspace = true } ignore = { workspace = true } itertools = { workspace = true } -line_ending = { workspace = true } -lsp = { workspace = true } ruff_server = { workspace = true } +ruff_source_file = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } workspace = { workspace = true } diff --git a/crates/air/src/commands/format.rs b/crates/air/src/commands/format.rs index d2feacf8..b6dca832 100644 --- a/crates/air/src/commands/format.rs +++ b/crates/air/src/commands/format.rs @@ -129,7 +129,7 @@ fn format_file( let options = settings.to_format_options(&source); - let source = line_ending::normalize(source); + let source = ruff_source_file::normalize_crlf_newlines(source); let formatted = match format_source(source.as_str(), options) { Ok(formatted) => formatted, Err(err) => return Err(FormatCommandError::Format(path.clone(), err)), diff --git a/crates/air_formatter_test/Cargo.toml b/crates/air_formatter_test/Cargo.toml index 86bbe764..f61f11b2 100644 --- a/crates/air_formatter_test/Cargo.toml +++ b/crates/air_formatter_test/Cargo.toml @@ -21,7 +21,7 @@ biome_formatter = { workspace = true } biome_parser = { workspace = true } biome_rowan = { workspace = true } insta = { workspace = true, features = ["glob"] } -line_ending = { workspace = true } +ruff_source_file = { workspace = true } serde = { workspace = true, features = ["derive"] } similar = "2.6.0" similar-asserts = "1.6.0" diff --git a/crates/air_formatter_test/src/spec.rs b/crates/air_formatter_test/src/spec.rs index 78e93112..c0fea238 100644 --- a/crates/air_formatter_test/src/spec.rs +++ b/crates/air_formatter_test/src/spec.rs @@ -29,7 +29,7 @@ impl<'a> SpecTestFile<'a> { let input_code = std::fs::read_to_string(input_file).unwrap(); // Normalize to Unix line endings - let input_code = line_ending::normalize(input_code); + let input_code = ruff_source_file::normalize_crlf_newlines(input_code); // For the whole file, not a specific range right now let range_start_index = None; diff --git a/crates/air_r_formatter/Cargo.toml b/crates/air_r_formatter/Cargo.toml index b64f0406..2b09fce6 100644 --- a/crates/air_r_formatter/Cargo.toml +++ b/crates/air_r_formatter/Cargo.toml @@ -24,7 +24,6 @@ tracing = { workspace = true } air_formatter_test = { workspace = true } air_r_parser = { workspace = true } biome_parser = { workspace = true } -line_ending = { workspace = true } tests_macros = { workspace = true } [lints] diff --git a/crates/air_r_parser/Cargo.toml b/crates/air_r_parser/Cargo.toml index 6c41d910..491557f7 100644 --- a/crates/air_r_parser/Cargo.toml +++ b/crates/air_r_parser/Cargo.toml @@ -28,7 +28,7 @@ tree-sitter-r = { workspace = true } biome_console = { workspace = true } biome_diagnostics = { workspace = true } insta = { workspace = true } -line_ending = { workspace = true } +ruff_source_file = { workspace = true } tests_macros = { workspace = true } # cargo-workspaces metadata diff --git a/crates/air_r_parser/tests/spec_test.rs b/crates/air_r_parser/tests/spec_test.rs index d22860a9..f70f365d 100644 --- a/crates/air_r_parser/tests/spec_test.rs +++ b/crates/air_r_parser/tests/spec_test.rs @@ -53,7 +53,7 @@ pub fn run(test_case: &str, _snapshot_name: &str, test_directory: &str, outcome_ .expect("Expected test path to be a readable file in UTF8 encoding"); // Normalize to Unix line endings - let content = line_ending::normalize(content); + let content = ruff_source_file::normalize_crlf_newlines(content); let options = RParserOptions::default(); let parsed = parse(&content, options); diff --git a/crates/ruff_source_file/src/lib.rs b/crates/ruff_source_file/src/lib.rs index 5bf43e3a..c36dbcd0 100644 --- a/crates/ruff_source_file/src/lib.rs +++ b/crates/ruff_source_file/src/lib.rs @@ -1,278 +1,12 @@ -use std::cmp::Ordering; -use std::fmt::{Debug, Display, Formatter}; -use std::sync::{Arc, OnceLock}; - #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use ruff_text_size::{Ranged, TextRange, TextSize}; - -pub use crate::line_index::{LineIndex, OneIndexed}; -pub use crate::line_ranges::LineRanges; -pub use crate::newlines::{ - find_newline, Line, LineEnding, NewlineWithTrailingNewline, UniversalNewlineIterator, - UniversalNewlines, -}; +pub use crate::line_index::LineIndex; +pub use crate::newlines::{find_newline, normalize_crlf_newlines, LineEnding}; +pub use crate::one_indexed::OneIndexed; +pub use crate::source_location::SourceLocation; mod line_index; -mod line_ranges; mod newlines; - -/// Gives access to the source code of a file and allows mapping between [`TextSize`] and [`SourceLocation`]. -#[derive(Debug)] -pub struct SourceCode<'src, 'index> { - text: &'src str, - index: &'index LineIndex, -} - -impl<'src, 'index> SourceCode<'src, 'index> { - pub fn new(content: &'src str, index: &'index LineIndex) -> Self { - Self { - text: content, - index, - } - } - - /// Computes the one indexed row and column numbers for `offset`. - #[inline] - pub fn source_location(&self, offset: TextSize) -> SourceLocation { - self.index.source_location(offset, self.text) - } - - #[inline] - pub fn line_index(&self, offset: TextSize) -> OneIndexed { - self.index.line_index(offset) - } - - /// Take the source code up to the given [`TextSize`]. - #[inline] - pub fn up_to(&self, offset: TextSize) -> &'src str { - &self.text[TextRange::up_to(offset)] - } - - /// Take the source code after the given [`TextSize`]. - #[inline] - pub fn after(&self, offset: TextSize) -> &'src str { - &self.text[usize::from(offset)..] - } - - /// Take the source code between the given [`TextRange`]. - pub fn slice(&self, ranged: T) -> &'src str { - &self.text[ranged.range()] - } - - pub fn line_start(&self, line: OneIndexed) -> TextSize { - self.index.line_start(line, self.text) - } - - pub fn line_end(&self, line: OneIndexed) -> TextSize { - self.index.line_end(line, self.text) - } - - pub fn line_end_exclusive(&self, line: OneIndexed) -> TextSize { - self.index.line_end_exclusive(line, self.text) - } - - pub fn line_range(&self, line: OneIndexed) -> TextRange { - self.index.line_range(line, self.text) - } - - /// Returns the source text of the line with the given index - #[inline] - pub fn line_text(&self, index: OneIndexed) -> &'src str { - let range = self.index.line_range(index, self.text); - &self.text[range] - } - - /// Returns the source text - pub fn text(&self) -> &'src str { - self.text - } - - /// Returns the number of lines - #[inline] - pub fn line_count(&self) -> usize { - self.index.line_count() - } -} - -impl PartialEq for SourceCode<'_, '_> { - fn eq(&self, other: &Self) -> bool { - self.text == other.text - } -} - -impl Eq for SourceCode<'_, '_> {} - -/// A Builder for constructing a [`SourceFile`] -pub struct SourceFileBuilder { - name: Box, - code: Box, - index: Option, -} - -impl SourceFileBuilder { - /// Creates a new builder for a file named `name`. - pub fn new>, Code: Into>>(name: Name, code: Code) -> Self { - Self { - name: name.into(), - code: code.into(), - index: None, - } - } - - #[must_use] - pub fn line_index(mut self, index: LineIndex) -> Self { - self.index = Some(index); - self - } - - pub fn set_line_index(&mut self, index: LineIndex) { - self.index = Some(index); - } - - /// Consumes `self` and returns the [`SourceFile`]. - pub fn finish(self) -> SourceFile { - let index = if let Some(index) = self.index { - OnceLock::from(index) - } else { - OnceLock::new() - }; - - SourceFile { - inner: Arc::new(SourceFileInner { - name: self.name, - code: self.code, - line_index: index, - }), - } - } -} - -/// A source file that is identified by its name. Optionally stores the source code and [`LineIndex`]. -/// -/// Cloning a [`SourceFile`] is cheap, because it only requires bumping a reference count. -#[derive(Clone, Eq, PartialEq)] -pub struct SourceFile { - inner: Arc, -} - -impl Debug for SourceFile { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SourceFile") - .field("name", &self.name()) - .field("code", &self.source_text()) - .finish() - } -} - -impl SourceFile { - /// Returns the name of the source file (filename). - #[inline] - pub fn name(&self) -> &str { - &self.inner.name - } - - #[inline] - pub fn slice(&self, range: TextRange) -> &str { - &self.source_text()[range] - } - - pub fn to_source_code(&self) -> SourceCode { - SourceCode { - text: self.source_text(), - index: self.index(), - } - } - - fn index(&self) -> &LineIndex { - self.inner - .line_index - .get_or_init(|| LineIndex::from_source_text(self.source_text())) - } - - /// Returns the source code. - #[inline] - pub fn source_text(&self) -> &str { - &self.inner.code - } -} - -impl PartialOrd for SourceFile { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for SourceFile { - fn cmp(&self, other: &Self) -> Ordering { - // Short circuit if these are the same source files - if Arc::ptr_eq(&self.inner, &other.inner) { - Ordering::Equal - } else { - self.inner.name.cmp(&other.inner.name) - } - } -} - -struct SourceFileInner { - name: Box, - code: Box, - line_index: OnceLock, -} - -impl PartialEq for SourceFileInner { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.code == other.code - } -} - -impl Eq for SourceFileInner {} - -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct SourceLocation { - pub row: OneIndexed, - pub column: OneIndexed, -} - -impl Default for SourceLocation { - fn default() -> Self { - Self { - row: OneIndexed::MIN, - column: OneIndexed::MIN, - } - } -} - -impl Debug for SourceLocation { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SourceLocation") - .field("row", &self.row.get()) - .field("column", &self.column.get()) - .finish() - } -} - -impl std::fmt::Display for SourceLocation { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{row}:{column}", row = self.row, column = self.column) - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum SourceRow { - /// A row within a cell in a Jupyter Notebook. - Notebook { cell: OneIndexed, line: OneIndexed }, - /// A row within a source file. - SourceFile { line: OneIndexed }, -} - -impl Display for SourceRow { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - SourceRow::Notebook { cell, line } => write!(f, "cell {cell}, line {line}"), - SourceRow::SourceFile { line } => write!(f, "line {line}"), - } - } -} +mod one_indexed; +mod source_location; diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs index e9d211ca..ae60e06e 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/ruff_source_file/src/line_index.rs @@ -1,14 +1,13 @@ use std::fmt; use std::fmt::{Debug, Formatter}; -use std::num::{NonZeroUsize, ParseIntError}; use std::ops::Deref; -use std::str::FromStr; use std::sync::Arc; use ruff_text_size::{TextLen, TextRange, TextSize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use crate::OneIndexed; use crate::SourceLocation; /// Index for fast [byte offset](TextSize) to [`SourceLocation`] conversions. @@ -335,101 +334,6 @@ impl IndexKind { } } -/// Type-safe wrapper for a value whose logical range starts at `1`, for -/// instance the line or column numbers in a file -/// -/// Internally this is represented as a [`NonZeroUsize`], this enables some -/// memory optimizations -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct OneIndexed(NonZeroUsize); - -impl OneIndexed { - /// The largest value that can be represented by this integer type - pub const MAX: Self = unwrap(Self::new(usize::MAX)); - // SAFETY: These constants are being initialized with non-zero values - /// The smallest value that can be represented by this integer type. - pub const MIN: Self = unwrap(Self::new(1)); - pub const ONE: NonZeroUsize = unwrap(NonZeroUsize::new(1)); - - /// Creates a non-zero if the given value is not zero. - pub const fn new(value: usize) -> Option { - match NonZeroUsize::new(value) { - Some(value) => Some(Self(value)), - None => None, - } - } - - /// Construct a new [`OneIndexed`] from a zero-indexed value - pub const fn from_zero_indexed(value: usize) -> Self { - Self(Self::ONE.saturating_add(value)) - } - - /// Returns the value as a primitive type. - pub const fn get(self) -> usize { - self.0.get() - } - - /// Return the zero-indexed primitive value for this [`OneIndexed`] - pub const fn to_zero_indexed(self) -> usize { - self.0.get() - 1 - } - - /// Saturating integer addition. Computes `self + rhs`, saturating at - /// the numeric bounds instead of overflowing. - #[must_use] - pub const fn saturating_add(self, rhs: usize) -> Self { - match NonZeroUsize::new(self.0.get().saturating_add(rhs)) { - Some(value) => Self(value), - None => Self::MAX, - } - } - - /// Saturating integer subtraction. Computes `self - rhs`, saturating - /// at the numeric bounds instead of overflowing. - #[must_use] - pub const fn saturating_sub(self, rhs: usize) -> Self { - match NonZeroUsize::new(self.0.get().saturating_sub(rhs)) { - Some(value) => Self(value), - None => Self::MIN, - } - } - - /// Checked addition. Returns `None` if overflow occurred. - #[must_use] - pub fn checked_add(self, rhs: Self) -> Option { - self.0.checked_add(rhs.0.get()).map(Self) - } - - /// Checked subtraction. Returns `None` if overflow occurred. - #[must_use] - pub fn checked_sub(self, rhs: Self) -> Option { - self.0.get().checked_sub(rhs.get()).and_then(Self::new) - } -} - -impl fmt::Display for OneIndexed { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - std::fmt::Debug::fmt(&self.0.get(), f) - } -} - -/// A const `Option::unwrap` without nightly features: -/// [Tracking issue](https://github.com/rust-lang/rust/issues/67441) -const fn unwrap(option: Option) -> T { - match option { - Some(value) => value, - None => panic!("unwrapping None"), - } -} - -impl FromStr for OneIndexed { - type Err = ParseIntError; - fn from_str(s: &str) -> Result { - Ok(OneIndexed(NonZeroUsize::from_str(s)?)) - } -} - #[cfg(test)] mod tests { use ruff_text_size::TextSize; diff --git a/crates/ruff_source_file/src/line_ranges.rs b/crates/ruff_source_file/src/line_ranges.rs deleted file mode 100644 index 77c19837..00000000 --- a/crates/ruff_source_file/src/line_ranges.rs +++ /dev/null @@ -1,396 +0,0 @@ -use crate::find_newline; -use memchr::{memchr2, memrchr2}; -use ruff_text_size::{TextLen, TextRange, TextSize}; -use std::ops::Add; - -/// Extension trait for [`str`] that provides methods for working with ranges of lines. -pub trait LineRanges { - /// Computes the start position of the line of `offset`. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::TextSize; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\rthird line"; - /// - /// assert_eq!(text.line_start(TextSize::from(0)), TextSize::from(0)); - /// assert_eq!(text.line_start(TextSize::from(4)), TextSize::from(0)); - /// - /// assert_eq!(text.line_start(TextSize::from(14)), TextSize::from(11)); - /// assert_eq!(text.line_start(TextSize::from(28)), TextSize::from(23)); - /// ``` - /// - /// ## Panics - /// If `offset` is out of bounds. - fn line_start(&self, offset: TextSize) -> TextSize; - - /// Computes the start position of the file contents: either the first byte, or the byte after - /// the BOM. - fn bom_start_offset(&self) -> TextSize; - - /// Returns `true` if `offset` is at the start of a line. - fn is_at_start_of_line(&self, offset: TextSize) -> bool { - self.line_start(offset) == offset - } - - /// Computes the offset that is right after the newline character that ends `offset`'s line. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!(text.full_line_end(TextSize::from(3)), TextSize::from(11)); - /// assert_eq!(text.full_line_end(TextSize::from(14)), TextSize::from(24)); - /// assert_eq!(text.full_line_end(TextSize::from(28)), TextSize::from(34)); - /// ``` - /// - /// ## Panics - /// - /// If `offset` is passed the end of the content. - fn full_line_end(&self, offset: TextSize) -> TextSize; - - /// Computes the offset that is right before the newline character that ends `offset`'s line. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!(text.line_end(TextSize::from(3)), TextSize::from(10)); - /// assert_eq!(text.line_end(TextSize::from(14)), TextSize::from(22)); - /// assert_eq!(text.line_end(TextSize::from(28)), TextSize::from(34)); - /// ``` - /// - /// ## Panics - /// - /// If `offset` is passed the end of the content. - fn line_end(&self, offset: TextSize) -> TextSize; - - /// Computes the range of this `offset`s line. - /// - /// The range starts at the beginning of the line and goes up to, and including, the new line character - /// at the end of the line. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!(text.full_line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(11))); - /// assert_eq!(text.full_line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(24))); - /// assert_eq!(text.full_line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34))); - /// ``` - /// - /// ## Panics - /// If `offset` is out of bounds. - fn full_line_range(&self, offset: TextSize) -> TextRange { - TextRange::new(self.line_start(offset), self.full_line_end(offset)) - } - - /// Computes the range of this `offset`s line ending before the newline character. - /// - /// The range starts at the beginning of the line and goes up to, but excluding, the new line character - /// at the end of the line. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!(text.line_range(TextSize::from(3)), TextRange::new(TextSize::from(0), TextSize::from(10))); - /// assert_eq!(text.line_range(TextSize::from(14)), TextRange::new(TextSize::from(11), TextSize::from(22))); - /// assert_eq!(text.line_range(TextSize::from(28)), TextRange::new(TextSize::from(24), TextSize::from(34))); - /// ``` - /// - /// ## Panics - /// If `offset` is out of bounds. - fn line_range(&self, offset: TextSize) -> TextRange { - TextRange::new(self.line_start(offset), self.line_end(offset)) - } - - /// Returns the text of the `offset`'s line. - /// - /// The line includes the newline characters at the end of the line. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!(text.full_line_str(TextSize::from(3)), "First line\n"); - /// assert_eq!(text.full_line_str(TextSize::from(14)), "second line\r\n"); - /// assert_eq!(text.full_line_str(TextSize::from(28)), "third line"); - /// ``` - /// - /// ## Panics - /// If `offset` is out of bounds. - fn full_line_str(&self, offset: TextSize) -> &str; - - /// Returns the text of the `offset`'s line. - /// - /// Excludes the newline characters at the end of the line. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!(text.line_str(TextSize::from(3)), "First line"); - /// assert_eq!(text.line_str(TextSize::from(14)), "second line"); - /// assert_eq!(text.line_str(TextSize::from(28)), "third line"); - /// ``` - /// - /// ## Panics - /// If `offset` is out of bounds. - fn line_str(&self, offset: TextSize) -> &str; - - /// Computes the range of all lines that this `range` covers. - /// - /// The range starts at the beginning of the line at `range.start()` and goes up to, and including, the new line character - /// at the end of `range.ends()`'s line. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!( - /// text.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))), - /// TextRange::new(TextSize::from(0), TextSize::from(11)) - /// ); - /// assert_eq!( - /// text.full_lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))), - /// TextRange::new(TextSize::from(0), TextSize::from(24)) - /// ); - /// ``` - /// - /// ## Panics - /// If the start or end of `range` is out of bounds. - fn full_lines_range(&self, range: TextRange) -> TextRange { - TextRange::new( - self.line_start(range.start()), - self.full_line_end(range.end()), - ) - } - - /// Computes the range of all lines that this `range` covers. - /// - /// The range starts at the beginning of the line at `range.start()` and goes up to, but excluding, the new line character - /// at the end of `range.end()`'s line. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!( - /// text.lines_range(TextRange::new(TextSize::from(3), TextSize::from(5))), - /// TextRange::new(TextSize::from(0), TextSize::from(10)) - /// ); - /// assert_eq!( - /// text.lines_range(TextRange::new(TextSize::from(3), TextSize::from(14))), - /// TextRange::new(TextSize::from(0), TextSize::from(22)) - /// ); - /// ``` - /// - /// ## Panics - /// If the start or end of `range` is out of bounds. - fn lines_range(&self, range: TextRange) -> TextRange { - TextRange::new(self.line_start(range.start()), self.line_end(range.end())) - } - - /// Returns true if the text of `range` contains any line break. - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert!( - /// !text.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(5))), - /// ); - /// assert!( - /// text.contains_line_break(TextRange::new(TextSize::from(3), TextSize::from(14))), - /// ); - /// ``` - /// - /// ## Panics - /// If the `range` is out of bounds. - fn contains_line_break(&self, range: TextRange) -> bool; - - /// Returns the text of all lines that include `range`. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!( - /// text.lines_str(TextRange::new(TextSize::from(3), TextSize::from(5))), - /// "First line" - /// ); - /// assert_eq!( - /// text.lines_str(TextRange::new(TextSize::from(3), TextSize::from(14))), - /// "First line\nsecond line" - /// ); - /// ``` - /// - /// ## Panics - /// If the start or end of `range` is out of bounds. - fn lines_str(&self, range: TextRange) -> &str; - - /// Returns the text of all lines that include `range`. - /// - /// Includes the newline characters of the last line. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange, TextSize}; - /// # use ruff_source_file::LineRanges; - /// - /// let text = "First line\nsecond line\r\nthird line"; - /// - /// assert_eq!( - /// text.full_lines_str(TextRange::new(TextSize::from(3), TextSize::from(5))), - /// "First line\n" - /// ); - /// assert_eq!( - /// text.full_lines_str(TextRange::new(TextSize::from(3), TextSize::from(14))), - /// "First line\nsecond line\r\n" - /// ); - /// ``` - /// - /// ## Panics - /// If the start or end of `range` is out of bounds. - fn full_lines_str(&self, range: TextRange) -> &str; - - /// The number of lines `range` spans. - /// - /// ## Examples - /// - /// ``` - /// # use ruff_text_size::{Ranged, TextRange}; - /// # use ruff_source_file::LineRanges; - /// - /// assert_eq!("a\nb".count_lines(TextRange::up_to(1.into())), 0); - /// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(3.into())), 1, "Up to the end of the second line"); - /// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(4.into())), 2, "In between the line break characters"); - /// assert_eq!("a\nb\r\nc".count_lines(TextRange::up_to(5.into())), 2); - /// assert_eq!("Single line".count_lines(TextRange::up_to(13.into())), 0); - /// assert_eq!("out\nof\nbounds end".count_lines(TextRange::up_to(55.into())), 2); - /// ``` - fn count_lines(&self, range: TextRange) -> u32 { - let mut count = 0; - let mut line_end = self.line_end(range.start()); - - loop { - let next_line_start = self.full_line_end(line_end); - - // Reached the end of the string - if next_line_start == line_end { - break count; - } - - // Range ends at the line boundary - if line_end >= range.end() { - break count; - } - - count += 1; - - line_end = self.line_end(next_line_start); - } - } -} - -impl LineRanges for str { - fn line_start(&self, offset: TextSize) -> TextSize { - let bytes = self[TextRange::up_to(offset)].as_bytes(); - if let Some(index) = memrchr2(b'\n', b'\r', bytes) { - // SAFETY: Safe because `index < offset` - TextSize::try_from(index).unwrap().add(TextSize::from(1)) - } else { - self.bom_start_offset() - } - } - - fn bom_start_offset(&self) -> TextSize { - if self.starts_with('\u{feff}') { - // Skip the BOM. - '\u{feff}'.text_len() - } else { - // Start of file. - TextSize::default() - } - } - - fn full_line_end(&self, offset: TextSize) -> TextSize { - let slice = &self[usize::from(offset)..]; - if let Some((index, line_ending)) = find_newline(slice) { - offset + TextSize::try_from(index).unwrap() + line_ending.text_len() - } else { - self.text_len() - } - } - - fn line_end(&self, offset: TextSize) -> TextSize { - let slice = &self[offset.to_usize()..]; - if let Some(index) = memchr2(b'\n', b'\r', slice.as_bytes()) { - offset + TextSize::try_from(index).unwrap() - } else { - self.text_len() - } - } - - fn full_line_str(&self, offset: TextSize) -> &str { - &self[self.full_line_range(offset)] - } - - fn line_str(&self, offset: TextSize) -> &str { - &self[self.line_range(offset)] - } - - fn contains_line_break(&self, range: TextRange) -> bool { - memchr2(b'\n', b'\r', self[range].as_bytes()).is_some() - } - - fn lines_str(&self, range: TextRange) -> &str { - &self[self.lines_range(range)] - } - - fn full_lines_str(&self, range: TextRange) -> &str { - &self[self.full_lines_range(range)] - } -} diff --git a/crates/ruff_source_file/src/newlines.rs b/crates/ruff_source_file/src/newlines.rs index deb6d846..f4ef84d2 100644 --- a/crates/ruff_source_file/src/newlines.rs +++ b/crates/ruff_source_file/src/newlines.rs @@ -1,55 +1,41 @@ -use std::iter::FusedIterator; -use std::ops::Deref; +use std::sync::LazyLock; -use memchr::{memchr2, memrchr2}; -use ruff_text_size::{TextLen, TextRange, TextSize}; +use memchr::memchr2; +use memchr::memmem; +use ruff_text_size::TextSize; -/// Extension trait for [`str`] that provides a [`UniversalNewlineIterator`]. -pub trait UniversalNewlines { - fn universal_newlines(&self) -> UniversalNewlineIterator<'_>; -} +static CRLF_FINDER: LazyLock = LazyLock::new(|| memmem::Finder::new(b"\r\n")); -impl UniversalNewlines for str { - fn universal_newlines(&self) -> UniversalNewlineIterator<'_> { - UniversalNewlineIterator::from(self) - } +/// Line ending styles +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum LineEnding { + Lf, + Cr, + Crlf, } -/// Like [`str::lines`], but accommodates LF, CRLF, and CR line endings, -/// the latter of which are not supported by [`str::lines`]. -/// -/// ## Examples -/// -/// ```rust -/// # use ruff_text_size::TextSize; -/// # use ruff_source_file::{Line, UniversalNewlineIterator}; -/// let mut lines = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop"); -/// -/// assert_eq!(lines.next_back(), Some(Line::new("bop", TextSize::from(14)))); -/// assert_eq!(lines.next(), Some(Line::new("foo\n", TextSize::from(0)))); -/// assert_eq!(lines.next_back(), Some(Line::new("baz\r", TextSize::from(10)))); -/// assert_eq!(lines.next(), Some(Line::new("bar\n", TextSize::from(4)))); -/// assert_eq!(lines.next_back(), Some(Line::new("\r\n", TextSize::from(8)))); -/// assert_eq!(lines.next(), None); -/// ``` -#[derive(Clone)] -pub struct UniversalNewlineIterator<'a> { - text: &'a str, - offset: TextSize, - offset_back: TextSize, -} +impl LineEnding { + pub const fn as_str(&self) -> &'static str { + match self { + LineEnding::Lf => "\n", + LineEnding::Crlf => "\r\n", + LineEnding::Cr => "\r", + } + } -impl<'a> UniversalNewlineIterator<'a> { - pub fn with_offset(text: &'a str, offset: TextSize) -> UniversalNewlineIterator<'a> { - UniversalNewlineIterator { - text, - offset, - offset_back: offset + text.text_len(), + #[allow(clippy::len_without_is_empty)] + pub const fn len(&self) -> usize { + match self { + LineEnding::Lf | LineEnding::Cr => 1, + LineEnding::Crlf => 2, } } - pub fn from(text: &'a str) -> UniversalNewlineIterator<'a> { - Self::with_offset(text, TextSize::default()) + pub const fn text_len(&self) -> TextSize { + match self { + LineEnding::Lf | LineEnding::Cr => TextSize::new(1), + LineEnding::Crlf => TextSize::new(2), + } } } @@ -62,7 +48,7 @@ pub fn find_newline(text: &str) -> Option<(usize, LineEnding)> { // Explicit branch for `\n` as this is the most likely path b'\n' => LineEnding::Lf, // '\r\n' - b'\r' if bytes.get(position.saturating_add(1)) == Some(&b'\n') => LineEnding::CrLf, + b'\r' if bytes.get(position.saturating_add(1)) == Some(&b'\n') => LineEnding::Crlf, // '\r' _ => LineEnding::Cr, }; @@ -73,385 +59,85 @@ pub fn find_newline(text: &str) -> Option<(usize, LineEnding)> { } } -impl<'a> Iterator for UniversalNewlineIterator<'a> { - type Item = Line<'a>; - - #[inline] - fn next(&mut self) -> Option> { - if self.text.is_empty() { - return None; - } - - let line = if let Some((newline_position, line_ending)) = find_newline(self.text) { - let (text, remainder) = self.text.split_at(newline_position + line_ending.len()); - - let line = Line { - offset: self.offset, - text, - }; - - self.text = remainder; - self.offset += text.text_len(); - - line - } - // Last line - else { - Line { - offset: self.offset, - text: std::mem::take(&mut self.text), - } - }; - - Some(line) - } - - fn last(mut self) -> Option { - self.next_back() - } -} - -impl DoubleEndedIterator for UniversalNewlineIterator<'_> { - #[inline] - fn next_back(&mut self) -> Option { - if self.text.is_empty() { - return None; - } - - let len = self.text.len(); - - // Trim any trailing newlines. - let haystack = match self.text.as_bytes()[len - 1] { - b'\n' if len > 1 && self.text.as_bytes()[len - 2] == b'\r' => &self.text[..len - 2], - b'\n' | b'\r' => &self.text[..len - 1], - _ => self.text, - }; - - // Find the end of the previous line. The previous line is the text up to, but not including - // the newline character. - let line = if let Some(line_end) = memrchr2(b'\n', b'\r', haystack.as_bytes()) { - // '\n' or '\r' or '\r\n' - let (remainder, line) = self.text.split_at(line_end + 1); - self.text = remainder; - self.offset_back -= line.text_len(); - - Line { - text: line, - offset: self.offset_back, - } - } else { - // Last line - let offset = self.offset_back - self.text.text_len(); - Line { - text: std::mem::take(&mut self.text), - offset, +/// Normalize line endings within a string +/// +/// We replace `\r\n` with `\n` in-place, which doesn't break utf-8 encoding. +/// While we *can* call `as_mut_vec` and do surgery on the live string +/// directly, let's rather steal the contents of `x`. This makes the code +/// safe even if a panic occurs. +/// +/// # Source +/// +/// --- +/// authors = ["rust-analyzer team"] +/// license = "MIT OR Apache-2.0" +/// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/line_index.rs" +/// --- +pub fn normalize_crlf_newlines(text: String) -> String { + let mut buf = text.into_bytes(); + let mut gap_len = 0; + let mut tail = buf.as_mut_slice(); + let mut crlf_seen = false; + + loop { + let idx = match CRLF_FINDER.find(&tail[gap_len..]) { + None if crlf_seen => tail.len(), + // SAFETY: buf is unchanged and therefore still contains utf8 data + None => return unsafe { String::from_utf8_unchecked(buf) }, + Some(idx) => { + crlf_seen = true; + idx + gap_len } }; - - Some(line) - } -} - -impl FusedIterator for UniversalNewlineIterator<'_> {} - -/// Like [`UniversalNewlineIterator`], but includes a trailing newline as an empty line. -pub struct NewlineWithTrailingNewline<'a> { - trailing: Option>, - underlying: UniversalNewlineIterator<'a>, -} - -impl<'a> NewlineWithTrailingNewline<'a> { - pub fn from(input: &'a str) -> NewlineWithTrailingNewline<'a> { - Self::with_offset(input, TextSize::default()) - } - - pub fn with_offset(input: &'a str, offset: TextSize) -> Self { - NewlineWithTrailingNewline { - underlying: UniversalNewlineIterator::with_offset(input, offset), - trailing: if input.ends_with(['\r', '\n']) { - Some(Line { - text: "", - offset: offset + input.text_len(), - }) - } else { - None - }, - } - } -} - -impl<'a> Iterator for NewlineWithTrailingNewline<'a> { - type Item = Line<'a>; - - #[inline] - fn next(&mut self) -> Option { - self.underlying.next().or_else(|| self.trailing.take()) - } -} - -impl DoubleEndedIterator for NewlineWithTrailingNewline<'_> { - #[inline] - fn next_back(&mut self) -> Option { - self.trailing.take().or_else(|| self.underlying.next_back()) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Line<'a> { - text: &'a str, - offset: TextSize, -} - -impl<'a> Line<'a> { - pub fn new(text: &'a str, offset: TextSize) -> Self { - Self { text, offset } - } - - #[inline] - pub const fn start(&self) -> TextSize { - self.offset - } - - /// Returns the byte offset where the line ends, including its terminating new line character. - #[inline] - pub fn full_end(&self) -> TextSize { - self.offset + self.full_text_len() - } - - /// Returns the byte offset where the line ends, excluding its new line character - #[inline] - pub fn end(&self) -> TextSize { - self.offset + self.as_str().text_len() - } - - /// Returns the range of the line, including its terminating new line character. - #[inline] - pub fn full_range(&self) -> TextRange { - TextRange::at(self.offset, self.text.text_len()) - } - - /// Returns the range of the line, excluding its terminating new line character - #[inline] - pub fn range(&self) -> TextRange { - TextRange::new(self.start(), self.end()) - } - - /// Returns the line's new line character, if any. - #[inline] - pub fn line_ending(&self) -> Option { - let mut bytes = self.text.bytes().rev(); - match bytes.next() { - Some(b'\n') => { - if bytes.next() == Some(b'\r') { - Some(LineEnding::CrLf) - } else { - Some(LineEnding::Lf) - } - } - Some(b'\r') => Some(LineEnding::Cr), - _ => None, - } - } - - /// Returns the text of the line, excluding the terminating new line character. - #[inline] - pub fn as_str(&self) -> &'a str { - let newline_len = self - .line_ending() - .map_or(0, |line_ending| line_ending.len()); - &self.text[..self.text.len() - newline_len] - } - - /// Returns the line's text, including the terminating new line character. - #[inline] - pub fn as_full_str(&self) -> &'a str { - self.text - } - - #[inline] - pub fn full_text_len(&self) -> TextSize { - self.text.text_len() - } -} - -impl Deref for Line<'_> { - type Target = str; - - fn deref(&self) -> &Self::Target { - self.as_str() - } -} - -impl PartialEq<&str> for Line<'_> { - fn eq(&self, other: &&str) -> bool { - self.as_str() == *other - } -} - -impl PartialEq> for &str { - fn eq(&self, other: &Line<'_>) -> bool { - *self == other.as_str() - } -} - -/// The line ending style used in Python source code. -/// See -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum LineEnding { - Lf, - Cr, - CrLf, -} - -impl Default for LineEnding { - fn default() -> Self { - if cfg!(windows) { - LineEnding::CrLf - } else { - LineEnding::Lf - } - } -} - -impl LineEnding { - pub const fn as_str(&self) -> &'static str { - match self { - LineEnding::Lf => "\n", - LineEnding::CrLf => "\r\n", - LineEnding::Cr => "\r", - } - } - - #[allow(clippy::len_without_is_empty)] - pub const fn len(&self) -> usize { - match self { - LineEnding::Lf | LineEnding::Cr => 1, - LineEnding::CrLf => 2, + tail.copy_within(gap_len..idx, 0); + tail = &mut tail[idx - gap_len..]; + if tail.len() == gap_len { + break; } + gap_len += 1; } - pub const fn text_len(&self) -> TextSize { - match self { - LineEnding::Lf | LineEnding::Cr => TextSize::new(1), - LineEnding::CrLf => TextSize::new(2), - } - } -} - -impl Deref for LineEnding { - type Target = str; - - fn deref(&self) -> &Self::Target { - self.as_str() + // Account for removed `\r`. + // After `set_len`, `buf` is guaranteed to contain utf-8 again. + unsafe { + let new_len = buf.len() - gap_len; + buf.set_len(new_len); + String::from_utf8_unchecked(buf) } } #[cfg(test)] mod tests { - use ruff_text_size::TextSize; - - use super::{Line, UniversalNewlineIterator}; + use super::*; #[test] - fn universal_newlines_empty_str() { - let lines: Vec<_> = UniversalNewlineIterator::from("").collect(); - assert_eq!(lines, Vec::::new()); - - let lines: Vec<_> = UniversalNewlineIterator::from("").rev().collect(); - assert_eq!(lines, Vec::::new()); + fn unix() { + let src = "a\nb\nc\n\n\n\n"; + assert_eq!(find_newline(src), Some((1, LineEnding::Lf))); + assert_eq!(normalize_crlf_newlines(src.to_string()), src); } #[test] - fn universal_newlines_forward() { - let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop").collect(); + fn dos() { + let src = "\r\na\r\n\r\nb\r\nc\r\n\r\n\r\n\r\n"; + assert_eq!(find_newline(src), Some((0, LineEnding::Crlf))); assert_eq!( - lines, - vec![ - Line::new("foo\n", TextSize::from(0)), - Line::new("bar\n", TextSize::from(4)), - Line::new("\r\n", TextSize::from(8)), - Line::new("baz\r", TextSize::from(10)), - Line::new("bop", TextSize::from(14)), - ] - ); - - let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop\n").collect(); - assert_eq!( - lines, - vec![ - Line::new("foo\n", TextSize::from(0)), - Line::new("bar\n", TextSize::from(4)), - Line::new("\r\n", TextSize::from(8)), - Line::new("baz\r", TextSize::from(10)), - Line::new("bop\n", TextSize::from(14)), - ] - ); - - let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop\n\n").collect(); - assert_eq!( - lines, - vec![ - Line::new("foo\n", TextSize::from(0)), - Line::new("bar\n", TextSize::from(4)), - Line::new("\r\n", TextSize::from(8)), - Line::new("baz\r", TextSize::from(10)), - Line::new("bop\n", TextSize::from(14)), - Line::new("\n", TextSize::from(18)), - ] + normalize_crlf_newlines(src.to_string()), + "\na\n\nb\nc\n\n\n\n" ); } #[test] - fn universal_newlines_backwards() { - let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop") - .rev() - .collect(); - assert_eq!( - lines, - vec![ - Line::new("bop", TextSize::from(14)), - Line::new("baz\r", TextSize::from(10)), - Line::new("\r\n", TextSize::from(8)), - Line::new("bar\n", TextSize::from(4)), - Line::new("foo\n", TextSize::from(0)), - ] - ); - - let lines: Vec<_> = UniversalNewlineIterator::from("foo\nbar\n\nbaz\rbop\n") - .rev() - .map(|line| line.as_str()) - .collect(); - - assert_eq!( - lines, - vec![ - Line::new("bop\n", TextSize::from(13)), - Line::new("baz\r", TextSize::from(9)), - Line::new("\n", TextSize::from(8)), - Line::new("bar\n", TextSize::from(4)), - Line::new("foo\n", TextSize::from(0)), - ] - ); + fn mixed() { + let src = "a\r\nb\r\nc\r\n\n\r\n\n"; + assert_eq!(find_newline(src), Some((1, LineEnding::Crlf))); + assert_eq!(normalize_crlf_newlines(src.to_string()), "a\nb\nc\n\n\n\n"); } #[test] - fn universal_newlines_mixed() { - let mut lines = UniversalNewlineIterator::from("foo\nbar\n\r\nbaz\rbop"); - - assert_eq!( - lines.next_back(), - Some(Line::new("bop", TextSize::from(14))) - ); - assert_eq!(lines.next(), Some(Line::new("foo\n", TextSize::from(0)))); - assert_eq!( - lines.next_back(), - Some(Line::new("baz\r", TextSize::from(10))) - ); - assert_eq!(lines.next(), Some(Line::new("bar\n", TextSize::from(4)))); - assert_eq!( - lines.next_back(), - Some(Line::new("\r\n", TextSize::from(8))) - ); - assert_eq!(lines.next(), None); + fn none() { + let src = "abc"; + assert_eq!(find_newline(src), None); + assert_eq!(normalize_crlf_newlines(src.to_string()), src); } } diff --git a/crates/ruff_source_file/src/one_indexed.rs b/crates/ruff_source_file/src/one_indexed.rs new file mode 100644 index 00000000..d56de9b4 --- /dev/null +++ b/crates/ruff_source_file/src/one_indexed.rs @@ -0,0 +1,100 @@ +use std::fmt; +use std::fmt::Formatter; +use std::num::NonZeroUsize; +use std::num::ParseIntError; +use std::str::FromStr; + +/// Type-safe wrapper for a value whose logical range starts at `1`, for +/// instance the line or column numbers in a file +/// +/// Internally this is represented as a [`NonZeroUsize`], this enables some +/// memory optimizations +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct OneIndexed(NonZeroUsize); + +impl OneIndexed { + /// The largest value that can be represented by this integer type + pub const MAX: Self = unwrap(Self::new(usize::MAX)); + // SAFETY: These constants are being initialized with non-zero values + /// The smallest value that can be represented by this integer type. + pub const MIN: Self = unwrap(Self::new(1)); + pub const ONE: NonZeroUsize = unwrap(NonZeroUsize::new(1)); + + /// Creates a non-zero if the given value is not zero. + pub const fn new(value: usize) -> Option { + match NonZeroUsize::new(value) { + Some(value) => Some(Self(value)), + None => None, + } + } + + /// Construct a new [`OneIndexed`] from a zero-indexed value + pub const fn from_zero_indexed(value: usize) -> Self { + Self(Self::ONE.saturating_add(value)) + } + + /// Returns the value as a primitive type. + pub const fn get(self) -> usize { + self.0.get() + } + + /// Return the zero-indexed primitive value for this [`OneIndexed`] + pub const fn to_zero_indexed(self) -> usize { + self.0.get() - 1 + } + + /// Saturating integer addition. Computes `self + rhs`, saturating at + /// the numeric bounds instead of overflowing. + #[must_use] + pub const fn saturating_add(self, rhs: usize) -> Self { + match NonZeroUsize::new(self.0.get().saturating_add(rhs)) { + Some(value) => Self(value), + None => Self::MAX, + } + } + + /// Saturating integer subtraction. Computes `self - rhs`, saturating + /// at the numeric bounds instead of overflowing. + #[must_use] + pub const fn saturating_sub(self, rhs: usize) -> Self { + match NonZeroUsize::new(self.0.get().saturating_sub(rhs)) { + Some(value) => Self(value), + None => Self::MIN, + } + } + + /// Checked addition. Returns `None` if overflow occurred. + #[must_use] + pub fn checked_add(self, rhs: Self) -> Option { + self.0.checked_add(rhs.0.get()).map(Self) + } + + /// Checked subtraction. Returns `None` if overflow occurred. + #[must_use] + pub fn checked_sub(self, rhs: Self) -> Option { + self.0.get().checked_sub(rhs.get()).and_then(Self::new) + } +} + +impl fmt::Display for OneIndexed { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + std::fmt::Debug::fmt(&self.0.get(), f) + } +} + +/// A const `Option::unwrap` without nightly features: +/// [Tracking issue](https://github.com/rust-lang/rust/issues/67441) +const fn unwrap(option: Option) -> T { + match option { + Some(value) => value, + None => panic!("unwrapping None"), + } +} + +impl FromStr for OneIndexed { + type Err = ParseIntError; + fn from_str(s: &str) -> Result { + Ok(OneIndexed(NonZeroUsize::from_str(s)?)) + } +} diff --git a/crates/ruff_source_file/src/source_location.rs b/crates/ruff_source_file/src/source_location.rs new file mode 100644 index 00000000..fdc8fab8 --- /dev/null +++ b/crates/ruff_source_file/src/source_location.rs @@ -0,0 +1,34 @@ +use std::fmt::{Debug, Formatter}; + +use crate::OneIndexed; + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SourceLocation { + pub row: OneIndexed, + pub column: OneIndexed, +} + +impl Default for SourceLocation { + fn default() -> Self { + Self { + row: OneIndexed::MIN, + column: OneIndexed::MIN, + } + } +} + +impl Debug for SourceLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SourceLocation") + .field("row", &self.row.get()) + .field("column", &self.column.get()) + .finish() + } +} + +impl std::fmt::Display for SourceLocation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{row}:{column}", row = self.row, column = self.column) + } +} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 888fe4d6..54a06b45 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -16,9 +16,9 @@ air_r_formatter = { workspace = true } biome_formatter = { workspace = true, features = ["serde"] } fs = { workspace = true } ignore = { workspace = true } -line_ending = { workspace = true } rustc-hash = { workspace = true } thiserror = { workspace = true } +ruff_source_file = { workspace = true } serde = { workspace = true, features = ["derive"] } toml = { workspace = true } diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs index dec96df6..63e5d70f 100644 --- a/crates/workspace/src/settings.rs +++ b/crates/workspace/src/settings.rs @@ -1,19 +1,16 @@ mod indent_style; mod indent_width; -// TODO: Can we pick a better crate name for `line_ending` so these don't collide? -#[path = "settings/line_ending.rs"] -mod line_ending_setting; +mod line_ending; mod line_length; mod magic_line_break; pub use indent_style::*; pub use indent_width::*; -pub use line_ending_setting::*; +pub use line_ending::*; pub use line_length::*; pub use magic_line_break::*; use air_r_formatter::context::RFormatOptions; -use line_ending; /// Resolved configuration settings used within air /// @@ -44,9 +41,11 @@ impl FormatSettings { LineEnding::Native => biome_formatter::LineEnding::Crlf, #[cfg(not(target_os = "windows"))] LineEnding::Native => biome_formatter::LineEnding::Lf, - LineEnding::Auto => match line_ending::infer(source) { - line_ending::LineEnding::Lf => biome_formatter::LineEnding::Lf, - line_ending::LineEnding::Crlf => biome_formatter::LineEnding::Crlf, + LineEnding::Auto => match ruff_source_file::find_newline(source) { + Some((_, ruff_source_file::LineEnding::Lf)) => biome_formatter::LineEnding::Lf, + Some((_, ruff_source_file::LineEnding::Crlf)) => biome_formatter::LineEnding::Crlf, + Some((_, ruff_source_file::LineEnding::Cr)) => biome_formatter::LineEnding::Cr, + None => biome_formatter::LineEnding::Lf, }, }; From 95487aa8aaeb60869c49d6ee34ed53d10481b83a Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 26 Dec 2024 12:20:56 -0500 Subject: [PATCH 04/44] Drop ruff_text_size in favor of biome's --- Cargo.lock | 29 +- Cargo.toml | 1 - crates/ruff_server/Cargo.toml | 2 +- crates/ruff_server/src/edit/range.rs | 22 +- crates/ruff_server/src/edit/replacement.rs | 12 +- crates/ruff_source_file/Cargo.toml | 4 +- crates/ruff_source_file/src/line_index.rs | 18 +- crates/ruff_source_file/src/newlines.rs | 8 +- crates/ruff_text_size/Cargo.toml | 25 - crates/ruff_text_size/src/lib.rs | 36 -- crates/ruff_text_size/src/range.rs | 544 -------------------- crates/ruff_text_size/src/schemars_impls.rs | 33 -- crates/ruff_text_size/src/serde_impls.rs | 47 -- crates/ruff_text_size/src/size.rs | 196 ------- crates/ruff_text_size/src/traits.rs | 99 ---- crates/ruff_text_size/tests/auto_traits.rs | 18 - crates/ruff_text_size/tests/constructors.rs | 24 - crates/ruff_text_size/tests/indexing.rs | 8 - crates/ruff_text_size/tests/main.rs | 79 --- crates/ruff_text_size/tests/serde.rs | 83 --- 20 files changed, 35 insertions(+), 1253 deletions(-) delete mode 100644 crates/ruff_text_size/Cargo.toml delete mode 100644 crates/ruff_text_size/src/lib.rs delete mode 100644 crates/ruff_text_size/src/range.rs delete mode 100644 crates/ruff_text_size/src/schemars_impls.rs delete mode 100644 crates/ruff_text_size/src/serde_impls.rs delete mode 100644 crates/ruff_text_size/src/size.rs delete mode 100644 crates/ruff_text_size/src/traits.rs delete mode 100644 crates/ruff_text_size/tests/auto_traits.rs delete mode 100644 crates/ruff_text_size/tests/constructors.rs delete mode 100644 crates/ruff_text_size/tests/indexing.rs delete mode 100644 crates/ruff_text_size/tests/main.rs delete mode 100644 crates/ruff_text_size/tests/serde.rs diff --git a/Cargo.lock b/Cargo.lock index 8ca7f2f7..8eae5823 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1705,6 +1705,7 @@ dependencies = [ "air_r_formatter", "air_r_parser", "anyhow", + "biome_text_size", "cargo_metadata", "crossbeam", "ignore", @@ -1715,7 +1716,6 @@ dependencies = [ "lsp-types 0.95.1", "regex", "ruff_source_file", - "ruff_text_size", "rustc-hash", "serde", "serde_json", @@ -1729,19 +1729,9 @@ dependencies = [ name = "ruff_source_file" version = "0.0.0" dependencies = [ + "biome_text_size", "memchr", - "ruff_text_size", - "serde", -] - -[[package]] -name = "ruff_text_size" -version = "0.0.0" -dependencies = [ - "schemars", "serde", - "serde_test", - "static_assertions", ] [[package]] @@ -1938,15 +1928,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_test" -version = "1.0.177" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" -dependencies = [ - "serde", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -2034,12 +2015,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index e1e82142..5f87a4eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,6 @@ lsp_test = { path = "./crates/lsp_test" } tests_macros = { path = "./crates/tests_macros" } ruff_server = { path = "./crates/ruff_server" } ruff_source_file = { path = "./crates/ruff_source_file" } -ruff_text_size = { path = "./crates/ruff_text_size" } workspace = { path = "./crates/workspace" } anyhow = "1.0.89" diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 46c1a73a..7b0654d9 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -14,8 +14,8 @@ license = { workspace = true } [dependencies] air_r_formatter = { workspace = true } air_r_parser = { workspace = true } +biome_text_size = { workspace = true } ruff_source_file = { workspace = true } -ruff_text_size = { workspace = true } workspace = { workspace = true } anyhow = { workspace = true } diff --git a/crates/ruff_server/src/edit/range.rs b/crates/ruff_server/src/edit/range.rs index 29fb12fd..89649fd1 100644 --- a/crates/ruff_server/src/edit/range.rs +++ b/crates/ruff_server/src/edit/range.rs @@ -5,10 +5,10 @@ // +------------------------------------------------------------+ use super::PositionEncoding; +use biome_text_size::{TextRange, TextSize}; use lsp_types as types; use ruff_source_file::OneIndexed; use ruff_source_file::{LineIndex, SourceLocation}; -use ruff_text_size::{TextRange, TextSize}; pub(crate) trait RangeExt { fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) @@ -41,16 +41,16 @@ impl RangeExt for lsp_types::Range { let (start_column_offset, end_column_offset) = match encoding { PositionEncoding::UTF8 => ( - TextSize::new(self.start.character), - TextSize::new(self.end.character), + TextSize::from(self.start.character), + TextSize::from(self.end.character), ), PositionEncoding::UTF16 => { // Fast path for ASCII only documents if index.is_ascii() { ( - TextSize::new(self.start.character), - TextSize::new(self.end.character), + TextSize::from(self.start.character), + TextSize::from(self.end.character), ) } else { // UTF16 encodes characters either as one or two 16 bit words. @@ -80,8 +80,8 @@ impl RangeExt for lsp_types::Range { }; TextRange::new( - start_line.start() + start_column_offset.clamp(TextSize::new(0), start_line.end()), - end_line.start() + end_column_offset.clamp(TextSize::new(0), end_line.end()), + start_line.start() + start_column_offset.clamp(TextSize::from(0), start_line.end()), + end_line.start() + end_column_offset.clamp(TextSize::from(0), end_line.end()), ) } } @@ -107,7 +107,7 @@ impl ToRangeExt for TextRange { /// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { - let mut utf8_code_unit_offset = TextSize::new(0); + let mut utf8_code_unit_offset = TextSize::from(0); let mut i = 0u32; @@ -119,7 +119,7 @@ fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { // Count characters encoded as two 16 bit words as 2 characters. { utf8_code_unit_offset += - TextSize::new(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); + TextSize::from(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); i += u32::try_from(c.len_utf16()).expect("utf16 len always <=2"); } } @@ -139,7 +139,7 @@ fn offset_to_source_location( let column = offset - index.line_start(row, text); SourceLocation { - column: OneIndexed::from_zero_indexed(column.to_usize()), + column: OneIndexed::from_zero_indexed(column.into()), row, } } @@ -147,7 +147,7 @@ fn offset_to_source_location( let row = index.line_index(offset); let column = if index.is_ascii() { - (offset - index.line_start(row, text)).to_usize() + (offset - index.line_start(row, text)).into() } else { let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)]; up_to_line.encode_utf16().count() diff --git a/crates/ruff_server/src/edit/replacement.rs b/crates/ruff_server/src/edit/replacement.rs index 701d120d..3729e2fc 100644 --- a/crates/ruff_server/src/edit/replacement.rs +++ b/crates/ruff_server/src/edit/replacement.rs @@ -4,7 +4,7 @@ // | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | // +------------------------------------------------------------+ -use ruff_text_size::{TextLen, TextRange, TextSize}; +use biome_text_size::{TextLen, TextRange, TextSize}; #[derive(Debug)] pub(crate) struct Replacement { @@ -68,8 +68,10 @@ impl Replacement { #[cfg(test)] mod tests { + use std::ops::Range; + + use biome_text_size::TextRange; use ruff_source_file::LineIndex; - use ruff_text_size::TextRange; use super::Replacement; @@ -82,11 +84,9 @@ mod tests { modified, modified_index.line_starts(), ); + let range: Range = replacement.source_range.into(); let mut expected = source.to_string(); - expected.replace_range( - replacement.source_range.start().to_usize()..replacement.source_range.end().to_usize(), - &modified[replacement.modified_range], - ); + expected.replace_range(range, &modified[replacement.modified_range]); (replacement, expected) } diff --git a/crates/ruff_source_file/Cargo.toml b/crates/ruff_source_file/Cargo.toml index 521a37c7..95b19909 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/ruff_source_file/Cargo.toml @@ -12,7 +12,7 @@ license = { workspace = true } [lib] [dependencies] -ruff_text_size = { workspace = true } +biome_text_size = { workspace = true } memchr = { workspace = true } serde = { workspace = true, optional = true } @@ -20,7 +20,7 @@ serde = { workspace = true, optional = true } [dev-dependencies] [features] -serde = ["dep:serde", "ruff_text_size/serde"] +serde = ["dep:serde", "biome_text_size/serde"] [lints] workspace = true diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs index ae60e06e..5fd219fd 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/ruff_source_file/src/line_index.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Formatter}; use std::ops::Deref; use std::sync::Arc; -use ruff_text_size::{TextLen, TextRange, TextSize}; +use biome_text_size::{TextLen, TextRange, TextSize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -70,7 +70,7 @@ impl LineIndex { /// ## Examples /// /// ``` - /// # use ruff_text_size::TextSize; + /// # use biome_text_size::TextSize; /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; /// let source = "def a():\n pass"; /// let index = LineIndex::from_source_text(source); @@ -139,7 +139,7 @@ impl LineIndex { /// ## Examples /// /// ``` - /// # use ruff_text_size::TextSize; + /// # use biome_text_size::TextSize; /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; /// let source = "def a():\n pass"; /// let index = LineIndex::from_source_text(source); @@ -200,7 +200,7 @@ impl LineIndex { if row_index.saturating_add(1) >= starts.len() { contents.text_len() } else { - starts[row_index + 1] - TextSize::new(1) + starts[row_index + 1] - TextSize::from(1) } } @@ -228,7 +228,7 @@ impl LineIndex { /// /// ``` /// use ruff_source_file::{LineIndex, OneIndexed}; - /// use ruff_text_size::TextSize; + /// use biome_text_size::TextSize; /// let source = r#"a = 4 /// c = "some string" /// x = b"#; @@ -252,7 +252,7 @@ impl LineIndex { /// /// ``` /// use ruff_source_file::{LineIndex, OneIndexed}; - /// use ruff_text_size::TextSize; + /// use biome_text_size::TextSize; /// let source = r#"a = 4 /// c = "❤️" /// x = b"#; @@ -285,14 +285,14 @@ impl LineIndex { line_range.start() + TextSize::try_from(column.to_zero_indexed()) .unwrap_or(line_range.len()) - .clamp(TextSize::new(0), line_range.len()) + .clamp(TextSize::from(0), line_range.len()) } IndexKind::Utf8 => { let rest = &contents[line_range]; let column_offset: TextSize = rest .chars() .take(column.to_zero_indexed()) - .map(ruff_text_size::TextLen::text_len) + .map(biome_text_size::TextLen::text_len) .sum(); line_range.start() + column_offset } @@ -336,7 +336,7 @@ impl IndexKind { #[cfg(test)] mod tests { - use ruff_text_size::TextSize; + use biome_text_size::TextSize; use crate::line_index::LineIndex; use crate::{OneIndexed, SourceLocation}; diff --git a/crates/ruff_source_file/src/newlines.rs b/crates/ruff_source_file/src/newlines.rs index f4ef84d2..05295af0 100644 --- a/crates/ruff_source_file/src/newlines.rs +++ b/crates/ruff_source_file/src/newlines.rs @@ -1,8 +1,8 @@ use std::sync::LazyLock; +use biome_text_size::TextSize; use memchr::memchr2; use memchr::memmem; -use ruff_text_size::TextSize; static CRLF_FINDER: LazyLock = LazyLock::new(|| memmem::Finder::new(b"\r\n")); @@ -31,10 +31,10 @@ impl LineEnding { } } - pub const fn text_len(&self) -> TextSize { + pub fn text_len(&self) -> TextSize { match self { - LineEnding::Lf | LineEnding::Cr => TextSize::new(1), - LineEnding::Crlf => TextSize::new(2), + LineEnding::Lf | LineEnding::Cr => TextSize::from(1), + LineEnding::Crlf => TextSize::from(2), } } } diff --git a/crates/ruff_text_size/Cargo.toml b/crates/ruff_text_size/Cargo.toml deleted file mode 100644 index d5195450..00000000 --- a/crates/ruff_text_size/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "ruff_text_size" -version = "0.0.0" -publish = false -edition = "2021" -rust-version = "1.67.1" - -[dependencies] -serde = { workspace = true, optional = true } -schemars = { workspace = true, optional = true } - -[dev-dependencies] -serde_test = { workspace = true } -static_assertions = { workspace = true } - -[features] -serde = ["dep:serde"] - -[lints] -workspace = true - -[[test]] -name = "serde" -path = "tests/serde.rs" -required-features = ["serde"] diff --git a/crates/ruff_text_size/src/lib.rs b/crates/ruff_text_size/src/lib.rs deleted file mode 100644 index 0276e0b3..00000000 --- a/crates/ruff_text_size/src/lib.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Newtypes for working with text sizes/ranges in a more type-safe manner. -//! -//! This library can help with two things: -//! * Reducing storage requirements for offsets and ranges, under the -//! assumption that 32 bits is enough. -//! * Providing standard vocabulary types for applications where text ranges -//! are pervasive. -//! -//! However, you should not use this library simply because you work with -//! strings. In the overwhelming majority of cases, using `usize` and -//! `std::ops::Range` is better. In particular, if you are publishing a -//! library, using only std types in the interface would make it more -//! interoperable. Similarly, if you are writing something like a lexer, which -//! produces, but does not *store* text ranges, then sticking to `usize` would -//! be better. -//! -//! Minimal Supported Rust Version: latest stable. - -#![forbid(unsafe_code)] -#![warn(missing_debug_implementations, missing_docs)] - -mod range; -mod size; -mod traits; - -#[cfg(feature = "schemars")] -mod schemars_impls; -#[cfg(feature = "serde")] -mod serde_impls; - -pub use crate::{ - range::TextRange, size::TextSize, traits::Ranged, traits::TextLen, traits::TextSlice, -}; - -#[cfg(target_pointer_width = "16")] -compile_error!("text-size assumes usize >= u32 and does not work on 16-bit targets"); diff --git a/crates/ruff_text_size/src/range.rs b/crates/ruff_text_size/src/range.rs deleted file mode 100644 index aa517345..00000000 --- a/crates/ruff_text_size/src/range.rs +++ /dev/null @@ -1,544 +0,0 @@ -use cmp::Ordering; - -use { - crate::TextSize, - std::{ - cmp, fmt, - ops::{Add, AddAssign, Bound, Index, IndexMut, Range, RangeBounds, Sub, SubAssign}, - }, -}; - -/// A range in text, represented as a pair of [`TextSize`][struct@TextSize]. -/// -/// It is a logic error for `start` to be greater than `end`. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct TextRange { - // Invariant: start <= end - start: TextSize, - end: TextSize, -} - -impl fmt::Debug for TextRange { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}..{}", self.start().raw, self.end().raw) - } -} - -impl TextRange { - /// Creates a new `TextRange` with the given `start` and `end` (`start..end`). - /// - /// # Panics - /// - /// Panics if `end < start`. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let start = TextSize::from(5); - /// let end = TextSize::from(10); - /// let range = TextRange::new(start, end); - /// - /// assert_eq!(range.start(), start); - /// assert_eq!(range.end(), end); - /// assert_eq!(range.len(), end - start); - /// ``` - #[inline] - pub const fn new(start: TextSize, end: TextSize) -> TextRange { - assert!(start.raw <= end.raw); - TextRange { start, end } - } - - /// Create a new `TextRange` with the given `offset` and `len` (`offset..offset + len`). - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let text = "0123456789"; - /// - /// let offset = TextSize::from(2); - /// let length = TextSize::from(5); - /// let range = TextRange::at(offset, length); - /// - /// assert_eq!(range, TextRange::new(offset, offset + length)); - /// assert_eq!(&text[range], "23456") - /// ``` - #[inline] - pub fn at(offset: TextSize, len: TextSize) -> TextRange { - TextRange::new(offset, offset + len) - } - - /// Create a zero-length range at the specified offset (`offset..offset`). - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let point: TextSize; - /// # point = TextSize::from(3); - /// let range = TextRange::empty(point); - /// assert!(range.is_empty()); - /// assert_eq!(range, TextRange::new(point, point)); - /// ``` - #[inline] - pub fn empty(offset: TextSize) -> TextRange { - TextRange { - start: offset, - end: offset, - } - } - - /// Create a range up to the given end (`..end`). - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let point: TextSize; - /// # point = TextSize::from(12); - /// let range = TextRange::up_to(point); - /// - /// assert_eq!(range.len(), point); - /// assert_eq!(range, TextRange::new(0.into(), point)); - /// assert_eq!(range, TextRange::at(0.into(), point)); - /// ``` - #[inline] - pub fn up_to(end: TextSize) -> TextRange { - TextRange { - start: 0.into(), - end, - } - } -} - -/// Identity methods. -impl TextRange { - /// The start point of this range. - #[inline] - pub const fn start(self) -> TextSize { - self.start - } - - /// The end point of this range. - #[inline] - pub const fn end(self) -> TextSize { - self.end - } - - /// The size of this range. - #[inline] - pub const fn len(self) -> TextSize { - // HACK for const fn: math on primitives only - TextSize { - raw: self.end().raw - self.start().raw, - } - } - - /// Check if this range is empty. - #[inline] - pub const fn is_empty(self) -> bool { - // HACK for const fn: math on primitives only - self.start().raw == self.end().raw - } -} - -/// Manipulation methods. -impl TextRange { - /// Check if this range contains an offset. - /// - /// The end index is considered excluded. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let (start, end): (TextSize, TextSize); - /// # start = 10.into(); end = 20.into(); - /// let range = TextRange::new(start, end); - /// assert!(range.contains(start)); - /// assert!(!range.contains(end)); - /// ``` - #[inline] - pub fn contains(self, offset: TextSize) -> bool { - self.start() <= offset && offset < self.end() - } - - /// Check if this range contains an offset. - /// - /// The end index is considered included. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let (start, end): (TextSize, TextSize); - /// # start = 10.into(); end = 20.into(); - /// let range = TextRange::new(start, end); - /// assert!(range.contains_inclusive(start)); - /// assert!(range.contains_inclusive(end)); - /// ``` - #[inline] - pub fn contains_inclusive(self, offset: TextSize) -> bool { - self.start() <= offset && offset <= self.end() - } - - /// Check if this range completely contains another range. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let larger = TextRange::new(0.into(), 20.into()); - /// let smaller = TextRange::new(5.into(), 15.into()); - /// assert!(larger.contains_range(smaller)); - /// assert!(!smaller.contains_range(larger)); - /// - /// // a range always contains itself - /// assert!(larger.contains_range(larger)); - /// assert!(smaller.contains_range(smaller)); - /// ``` - #[inline] - pub fn contains_range(self, other: TextRange) -> bool { - self.start() <= other.start() && other.end() <= self.end() - } - - /// The range covered by both ranges, if it exists. - /// If the ranges touch but do not overlap, the output range is empty. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// assert_eq!( - /// TextRange::intersect( - /// TextRange::new(0.into(), 10.into()), - /// TextRange::new(5.into(), 15.into()), - /// ), - /// Some(TextRange::new(5.into(), 10.into())), - /// ); - /// ``` - #[inline] - pub fn intersect(self, other: TextRange) -> Option { - let start = cmp::max(self.start(), other.start()); - let end = cmp::min(self.end(), other.end()); - if end < start { - return None; - } - Some(TextRange::new(start, end)) - } - - /// Extends the range to cover `other` as well. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// assert_eq!( - /// TextRange::cover( - /// TextRange::new(0.into(), 5.into()), - /// TextRange::new(15.into(), 20.into()), - /// ), - /// TextRange::new(0.into(), 20.into()), - /// ); - /// ``` - #[inline] - #[must_use] - pub fn cover(self, other: TextRange) -> TextRange { - let start = cmp::min(self.start(), other.start()); - let end = cmp::max(self.end(), other.end()); - TextRange::new(start, end) - } - - /// Extends the range to cover `other` offsets as well. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// assert_eq!( - /// TextRange::empty(0.into()).cover_offset(20.into()), - /// TextRange::new(0.into(), 20.into()), - /// ) - /// ``` - #[inline] - #[must_use] - pub fn cover_offset(self, offset: TextSize) -> TextRange { - self.cover(TextRange::empty(offset)) - } - - /// Add an offset to this range. - /// - /// Note that this is not appropriate for changing where a `TextRange` is - /// within some string; rather, it is for changing the reference anchor - /// that the `TextRange` is measured against. - /// - /// The unchecked version (`Add::add`) will _always_ panic on overflow, - /// in contrast to primitive integers, which check in debug mode only. - #[inline] - pub fn checked_add(self, offset: TextSize) -> Option { - Some(TextRange { - start: self.start.checked_add(offset)?, - end: self.end.checked_add(offset)?, - }) - } - - /// Subtract an offset from this range. - /// - /// Note that this is not appropriate for changing where a `TextRange` is - /// within some string; rather, it is for changing the reference anchor - /// that the `TextRange` is measured against. - /// - /// The unchecked version (`Sub::sub`) will _always_ panic on overflow, - /// in contrast to primitive integers, which check in debug mode only. - #[inline] - pub fn checked_sub(self, offset: TextSize) -> Option { - Some(TextRange { - start: self.start.checked_sub(offset)?, - end: self.end.checked_sub(offset)?, - }) - } - - /// Relative order of the two ranges (overlapping ranges are considered - /// equal). - /// - /// - /// This is useful when, for example, binary searching an array of disjoint - /// ranges. - /// - /// # Examples - /// - /// ``` - /// # use ruff_text_size::*; - /// # use std::cmp::Ordering; - /// - /// let a = TextRange::new(0.into(), 3.into()); - /// let b = TextRange::new(4.into(), 5.into()); - /// assert_eq!(a.ordering(b), Ordering::Less); - /// - /// let a = TextRange::new(0.into(), 3.into()); - /// let b = TextRange::new(3.into(), 5.into()); - /// assert_eq!(a.ordering(b), Ordering::Less); - /// - /// let a = TextRange::new(0.into(), 3.into()); - /// let b = TextRange::new(2.into(), 5.into()); - /// assert_eq!(a.ordering(b), Ordering::Equal); - /// - /// let a = TextRange::new(0.into(), 3.into()); - /// let b = TextRange::new(2.into(), 2.into()); - /// assert_eq!(a.ordering(b), Ordering::Equal); - /// - /// let a = TextRange::new(2.into(), 3.into()); - /// let b = TextRange::new(2.into(), 2.into()); - /// assert_eq!(a.ordering(b), Ordering::Greater); - /// ``` - #[inline] - pub fn ordering(self, other: TextRange) -> Ordering { - if self.end() <= other.start() { - Ordering::Less - } else if other.end() <= self.start() { - Ordering::Greater - } else { - Ordering::Equal - } - } - - /// Subtracts an offset from the start position. - /// - /// - /// ## Panics - /// If `start - amount` is less than zero. - /// - /// ## Examples - /// - /// ``` - /// use ruff_text_size::{Ranged, TextRange, TextSize}; - /// - /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); - /// assert_eq!(range.sub_start(TextSize::from(2)), TextRange::new(TextSize::from(3), TextSize::from(10))); - /// ``` - #[inline] - #[must_use] - pub fn sub_start(&self, amount: TextSize) -> TextRange { - TextRange::new(self.start() - amount, self.end()) - } - - /// Adds an offset to the start position. - /// - /// ## Panics - /// If `start + amount > end` - /// - /// ## Examples - /// - /// ``` - /// use ruff_text_size::{Ranged, TextRange, TextSize}; - /// - /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); - /// assert_eq!(range.add_start(TextSize::from(3)), TextRange::new(TextSize::from(8), TextSize::from(10))); - /// ``` - #[inline] - #[must_use] - pub fn add_start(&self, amount: TextSize) -> TextRange { - TextRange::new(self.start() + amount, self.end()) - } - - /// Subtracts an offset from the end position. - /// - /// - /// ## Panics - /// If `end - amount < 0` or `end - amount < start` - /// - /// ## Examples - /// - /// ``` - /// use ruff_text_size::{Ranged, TextRange, TextSize}; - /// - /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); - /// assert_eq!(range.sub_end(TextSize::from(2)), TextRange::new(TextSize::from(5), TextSize::from(8))); - /// ``` - #[inline] - #[must_use] - pub fn sub_end(&self, amount: TextSize) -> TextRange { - TextRange::new(self.start(), self.end() - amount) - } - - /// Adds an offset to the end position. - /// - /// - /// ## Panics - /// If `end + amount > u32::MAX` - /// - /// ## Examples - /// - /// ``` - /// use ruff_text_size::{Ranged, TextRange, TextSize}; - /// - /// let range = TextRange::new(TextSize::from(5), TextSize::from(10)); - /// assert_eq!(range.add_end(TextSize::from(2)), TextRange::new(TextSize::from(5), TextSize::from(12))); - /// ``` - #[inline] - #[must_use] - pub fn add_end(&self, amount: TextSize) -> TextRange { - TextRange::new(self.start(), self.end() + amount) - } -} - -impl Index for str { - type Output = str; - #[inline] - fn index(&self, index: TextRange) -> &str { - &self[Range::::from(index)] - } -} - -impl Index for String { - type Output = str; - #[inline] - fn index(&self, index: TextRange) -> &str { - &self[Range::::from(index)] - } -} - -impl IndexMut for str { - #[inline] - fn index_mut(&mut self, index: TextRange) -> &mut str { - &mut self[Range::::from(index)] - } -} - -impl IndexMut for String { - #[inline] - fn index_mut(&mut self, index: TextRange) -> &mut str { - &mut self[Range::::from(index)] - } -} - -impl RangeBounds for TextRange { - fn start_bound(&self) -> Bound<&TextSize> { - Bound::Included(&self.start) - } - - fn end_bound(&self) -> Bound<&TextSize> { - Bound::Excluded(&self.end) - } -} - -impl From> for TextRange { - #[inline] - fn from(r: Range) -> Self { - TextRange::new(r.start, r.end) - } -} - -impl From for Range -where - T: From, -{ - #[inline] - fn from(r: TextRange) -> Self { - r.start().into()..r.end().into() - } -} - -macro_rules! ops { - (impl $Op:ident for TextRange by fn $f:ident = $op:tt) => { - impl $Op<&TextSize> for TextRange { - type Output = TextRange; - #[inline] - fn $f(self, other: &TextSize) -> TextRange { - self $op *other - } - } - impl $Op for &TextRange - where - TextRange: $Op, - { - type Output = TextRange; - #[inline] - fn $f(self, other: T) -> TextRange { - *self $op other - } - } - }; -} - -impl Add for TextRange { - type Output = TextRange; - #[inline] - fn add(self, offset: TextSize) -> TextRange { - self.checked_add(offset) - .expect("TextRange +offset overflowed") - } -} - -impl Sub for TextRange { - type Output = TextRange; - #[inline] - fn sub(self, offset: TextSize) -> TextRange { - self.checked_sub(offset) - .expect("TextRange -offset overflowed") - } -} - -ops!(impl Add for TextRange by fn add = +); -ops!(impl Sub for TextRange by fn sub = -); - -impl AddAssign for TextRange -where - TextRange: Add, -{ - #[inline] - fn add_assign(&mut self, rhs: A) { - *self = *self + rhs; - } -} - -impl SubAssign for TextRange -where - TextRange: Sub, -{ - #[inline] - fn sub_assign(&mut self, rhs: S) { - *self = *self - rhs; - } -} diff --git a/crates/ruff_text_size/src/schemars_impls.rs b/crates/ruff_text_size/src/schemars_impls.rs deleted file mode 100644 index a1c7fa36..00000000 --- a/crates/ruff_text_size/src/schemars_impls.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! This module implements the [`JsonSchema`] trait from the `schemars` crate for -//! [`TextSize`] and [`TextRange`] if the `schemars` feature is enabled. This trait -//! exposes meta-information on how a given type is serialized and deserialized -//! using `serde`, and is currently used to generate autocomplete information -//! for the `rome.json` configuration file and TypeScript types for the node.js -//! bindings to the Workspace API - -use crate::{TextRange, TextSize}; -use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; - -impl JsonSchema for TextSize { - fn schema_name() -> String { - String::from("TextSize") - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - // TextSize is represented as a raw u32, see serde_impls.rs for the - // actual implementation - ::json_schema(gen) - } -} - -impl JsonSchema for TextRange { - fn schema_name() -> String { - String::from("TextRange") - } - - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - // TextSize is represented as (TextSize, TextSize), see serde_impls.rs - // for the actual implementation - <(TextSize, TextSize)>::json_schema(gen) - } -} diff --git a/crates/ruff_text_size/src/serde_impls.rs b/crates/ruff_text_size/src/serde_impls.rs deleted file mode 100644 index b6885d67..00000000 --- a/crates/ruff_text_size/src/serde_impls.rs +++ /dev/null @@ -1,47 +0,0 @@ -use { - crate::{TextRange, TextSize}, - serde::{de, Deserialize, Deserializer, Serialize, Serializer}, -}; - -impl Serialize for TextSize { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.raw.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for TextSize { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - u32::deserialize(deserializer).map(TextSize::from) - } -} - -impl Serialize for TextRange { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - (self.start(), self.end()).serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for TextRange { - #[allow(clippy::nonminimal_bool)] - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let (start, end) = Deserialize::deserialize(deserializer)?; - if !(start <= end) { - return Err(de::Error::custom(format!( - "invalid range: {start:?}..{end:?}" - ))); - } - Ok(TextRange::new(start, end)) - } -} diff --git a/crates/ruff_text_size/src/size.rs b/crates/ruff_text_size/src/size.rs deleted file mode 100644 index 1b597698..00000000 --- a/crates/ruff_text_size/src/size.rs +++ /dev/null @@ -1,196 +0,0 @@ -use { - crate::TextLen, - std::{ - convert::TryFrom, - fmt, iter, - num::TryFromIntError, - ops::{Add, AddAssign, Sub, SubAssign}, - }, -}; - -/// A measure of text length. Also, equivalently, an index into text. -/// -/// This is a UTF-8 bytes offset stored as `u32`, but -/// most clients should treat it as an opaque measure. -/// -/// For cases that need to escape `TextSize` and return to working directly -/// with primitive integers, `TextSize` can be converted losslessly to/from -/// `u32` via [`From`] conversions as well as losslessly be converted [`Into`] -/// `usize`. The `usize -> TextSize` direction can be done via [`TryFrom`]. -/// -/// These escape hatches are primarily required for unit testing and when -/// converting from UTF-8 size to another coordinate space, such as UTF-16. -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct TextSize { - pub(crate) raw: u32, -} - -impl fmt::Debug for TextSize { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.raw) - } -} - -impl TextSize { - /// Creates a new `TextSize` at the given `offset`. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// assert_eq!(TextSize::from(4), TextSize::new(4)); - /// ``` - pub const fn new(offset: u32) -> Self { - Self { raw: offset } - } - - /// The text size of some primitive text-like object. - /// - /// Accepts `char`, `&str`, and `&String`. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// let char_size = TextSize::of('🦀'); - /// assert_eq!(char_size, TextSize::from(4)); - /// - /// let str_size = TextSize::of("rust-analyzer"); - /// assert_eq!(str_size, TextSize::from(13)); - /// ``` - #[inline] - pub fn of(text: T) -> TextSize { - text.text_len() - } - - /// Returns current raw `offset` as u32. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// assert_eq!(TextSize::from(4).to_u32(), 4); - /// ``` - pub const fn to_u32(&self) -> u32 { - self.raw - } - - /// Returns current raw `offset` as usize. - /// - /// # Examples - /// - /// ```rust - /// # use ruff_text_size::*; - /// assert_eq!(TextSize::from(4).to_usize(), 4); - /// ``` - pub const fn to_usize(&self) -> usize { - self.raw as usize - } -} - -/// Methods to act like a primitive integer type, where reasonably applicable. -// Last updated for parity with Rust 1.42.0. -impl TextSize { - /// Checked addition. Returns `None` if overflow occurred. - #[inline] - pub fn checked_add(self, rhs: TextSize) -> Option { - self.raw.checked_add(rhs.raw).map(|raw| TextSize { raw }) - } - - /// Checked subtraction. Returns `None` if overflow occurred. - #[inline] - pub fn checked_sub(self, rhs: TextSize) -> Option { - self.raw.checked_sub(rhs.raw).map(|raw| TextSize { raw }) - } -} - -impl From for TextSize { - #[inline] - fn from(raw: u32) -> Self { - TextSize::new(raw) - } -} - -impl From for u32 { - #[inline] - fn from(value: TextSize) -> Self { - value.to_u32() - } -} - -impl TryFrom for TextSize { - type Error = TryFromIntError; - #[inline] - fn try_from(value: usize) -> Result { - Ok(u32::try_from(value)?.into()) - } -} - -impl From for usize { - #[inline] - fn from(value: TextSize) -> Self { - value.to_usize() - } -} - -macro_rules! ops { - (impl $Op:ident for TextSize by fn $f:ident = $op:tt) => { - impl $Op for TextSize { - type Output = TextSize; - #[inline] - fn $f(self, other: TextSize) -> TextSize { - TextSize { raw: self.raw $op other.raw } - } - } - impl $Op<&TextSize> for TextSize { - type Output = TextSize; - #[inline] - fn $f(self, other: &TextSize) -> TextSize { - self $op *other - } - } - impl $Op for &TextSize - where - TextSize: $Op, - { - type Output = TextSize; - #[inline] - fn $f(self, other: T) -> TextSize { - *self $op other - } - } - }; -} - -ops!(impl Add for TextSize by fn add = +); -ops!(impl Sub for TextSize by fn sub = -); - -impl AddAssign for TextSize -where - TextSize: Add, -{ - #[inline] - fn add_assign(&mut self, rhs: A) { - *self = *self + rhs; - } -} - -impl SubAssign for TextSize -where - TextSize: Sub, -{ - #[inline] - fn sub_assign(&mut self, rhs: S) { - *self = *self - rhs; - } -} - -impl iter::Sum for TextSize -where - TextSize: Add, -{ - #[inline] - fn sum>(iter: I) -> TextSize { - iter.fold(0.into(), Add::add) - } -} diff --git a/crates/ruff_text_size/src/traits.rs b/crates/ruff_text_size/src/traits.rs deleted file mode 100644 index 0ea01513..00000000 --- a/crates/ruff_text_size/src/traits.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::sync::Arc; -use {crate::TextRange, crate::TextSize, std::convert::TryInto}; - -use priv_in_pub::Sealed; -mod priv_in_pub { - pub trait Sealed {} -} - -/// Primitives with a textual length that can be passed to [`TextSize::of`]. -pub trait TextLen: Copy + Sealed { - /// The textual length of this primitive. - fn text_len(self) -> TextSize; -} - -impl Sealed for &'_ str {} -impl TextLen for &'_ str { - #[inline] - fn text_len(self) -> TextSize { - self.len().try_into().unwrap() - } -} - -impl Sealed for &'_ String {} -impl TextLen for &'_ String { - #[inline] - fn text_len(self) -> TextSize { - self.as_str().text_len() - } -} - -impl Sealed for char {} -impl TextLen for char { - #[inline] - #[allow(clippy::cast_possible_truncation)] - fn text_len(self) -> TextSize { - (self.len_utf8() as u32).into() - } -} - -/// A ranged item in the source text. -pub trait Ranged { - /// The range of this item in the source text. - fn range(&self) -> TextRange; - - /// The start offset of this item in the source text. - fn start(&self) -> TextSize { - self.range().start() - } - - /// The end offset of this item in the source text. - fn end(&self) -> TextSize { - self.range().end() - } -} - -impl Ranged for TextRange { - fn range(&self) -> TextRange { - *self - } -} - -impl Ranged for &T -where - T: Ranged, -{ - fn range(&self) -> TextRange { - T::range(self) - } -} - -impl Ranged for Arc -where - T: Ranged, -{ - fn range(&self) -> TextRange { - T::range(self) - } -} - -/// A slice of the source text. -pub trait TextSlice: Sealed { - /// Returns the slice of the text within the given `range`. - /// - /// ## Note - /// - /// This is the same as `&self[range]` if `self` is a `str` and `range` a `TextRange`. - /// - /// ## Panics - /// If the range is out of bounds. - fn slice(&self, range: impl Ranged) -> &str; -} - -impl Sealed for str {} - -impl TextSlice for str { - fn slice(&self, ranged: impl Ranged) -> &str { - &self[ranged.range()] - } -} diff --git a/crates/ruff_text_size/tests/auto_traits.rs b/crates/ruff_text_size/tests/auto_traits.rs deleted file mode 100644 index 6adf8bd2..00000000 --- a/crates/ruff_text_size/tests/auto_traits.rs +++ /dev/null @@ -1,18 +0,0 @@ -use { - ruff_text_size::{TextRange, TextSize}, - static_assertions::assert_impl_all, - std::{ - fmt::Debug, - hash::Hash, - marker::{Send, Sync}, - panic::{RefUnwindSafe, UnwindSafe}, - }, -}; - -// auto traits -assert_impl_all!(TextSize: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); -assert_impl_all!(TextRange: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe); - -// common traits -assert_impl_all!(TextSize: Copy, Debug, Default, Hash, Ord); -assert_impl_all!(TextRange: Copy, Debug, Default, Hash, Eq); diff --git a/crates/ruff_text_size/tests/constructors.rs b/crates/ruff_text_size/tests/constructors.rs deleted file mode 100644 index 8ee2fde9..00000000 --- a/crates/ruff_text_size/tests/constructors.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ruff_text_size::TextSize; - -#[derive(Copy, Clone)] -struct BadRope<'a>(&'a [&'a str]); - -impl BadRope<'_> { - fn text_len(self) -> TextSize { - self.0.iter().copied().map(TextSize::of).sum() - } -} - -#[test] -fn main() { - let x: char = 'c'; - let _ = TextSize::of(x); - - let x: &str = "hello"; - let _ = TextSize::of(x); - - let x: &String = &"hello".into(); - let _ = TextSize::of(x); - - let _ = BadRope(&[""]).text_len(); -} diff --git a/crates/ruff_text_size/tests/indexing.rs b/crates/ruff_text_size/tests/indexing.rs deleted file mode 100644 index e7205fde..00000000 --- a/crates/ruff_text_size/tests/indexing.rs +++ /dev/null @@ -1,8 +0,0 @@ -use ruff_text_size::TextRange; - -#[test] -fn main() { - let range = TextRange::default(); - let _ = &""[range]; - let _ = &String::new()[range]; -} diff --git a/crates/ruff_text_size/tests/main.rs b/crates/ruff_text_size/tests/main.rs deleted file mode 100644 index 8d8dd091..00000000 --- a/crates/ruff_text_size/tests/main.rs +++ /dev/null @@ -1,79 +0,0 @@ -use { - ruff_text_size::{TextRange, TextSize}, - std::ops, -}; - -fn size(x: u32) -> TextSize { - TextSize::from(x) -} - -fn range(x: ops::Range) -> TextRange { - TextRange::new(x.start.into(), x.end.into()) -} - -#[test] -fn sum() { - let xs: Vec = vec![size(0), size(1), size(2)]; - assert_eq!(xs.iter().sum::(), size(3)); - assert_eq!(xs.into_iter().sum::(), size(3)); -} - -#[test] -fn math() { - assert_eq!(size(10) + size(5), size(15)); - assert_eq!(size(10) - size(5), size(5)); -} - -#[test] -fn checked_math() { - assert_eq!(size(1).checked_add(size(1)), Some(size(2))); - assert_eq!(size(1).checked_sub(size(1)), Some(size(0))); - assert_eq!(size(1).checked_sub(size(2)), None); - assert_eq!(size(!0).checked_add(size(1)), None); -} - -#[test] -#[rustfmt::skip] -fn contains() { - assert!( range(2..4).contains_range(range(2..3))); - assert!( ! range(2..4).contains_range(range(1..3))); -} - -#[test] -fn intersect() { - assert_eq!(range(1..2).intersect(range(2..3)), Some(range(2..2))); - assert_eq!(range(1..5).intersect(range(2..3)), Some(range(2..3))); - assert_eq!(range(1..2).intersect(range(3..4)), None); -} - -#[test] -fn cover() { - assert_eq!(range(1..2).cover(range(2..3)), range(1..3)); - assert_eq!(range(1..5).cover(range(2..3)), range(1..5)); - assert_eq!(range(1..2).cover(range(4..5)), range(1..5)); -} - -#[test] -fn cover_offset() { - assert_eq!(range(1..3).cover_offset(size(0)), range(0..3)); - assert_eq!(range(1..3).cover_offset(size(1)), range(1..3)); - assert_eq!(range(1..3).cover_offset(size(2)), range(1..3)); - assert_eq!(range(1..3).cover_offset(size(3)), range(1..3)); - assert_eq!(range(1..3).cover_offset(size(4)), range(1..4)); -} - -#[test] -#[rustfmt::skip] -fn contains_point() { - assert!( ! range(1..3).contains(size(0))); - assert!( range(1..3).contains(size(1))); - assert!( range(1..3).contains(size(2))); - assert!( ! range(1..3).contains(size(3))); - assert!( ! range(1..3).contains(size(4))); - - assert!( ! range(1..3).contains_inclusive(size(0))); - assert!( range(1..3).contains_inclusive(size(1))); - assert!( range(1..3).contains_inclusive(size(2))); - assert!( range(1..3).contains_inclusive(size(3))); - assert!( ! range(1..3).contains_inclusive(size(4))); -} diff --git a/crates/ruff_text_size/tests/serde.rs b/crates/ruff_text_size/tests/serde.rs deleted file mode 100644 index 0d8f9d4a..00000000 --- a/crates/ruff_text_size/tests/serde.rs +++ /dev/null @@ -1,83 +0,0 @@ -use { - ruff_text_size::{TextRange, TextSize}, - serde_test::{assert_de_tokens_error, assert_tokens, Token}, - std::ops, -}; - -fn size(x: u32) -> TextSize { - TextSize::from(x) -} - -fn range(x: ops::Range) -> TextRange { - TextRange::new(x.start.into(), x.end.into()) -} - -#[test] -fn size_serialization() { - assert_tokens(&size(00), &[Token::U32(00)]); - assert_tokens(&size(10), &[Token::U32(10)]); - assert_tokens(&size(20), &[Token::U32(20)]); - assert_tokens(&size(30), &[Token::U32(30)]); -} - -#[test] -fn range_serialization() { - assert_tokens( - &range(00..10), - &[ - Token::Tuple { len: 2 }, - Token::U32(00), - Token::U32(10), - Token::TupleEnd, - ], - ); - assert_tokens( - &range(10..20), - &[ - Token::Tuple { len: 2 }, - Token::U32(10), - Token::U32(20), - Token::TupleEnd, - ], - ); - assert_tokens( - &range(20..30), - &[ - Token::Tuple { len: 2 }, - Token::U32(20), - Token::U32(30), - Token::TupleEnd, - ], - ); - assert_tokens( - &range(30..40), - &[ - Token::Tuple { len: 2 }, - Token::U32(30), - Token::U32(40), - Token::TupleEnd, - ], - ); -} - -#[test] -fn invalid_range_deserialization() { - assert_tokens::( - &range(62..92), - &[ - Token::Tuple { len: 2 }, - Token::U32(62), - Token::U32(92), - Token::TupleEnd, - ], - ); - assert_de_tokens_error::( - &[ - Token::Tuple { len: 2 }, - Token::U32(92), - Token::U32(62), - Token::TupleEnd, - ], - "invalid range: 92..62", - ); -} From 7ae11dbdc969cb3b91dde3c6e1c33242069bff18 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 26 Dec 2024 13:08:47 -0500 Subject: [PATCH 05/44] Add in support for view `ViewFile` LSP method --- Cargo.lock | 3 + crates/ruff_server/Cargo.toml | 5 +- crates/ruff_server/src/server/api.rs | 3 + crates/ruff_server/src/server/api/requests.rs | 2 + .../src/server/api/requests/view_file.rs | 137 ++++++++++++++++++ 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 crates/ruff_server/src/server/api/requests/view_file.rs diff --git a/Cargo.lock b/Cargo.lock index 8eae5823..e3854b29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1705,6 +1705,7 @@ dependencies = [ "air_r_formatter", "air_r_parser", "anyhow", + "biome_formatter", "biome_text_size", "cargo_metadata", "crossbeam", @@ -1722,6 +1723,8 @@ dependencies = [ "thiserror 2.0.5", "tracing", "tracing-subscriber", + "tree-sitter", + "tree-sitter-r", "workspace", ] diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 7b0654d9..53a9d0a4 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -14,6 +14,7 @@ license = { workspace = true } [dependencies] air_r_formatter = { workspace = true } air_r_parser = { workspace = true } +biome_formatter = { workspace = true } biome_text_size = { workspace = true } ruff_source_file = { workspace = true } workspace = { workspace = true } @@ -30,7 +31,9 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } +tracing-subscriber = { workspace = true, features = ["ansi", "local-time"] } +tree-sitter = { workspace = true } +tree-sitter-r = { workspace = true } [dev-dependencies] insta = { workspace = true } diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index 3f1d084f..fd9eace3 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -40,6 +40,9 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> { request::FormatRange::METHOD => { background_request_task::(req, BackgroundSchedule::Fmt) } + request::ViewFile::METHOD => { + background_request_task::(req, BackgroundSchedule::Fmt) + } method => { tracing::warn!("Received request {method} which does not have a handler"); return Task::nothing(); diff --git a/crates/ruff_server/src/server/api/requests.rs b/crates/ruff_server/src/server/api/requests.rs index a08a2edf..c8e35fee 100644 --- a/crates/ruff_server/src/server/api/requests.rs +++ b/crates/ruff_server/src/server/api/requests.rs @@ -1,5 +1,6 @@ mod format; mod format_range; +mod view_file; use super::{ define_document_url, @@ -7,5 +8,6 @@ use super::{ }; pub(super) use format::Format; pub(super) use format_range::FormatRange; +pub(super) use view_file::ViewFile; type FormatResponse = Option>; diff --git a/crates/ruff_server/src/server/api/requests/view_file.rs b/crates/ruff_server/src/server/api/requests/view_file.rs new file mode 100644 index 00000000..759ba297 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/view_file.rs @@ -0,0 +1,137 @@ +use air_r_formatter::format_node; +use air_r_parser::RParserOptions; +use lsp_types::request::Request; +use serde::Deserialize; +use serde::Serialize; + +use crate::server::api::LSPResult; +use crate::server::client::Notifier; +use crate::server::Result; +use crate::session::DocumentSnapshot; + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +pub(crate) enum ViewFileKind { + TreeSitter, + SyntaxTree, + FormatTree, +} + +#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ViewFileParams { + /// From `lsp_types::TextDocumentPositionParams` + pub(crate) text_document: lsp_types::TextDocumentIdentifier, + pub(crate) position: lsp_types::Position, + + /// Viewer type + pub(crate) kind: ViewFileKind, +} + +#[derive(Debug)] +pub(crate) enum ViewFile {} + +impl Request for ViewFile { + type Params = ViewFileParams; + type Result = String; + const METHOD: &'static str = "air/viewFile"; +} + +impl super::RequestHandler for ViewFile { + type RequestType = ViewFile; +} + +impl super::BackgroundDocumentRequestHandler for ViewFile { + super::define_document_url!(params: &ViewFileParams); + fn run_with_snapshot( + snapshot: DocumentSnapshot, + _notifier: Notifier, + params: ViewFileParams, + ) -> Result { + view_file(&snapshot, ¶ms) + } +} + +fn view_file(snapshot: &DocumentSnapshot, params: &ViewFileParams) -> Result { + let contents = snapshot.query().as_single_document().contents(); + let settings = snapshot.query().settings(); + + match params.kind { + ViewFileKind::TreeSitter => { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(&tree_sitter_r::LANGUAGE.into()) + .map_err(anyhow::Error::new) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + + let Some(ast) = parser.parse(contents, None) else { + return Err(anyhow::anyhow!("Internal error during document parsing.")) + .with_failure_code(lsp_server::ErrorCode::InternalError); + }; + + if ast.root_node().has_error() { + return Ok(String::from("*Parse error*")); + } + + let mut output = String::new(); + let mut cursor = ast.root_node().walk(); + format_ts_node(&mut cursor, 0, &mut output); + Ok(output) + } + + ViewFileKind::SyntaxTree => { + let parse = air_r_parser::parse(contents, RParserOptions::default()); + + if parse.has_errors() { + return Ok(String::from("*Parse error*")); + } + + Ok(format!("{syntax:#?}", syntax = parse.syntax())) + } + + ViewFileKind::FormatTree => { + let parse = air_r_parser::parse(contents, RParserOptions::default()); + + if parse.has_errors() { + return Ok(String::from("*Parse error*")); + } + + let format_options = settings.format.to_format_options(contents); + + let formatted = format_node(format_options, &parse.syntax()) + .map_err(anyhow::Error::new) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + + Ok(format!("{document}", document = formatted.into_document())) + } + } +} + +fn format_ts_node(cursor: &mut tree_sitter::TreeCursor, depth: usize, output: &mut String) { + let node = cursor.node(); + let field_name = match cursor.field_name() { + Some(name) => format!("{name}: "), + None => String::new(), + }; + + let start = node.start_position(); + let end = node.end_position(); + let node_type = node.kind(); + + let indent = " ".repeat(depth * 4); + let start = format!("{}, {}", start.row, start.column); + let end = format!("{}, {}", end.row, end.column); + + output.push_str(&format!( + "{indent}{field_name}{node_type} [{start}] - [{end}]\n", + )); + + if cursor.goto_first_child() { + loop { + format_ts_node(cursor, depth + 1, output); + if !cursor.goto_next_sibling() { + break; + } + } + cursor.goto_parent(); + } +} From b3a24eb0a87e5d5fbc3d25faf989a86d1f08a36e Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 26 Dec 2024 13:19:28 -0500 Subject: [PATCH 06/44] Eat unused `setTrace` notification --- crates/ruff_server/src/server/api.rs | 1 + .../src/server/api/notifications.rs | 2 ++ .../src/server/api/notifications/set_trace.rs | 26 +++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 crates/ruff_server/src/server/api/notifications/set_trace.rs diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index fd9eace3..beab501c 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -75,6 +75,7 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { } notification::DidClose::METHOD => local_notification_task::(notif), notification::DidOpen::METHOD => local_notification_task::(notif), + notification::SetTrace::METHOD => local_notification_task::(notif), method => { tracing::warn!("Received notification {method} which does not have a handler."); return Task::nothing(); diff --git a/crates/ruff_server/src/server/api/notifications.rs b/crates/ruff_server/src/server/api/notifications.rs index 231f8f2d..2933a03d 100644 --- a/crates/ruff_server/src/server/api/notifications.rs +++ b/crates/ruff_server/src/server/api/notifications.rs @@ -5,6 +5,7 @@ mod did_change_watched_files; mod did_change_workspace; mod did_close; mod did_open; +mod set_trace; use super::traits::{NotificationHandler, SyncNotificationHandler}; pub(super) use cancel::Cancel; @@ -14,3 +15,4 @@ pub(super) use did_change_watched_files::DidChangeWatchedFiles; pub(super) use did_change_workspace::DidChangeWorkspace; pub(super) use did_close::DidClose; pub(super) use did_open::DidOpen; +pub(super) use set_trace::SetTrace; diff --git a/crates/ruff_server/src/server/api/notifications/set_trace.rs b/crates/ruff_server/src/server/api/notifications/set_trace.rs new file mode 100644 index 00000000..9c3a5716 --- /dev/null +++ b/crates/ruff_server/src/server/api/notifications/set_trace.rs @@ -0,0 +1,26 @@ +use crate::server::client::{Notifier, Requester}; +use crate::server::Result; +use crate::session::Session; +use lsp_types as types; +use lsp_types::notification as notif; + +pub(crate) struct SetTrace; + +impl super::NotificationHandler for SetTrace { + type NotificationType = notif::SetTrace; +} + +impl super::SyncNotificationHandler for SetTrace { + fn run( + _session: &mut Session, + _notifier: Notifier, + _requester: &mut Requester, + params: types::SetTraceParams, + ) -> Result<()> { + // Clients always send this request on initialization, but we don't use + // log information from here. + let value = params.value; + tracing::trace!("Ignoring `$/setTrace` notification with value {value:?}"); + Ok(()) + } +} From cd1705bc7944d31bbc03fa9b0d58b8069737771e Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Wed, 1 Jan 2025 17:44:34 -0500 Subject: [PATCH 07/44] Add range formatting support, add test client support --- Cargo.lock | 19 + Cargo.toml | 1 + crates/air/Cargo.toml | 1 + crates/air/src/commands/format.rs | 2 +- crates/air/src/commands/language_server.rs | 4 +- crates/air_formatter_test/src/spec.rs | 2 +- crates/air_r_parser/tests/spec_test.rs | 2 +- crates/ruff_server/Cargo.toml | 13 +- crates/ruff_server/src/edit.rs | 9 +- crates/ruff_server/src/edit/replacement.rs | 234 --------- crates/ruff_server/src/edit/text_diff.rs | 44 ++ crates/ruff_server/src/edit/text_document.rs | 97 +++- crates/ruff_server/src/edit/text_edit.rs | 338 ++++++++++++ crates/ruff_server/src/format.rs | 48 -- crates/ruff_server/src/lib.rs | 5 +- crates/ruff_server/src/logging.rs | 32 +- crates/ruff_server/src/message.rs | 15 +- crates/ruff_server/src/proto.rs | 4 + crates/ruff_server/src/proto/text_edit.rs | 39 ++ .../{edit/range.rs => proto/text_range.rs} | 87 +-- crates/ruff_server/src/server.rs | 29 +- .../src/server/api/requests/format.rs | 120 ++++- .../src/server/api/requests/format_range.rs | 497 +++++++++++++++++- ..._api__requests__format__tests__format.snap | 7 + ...__tests__format_range_logical_lines-2.snap | 7 + ...__tests__format_range_logical_lines-3.snap | 6 + ...__tests__format_range_logical_lines-4.snap | 8 + ...__tests__format_range_logical_lines-5.snap | 11 + ...ge__tests__format_range_logical_lines.snap | 6 + ...tests__format_range_mismatched_indent.snap | 6 + ..._tests__format_range_multiple_lines-2.snap | 7 + ...e__tests__format_range_multiple_lines.snap | 7 + ...mat_range__tests__format_range_none-2.snap | 5 + ...mat_range__tests__format_range_none-3.snap | 5 + ...ormat_range__tests__format_range_none.snap | 5 + ...tests__format_range_unmatched_lists-2.snap | 10 + ...tests__format_range_unmatched_lists-3.snap | 10 + ...tests__format_range_unmatched_lists-4.snap | 10 + ...tests__format_range_unmatched_lists-5.snap | 6 + ...tests__format_range_unmatched_lists-6.snap | 6 + ...__tests__format_range_unmatched_lists.snap | 10 + crates/ruff_server/src/server/connection.rs | 14 +- crates/ruff_server/src/test.rs | 7 + crates/ruff_server/src/test/client.rs | 78 +++ crates/ruff_server/src/test/client_ext.rs | 152 ++++++ crates/ruff_server/src/test/utils.rs | 27 + crates/ruff_source_file/src/lib.rs | 2 +- crates/ruff_source_file/src/line_index.rs | 16 +- crates/ruff_source_file/src/newlines.rs | 58 +- crates/server_test/Cargo.toml | 20 + crates/server_test/src/lib.rs | 3 + crates/server_test/src/lsp_client.rs | 185 +++++++ 52 files changed, 1888 insertions(+), 448 deletions(-) delete mode 100644 crates/ruff_server/src/edit/replacement.rs create mode 100644 crates/ruff_server/src/edit/text_diff.rs create mode 100644 crates/ruff_server/src/edit/text_edit.rs delete mode 100644 crates/ruff_server/src/format.rs create mode 100644 crates/ruff_server/src/proto.rs create mode 100644 crates/ruff_server/src/proto/text_edit.rs rename crates/ruff_server/src/{edit/range.rs => proto/text_range.rs} (79%) create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format__tests__format.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-2.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-3.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap create mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap create mode 100644 crates/ruff_server/src/test.rs create mode 100644 crates/ruff_server/src/test/client.rs create mode 100644 crates/ruff_server/src/test/client_ext.rs create mode 100644 crates/ruff_server/src/test/utils.rs create mode 100644 crates/server_test/Cargo.toml create mode 100644 crates/server_test/src/lib.rs create mode 100644 crates/server_test/src/lsp_client.rs diff --git a/Cargo.lock b/Cargo.lock index e3854b29..6ec2d28f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,7 @@ dependencies = [ "fs", "ignore", "itertools", + "lsp-server", "ruff_server", "ruff_source_file", "tempfile", @@ -1702,15 +1703,21 @@ dependencies = [ name = "ruff_server" version = "0.1.1" dependencies = [ + "air_r_factory", "air_r_formatter", "air_r_parser", + "air_r_syntax", "anyhow", + "assert_matches", "biome_formatter", + "biome_rowan", "biome_text_size", "cargo_metadata", "crossbeam", + "dissimilar", "ignore", "insta", + "itertools", "jod-thread", "libc", "lsp-server", @@ -1720,11 +1727,14 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "server_test", "thiserror 2.0.5", "tracing", "tracing-subscriber", "tree-sitter", "tree-sitter-r", + "url", + "uuid", "workspace", ] @@ -1931,6 +1941,15 @@ dependencies = [ "serde", ] +[[package]] +name = "server_test" +version = "0.0.0" +dependencies = [ + "lsp-server", + "lsp-types 0.95.1", + "url", +] + [[package]] name = "sharded-slab" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 5f87a4eb..a05bfb8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ lsp_test = { path = "./crates/lsp_test" } tests_macros = { path = "./crates/tests_macros" } ruff_server = { path = "./crates/ruff_server" } ruff_source_file = { path = "./crates/ruff_source_file" } +server_test = { path = "./crates/server_test" } workspace = { path = "./crates/workspace" } anyhow = "1.0.89" diff --git a/crates/air/Cargo.toml b/crates/air/Cargo.toml index 120bce02..d2897f87 100644 --- a/crates/air/Cargo.toml +++ b/crates/air/Cargo.toml @@ -23,6 +23,7 @@ clap = { workspace = true, features = ["wrap_help"] } fs = { workspace = true } ignore = { workspace = true } itertools = { workspace = true } +lsp-server = { workspace = true } ruff_server = { workspace = true } ruff_source_file = { workspace = true } thiserror = { workspace = true } diff --git a/crates/air/src/commands/format.rs b/crates/air/src/commands/format.rs index b6dca832..949f13df 100644 --- a/crates/air/src/commands/format.rs +++ b/crates/air/src/commands/format.rs @@ -129,7 +129,7 @@ fn format_file( let options = settings.to_format_options(&source); - let source = ruff_source_file::normalize_crlf_newlines(source); + let (source, _) = ruff_source_file::normalize_newlines(source); let formatted = match format_source(source.as_str(), options) { Ok(formatted) => formatted, Err(err) => return Err(FormatCommandError::Format(path.clone(), err)), diff --git a/crates/air/src/commands/language_server.rs b/crates/air/src/commands/language_server.rs index 490d847f..c921c4aa 100644 --- a/crates/air/src/commands/language_server.rs +++ b/crates/air/src/commands/language_server.rs @@ -13,6 +13,8 @@ pub(crate) fn language_server(_command: LanguageServerCommand) -> anyhow::Result .unwrap_or(four) .max(four); - let server = Server::new(worker_threads)?; + let (connection, connection_threads) = lsp_server::Connection::stdio(); + + let server = Server::new(worker_threads, connection, Some(connection_threads))?; server.run().map(|()| ExitStatus::Success) } diff --git a/crates/air_formatter_test/src/spec.rs b/crates/air_formatter_test/src/spec.rs index c0fea238..9dddd2ff 100644 --- a/crates/air_formatter_test/src/spec.rs +++ b/crates/air_formatter_test/src/spec.rs @@ -29,7 +29,7 @@ impl<'a> SpecTestFile<'a> { let input_code = std::fs::read_to_string(input_file).unwrap(); // Normalize to Unix line endings - let input_code = ruff_source_file::normalize_crlf_newlines(input_code); + let (input_code, _) = ruff_source_file::normalize_newlines(input_code); // For the whole file, not a specific range right now let range_start_index = None; diff --git a/crates/air_r_parser/tests/spec_test.rs b/crates/air_r_parser/tests/spec_test.rs index f70f365d..0a3d570e 100644 --- a/crates/air_r_parser/tests/spec_test.rs +++ b/crates/air_r_parser/tests/spec_test.rs @@ -53,7 +53,7 @@ pub fn run(test_case: &str, _snapshot_name: &str, test_directory: &str, outcome_ .expect("Expected test path to be a readable file in UTF8 encoding"); // Normalize to Unix line endings - let content = ruff_source_file::normalize_crlf_newlines(content); + let (content, _) = ruff_source_file::normalize_newlines(content); let options = RParserOptions::default(); let parsed = parse(&content, options); diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 53a9d0a4..c8770562 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -12,16 +12,21 @@ license = { workspace = true } [lib] [dependencies] +air_r_factory = { workspace = true } air_r_formatter = { workspace = true } air_r_parser = { workspace = true } -biome_formatter = { workspace = true } -biome_text_size = { workspace = true } +air_r_syntax = { workspace = true } ruff_source_file = { workspace = true } workspace = { workspace = true } anyhow = { workspace = true } +biome_formatter = { workspace = true } +biome_rowan = { workspace = true } +biome_text_size = { workspace = true } crossbeam = { workspace = true } +dissimilar = { workspace = true } ignore = { workspace = true } +itertools = { workspace = true } jod-thread = { workspace = true } lsp-server = { workspace = true } lsp-types = { workspace = true } @@ -34,9 +39,13 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["ansi", "local-time"] } tree-sitter = { workspace = true } tree-sitter-r = { workspace = true } +url = { workspace = true } +uuid = { workspace = true, features = ["v4"] } [dev-dependencies] +assert_matches = { workspace = true } insta = { workspace = true } +server_test = { workspace = true } [target.'cfg(target_vendor = "apple")'.dependencies] libc = { workspace = true } diff --git a/crates/ruff_server/src/edit.rs b/crates/ruff_server/src/edit.rs index 5ea2240d..c9f5f2a5 100644 --- a/crates/ruff_server/src/edit.rs +++ b/crates/ruff_server/src/edit.rs @@ -6,15 +6,14 @@ //! Types and utilities for working with text, modifying source files, and `Ruff <-> LSP` type conversion. -mod range; -mod replacement; +mod text_diff; mod text_document; +mod text_edit; use lsp_types::{PositionEncodingKind, Url}; -pub(crate) use range::{RangeExt, ToRangeExt}; -pub(crate) use replacement::Replacement; -pub use text_document::TextDocument; +pub(crate) use text_document::TextDocument; pub(crate) use text_document::{DocumentVersion, LanguageId}; +pub(crate) use text_edit::{Indel, TextEdit}; /// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`]. // Please maintain the order from least to greatest priority for the derived `Ord` impl. diff --git a/crates/ruff_server/src/edit/replacement.rs b/crates/ruff_server/src/edit/replacement.rs deleted file mode 100644 index 3729e2fc..00000000 --- a/crates/ruff_server/src/edit/replacement.rs +++ /dev/null @@ -1,234 +0,0 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - -use biome_text_size::{TextLen, TextRange, TextSize}; - -#[derive(Debug)] -pub(crate) struct Replacement { - pub(crate) source_range: TextRange, - pub(crate) modified_range: TextRange, -} - -impl Replacement { - /// Creates a [`Replacement`] that describes the `source_range` of `source` to replace - /// with `modified` sliced by `modified_range`. - pub(crate) fn between( - source: &str, - source_line_starts: &[TextSize], - modified: &str, - modified_line_starts: &[TextSize], - ) -> Self { - let mut source_start = TextSize::default(); - let mut modified_start = TextSize::default(); - - for (source_line_start, modified_line_start) in source_line_starts - .iter() - .copied() - .zip(modified_line_starts.iter().copied()) - .skip(1) - { - if source[TextRange::new(source_start, source_line_start)] - != modified[TextRange::new(modified_start, modified_line_start)] - { - break; - } - source_start = source_line_start; - modified_start = modified_line_start; - } - - let mut source_end = source.text_len(); - let mut modified_end = modified.text_len(); - - for (source_line_start, modified_line_start) in source_line_starts - .iter() - .rev() - .copied() - .zip(modified_line_starts.iter().rev().copied()) - { - if source_line_start < source_start - || modified_line_start < modified_start - || source[TextRange::new(source_line_start, source_end)] - != modified[TextRange::new(modified_line_start, modified_end)] - { - break; - } - source_end = source_line_start; - modified_end = modified_line_start; - } - - Replacement { - source_range: TextRange::new(source_start, source_end), - modified_range: TextRange::new(modified_start, modified_end), - } - } -} - -#[cfg(test)] -mod tests { - use std::ops::Range; - - use biome_text_size::TextRange; - use ruff_source_file::LineIndex; - - use super::Replacement; - - fn compute_replacement(source: &str, modified: &str) -> (Replacement, String) { - let source_index = LineIndex::from_source_text(source); - let modified_index = LineIndex::from_source_text(modified); - let replacement = Replacement::between( - source, - source_index.line_starts(), - modified, - modified_index.line_starts(), - ); - let range: Range = replacement.source_range.into(); - let mut expected = source.to_string(); - expected.replace_range(range, &modified[replacement.modified_range]); - (replacement, expected) - } - - #[test] - fn delete_first_line() { - let source = "aaaa -bbbb -cccc -"; - let modified = "bbbb -cccc -"; - let (replacement, expected) = compute_replacement(source, modified); - assert_eq!(replacement.source_range, TextRange::new(0.into(), 5.into())); - assert_eq!(replacement.modified_range, TextRange::empty(0.into())); - assert_eq!(modified, &expected); - } - - #[test] - fn delete_middle_line() { - let source = "aaaa -bbbb -cccc -dddd -"; - let modified = "aaaa -bbbb -dddd -"; - let (replacement, expected) = compute_replacement(source, modified); - assert_eq!( - replacement.source_range, - TextRange::new(10.into(), 15.into()) - ); - assert_eq!(replacement.modified_range, TextRange::empty(10.into())); - assert_eq!(modified, &expected); - } - - #[test] - fn delete_multiple_lines() { - let source = "aaaa -bbbb -cccc -dddd -eeee -ffff -"; - let modified = "aaaa -cccc -dddd -ffff -"; - let (replacement, expected) = compute_replacement(source, modified); - assert_eq!( - replacement.source_range, - TextRange::new(5.into(), 25.into()) - ); - assert_eq!( - replacement.modified_range, - TextRange::new(5.into(), 15.into()) - ); - assert_eq!(modified, &expected); - } - - #[test] - fn insert_first_line() { - let source = "bbbb -cccc -"; - let modified = "aaaa -bbbb -cccc -"; - let (replacement, expected) = compute_replacement(source, modified); - assert_eq!(replacement.source_range, TextRange::empty(0.into())); - assert_eq!( - replacement.modified_range, - TextRange::new(0.into(), 5.into()) - ); - assert_eq!(modified, &expected); - } - - #[test] - fn insert_middle_line() { - let source = "aaaa -cccc -"; - let modified = "aaaa -bbbb -cccc -"; - let (replacement, expected) = compute_replacement(source, modified); - assert_eq!(replacement.source_range, TextRange::empty(5.into())); - assert_eq!( - replacement.modified_range, - TextRange::new(5.into(), 10.into()) - ); - assert_eq!(modified, &expected); - } - - #[test] - fn insert_multiple_lines() { - let source = "aaaa -cccc -eeee -"; - let modified = "aaaa -bbbb -cccc -dddd -"; - let (replacement, expected) = compute_replacement(source, modified); - assert_eq!( - replacement.source_range, - TextRange::new(5.into(), 15.into()) - ); - assert_eq!( - replacement.modified_range, - TextRange::new(5.into(), 20.into()) - ); - assert_eq!(modified, &expected); - } - - #[test] - fn replace_lines() { - let source = "aaaa -bbbb -cccc -"; - let modified = "aaaa -bbcb -cccc -"; - let (replacement, expected) = compute_replacement(source, modified); - assert_eq!( - replacement.source_range, - TextRange::new(5.into(), 10.into()) - ); - assert_eq!( - replacement.modified_range, - TextRange::new(5.into(), 10.into()) - ); - assert_eq!(modified, &expected); - } -} diff --git a/crates/ruff_server/src/edit/text_diff.rs b/crates/ruff_server/src/edit/text_diff.rs new file mode 100644 index 00000000..72900368 --- /dev/null +++ b/crates/ruff_server/src/edit/text_diff.rs @@ -0,0 +1,44 @@ +// --- source +// authors = ["rust-analyzer team"] +// license = "MIT OR Apache-2.0" +// origin = "https://github.com/rust-lang/rust-analyzer/blob/8d5e91c9/crates/rust-analyzer/src/handlers/request.rs#L2483" +// --- + +use biome_text_size::{TextRange, TextSize}; + +use super::text_edit::TextEdit; + +pub(super) fn text_diff(left: &str, right: &str) -> TextEdit { + use dissimilar::Chunk; + + let chunks = dissimilar::diff(left, right); + + let mut builder = TextEdit::builder(); + let mut pos = TextSize::default(); + + let mut chunks = chunks.into_iter().peekable(); + while let Some(chunk) = chunks.next() { + if let (Chunk::Delete(deleted), Some(&Chunk::Insert(inserted))) = (chunk, chunks.peek()) { + chunks.next().unwrap(); + let deleted_len = TextSize::of(deleted); + builder.replace(TextRange::at(pos, deleted_len), inserted.into()); + pos += deleted_len; + continue; + } + + match chunk { + Chunk::Equal(text) => { + pos += TextSize::of(text); + } + Chunk::Delete(deleted) => { + let deleted_len = TextSize::of(deleted); + builder.delete(TextRange::at(pos, deleted_len)); + pos += deleted_len; + } + Chunk::Insert(inserted) => { + builder.insert(pos, inserted.into()); + } + } + } + builder.finish() +} diff --git a/crates/ruff_server/src/edit/text_document.rs b/crates/ruff_server/src/edit/text_document.rs index 71c91a98..6bf3f1f5 100644 --- a/crates/ruff_server/src/edit/text_document.rs +++ b/crates/ruff_server/src/edit/text_document.rs @@ -4,12 +4,13 @@ // | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | // +------------------------------------------------------------+ +use biome_rowan::TextRange; use lsp_types::TextDocumentContentChangeEvent; +use ruff_source_file::LineEnding; use ruff_source_file::LineIndex; use crate::edit::PositionEncoding; - -use super::RangeExt; +use crate::proto::TextRangeExt; pub(crate) type DocumentVersion = i32; @@ -17,8 +18,10 @@ pub(crate) type DocumentVersion = i32; /// with changes made by the user, including unsaved changes. #[derive(Debug, Clone)] pub struct TextDocument { - /// The string contents of the document. + /// The string contents of the document, normalized to unix line endings. contents: String, + /// The original line endings of the document. + ending: LineEnding, /// A computed line index for the document. This should always reflect /// the current version of `contents`. Using a function like [`Self::modify`] /// will re-calculate the line index automatically when the `contents` value is updated. @@ -47,15 +50,30 @@ impl From<&str> for LanguageId { impl TextDocument { pub fn new(contents: String, version: DocumentVersion) -> Self { + // Normalize to Unix line endings + let (contents, ending) = ruff_source_file::normalize_newlines(contents); let index = LineIndex::from_source_text(&contents); Self { contents, + ending, index, version, language_id: None, } } + #[cfg(test)] + pub fn doodle(contents: &str) -> Self { + Self::new(contents.into(), 0) + } + + #[cfg(test)] + pub fn doodle_and_range(contents: &str) -> (Self, biome_text_size::TextRange) { + let (contents, range) = crate::test::extract_marked_range(contents); + let doc = Self::new(contents, 0); + (doc, range) + } + #[must_use] pub fn with_language_id(mut self, language_id: &str) -> Self { self.language_id = Some(LanguageId::from(language_id)); @@ -71,6 +89,10 @@ impl TextDocument { &self.contents } + pub fn ending(&self) -> LineEnding { + self.ending + } + pub fn index(&self) -> &LineIndex { &self.index } @@ -85,15 +107,24 @@ impl TextDocument { pub fn apply_changes( &mut self, - changes: Vec, + mut changes: Vec, new_version: DocumentVersion, encoding: PositionEncoding, ) { + // Normalize line endings. Changing the line length of inserted or + // replaced text can't invalidate the text change events, even those + // applied subsequently, since those changes are specified with [line, + // col] coordinates. + for change in &mut changes.iter_mut() { + let text = std::mem::take(&mut change.text); + (change.text, _) = ruff_source_file::normalize_newlines(text); + } + if let [lsp_types::TextDocumentContentChangeEvent { range: None, text, .. }] = changes.as_slice() { - tracing::debug!("Fast path - replacing entire document"); + tracing::trace!("Fast path - replacing entire document"); self.modify(|contents, version| { contents.clone_from(text); *version = new_version; @@ -111,7 +142,7 @@ impl TextDocument { } in changes { if let Some(range) = range { - let range = range.to_text_range(&new_contents, &active_index, encoding); + let range = TextRange::from_proto(&range, &new_contents, &active_index, encoding); new_contents.replace_range( usize::from(range.start())..usize::from(range.end()), @@ -226,4 +257,58 @@ def interface(): "# ); } + + #[test] + fn test_document_position_encoding() { + // Replace `b` after `𐐀` which is at position 5 in UTF-8 + let utf8_range = lsp_types::Range { + start: lsp_types::Position { + line: 0, + character: 5, + }, + end: lsp_types::Position { + line: 0, + character: 6, + }, + }; + + // `b` is at position 3 in UTF-16 + let utf16_range = lsp_types::Range { + start: lsp_types::Position { + line: 0, + character: 3, + }, + end: lsp_types::Position { + line: 0, + character: 4, + }, + }; + + let utf8_replace_params = vec![lsp_types::TextDocumentContentChangeEvent { + range: Some(utf8_range), + range_length: None, + text: String::from("bar"), + }]; + let utf16_replace_params = vec![lsp_types::TextDocumentContentChangeEvent { + range: Some(utf16_range), + range_length: None, + text: String::from("bar"), + }]; + + let mut document = TextDocument::new("a𐐀b".into(), 1); + document.apply_changes( + utf8_replace_params, + document.version + 1, + PositionEncoding::UTF8, + ); + assert_eq!(document.contents(), "a𐐀bar"); + + let mut document = TextDocument::new("a𐐀b".into(), 1); + document.apply_changes( + utf16_replace_params, + document.version + 1, + PositionEncoding::UTF16, + ); + assert_eq!(document.contents, "a𐐀bar"); + } } diff --git a/crates/ruff_server/src/edit/text_edit.rs b/crates/ruff_server/src/edit/text_edit.rs new file mode 100644 index 00000000..65b76c35 --- /dev/null +++ b/crates/ruff_server/src/edit/text_edit.rs @@ -0,0 +1,338 @@ +// --- source +// authors = ["rust-analyzer team"] +// license = "MIT OR Apache-2.0" +// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/ide-db/src/text_edit.rs" +// --- + +//! Representation of a `TextEdit`. +//! +//! `rust-analyzer` never mutates text itself and only sends diffs to clients, +//! so `TextEdit` is the ultimate representation of the work done by +//! rust-analyzer. + +use biome_text_size::{TextRange, TextSize}; +use itertools::Itertools; +use std::cmp::max; + +/// `InsertDelete` -- a single "atomic" change to text +/// +/// Must not overlap with other `InDel`s +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Indel { + pub insert: String, + /// Refers to offsets in the original text + pub delete: TextRange, +} + +#[derive(Default, Debug, Clone)] +pub struct TextEdit { + /// Invariant: disjoint and sorted by `delete`. + indels: Vec, +} + +#[derive(Debug, Default, Clone)] +pub struct TextEditBuilder { + indels: Vec, +} + +impl Indel { + pub fn insert(offset: TextSize, text: String) -> Indel { + Indel::replace(TextRange::empty(offset), text) + } + pub fn delete(range: TextRange) -> Indel { + Indel::replace(range, String::new()) + } + pub fn replace(range: TextRange, replace_with: String) -> Indel { + Indel { + delete: range, + insert: replace_with, + } + } + + pub fn apply(&self, text: &mut String) { + let start: usize = self.delete.start().into(); + let end: usize = self.delete.end().into(); + text.replace_range(start..end, &self.insert); + } +} + +impl TextEdit { + pub fn builder() -> TextEditBuilder { + TextEditBuilder::default() + } + + pub fn insert(offset: TextSize, text: String) -> TextEdit { + let mut builder = TextEdit::builder(); + builder.insert(offset, text); + builder.finish() + } + + pub fn delete(range: TextRange) -> TextEdit { + let mut builder = TextEdit::builder(); + builder.delete(range); + builder.finish() + } + + pub fn replace(range: TextRange, replace_with: String) -> TextEdit { + let mut builder = TextEdit::builder(); + builder.replace(range, replace_with); + builder.finish() + } + + // --- Start Posit + pub fn diff(text: &str, replace_with: &str) -> TextEdit { + super::text_diff::text_diff(text, replace_with) + } + // --- End Posit + + pub fn len(&self) -> usize { + self.indels.len() + } + + pub fn is_empty(&self) -> bool { + self.indels.is_empty() + } + + pub fn iter(&self) -> std::slice::Iter<'_, Indel> { + self.into_iter() + } + + pub fn apply(&self, text: &mut String) { + match self.len() { + 0 => return, + 1 => { + self.indels[0].apply(text); + return; + } + _ => (), + } + + let text_size = TextSize::of(&*text); + let mut total_len = text_size; + let mut max_total_len = text_size; + for indel in &self.indels { + total_len += TextSize::of(&indel.insert); + total_len -= indel.delete.len(); + max_total_len = max(max_total_len, total_len); + } + + if let Some(additional) = max_total_len.checked_sub(text_size) { + text.reserve(additional.into()); + } + + for indel in self.indels.iter().rev() { + indel.apply(text); + } + + assert_eq!(TextSize::of(&*text), total_len); + } + + pub fn union(&mut self, other: TextEdit) -> Result<(), TextEdit> { + let iter_merge = self + .iter() + .merge_by(other.iter(), |l, r| l.delete.start() <= r.delete.start()); + if !check_disjoint(&mut iter_merge.clone()) { + return Err(other); + } + + // Only dedup deletions and replacements, keep all insertions + self.indels = iter_merge + .dedup_by(|a, b| a == b && !a.delete.is_empty()) + .cloned() + .collect(); + Ok(()) + } + + pub fn apply_to_offset(&self, offset: TextSize) -> Option { + let mut res = offset; + for indel in &self.indels { + if indel.delete.start() >= offset { + break; + } + if offset < indel.delete.end() { + return None; + } + res += TextSize::of(&indel.insert); + res -= indel.delete.len(); + } + Some(res) + } +} + +impl IntoIterator for TextEdit { + type Item = Indel; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.indels.into_iter() + } +} + +impl<'a> IntoIterator for &'a TextEdit { + type Item = &'a Indel; + type IntoIter = std::slice::Iter<'a, Indel>; + + fn into_iter(self) -> Self::IntoIter { + self.indels.iter() + } +} + +impl TextEditBuilder { + pub fn is_empty(&self) -> bool { + self.indels.is_empty() + } + pub fn replace(&mut self, range: TextRange, replace_with: String) { + self.indel(Indel::replace(range, replace_with)); + } + pub fn delete(&mut self, range: TextRange) { + self.indel(Indel::delete(range)); + } + pub fn insert(&mut self, offset: TextSize, text: String) { + self.indel(Indel::insert(offset, text)); + } + pub fn finish(self) -> TextEdit { + let mut indels = self.indels; + assert_disjoint_or_equal(&mut indels); + indels = coalesce_indels(indels); + TextEdit { indels } + } + pub fn invalidates_offset(&self, offset: TextSize) -> bool { + self.indels + .iter() + .any(|indel| indel.delete.contains_inclusive(offset)) + } + fn indel(&mut self, indel: Indel) { + self.indels.push(indel); + if self.indels.len() <= 16 { + assert_disjoint_or_equal(&mut self.indels); + } + } +} + +fn assert_disjoint_or_equal(indels: &mut [Indel]) { + assert!(check_disjoint_and_sort(indels)); +} + +fn check_disjoint_and_sort(indels: &mut [Indel]) -> bool { + indels.sort_by_key(|indel| (indel.delete.start(), indel.delete.end())); + check_disjoint(&mut indels.iter()) +} + +fn check_disjoint<'a, I>(indels: &mut I) -> bool +where + I: std::iter::Iterator + Clone, +{ + indels + .clone() + .zip(indels.skip(1)) + .all(|(l, r)| l.delete.end() <= r.delete.start() || l == r) +} + +fn coalesce_indels(indels: Vec) -> Vec { + indels + .into_iter() + .coalesce(|mut a, b| { + if a.delete.end() == b.delete.start() { + a.insert.push_str(&b.insert); + a.delete = TextRange::new(a.delete.start(), b.delete.end()); + Ok(a) + } else { + Err((a, b)) + } + }) + .collect_vec() +} + +#[cfg(test)] +mod tests { + use super::{TextEdit, TextEditBuilder, TextRange}; + + fn range(start: u32, end: u32) -> TextRange { + TextRange::new(start.into(), end.into()) + } + + #[test] + fn test_apply() { + let mut text = "_11h1_2222_xx3333_4444_6666".to_owned(); + let mut builder = TextEditBuilder::default(); + builder.replace(range(3, 4), "1".to_owned()); + builder.delete(range(11, 13)); + builder.insert(22.into(), "_5555".to_owned()); + + let text_edit = builder.finish(); + text_edit.apply(&mut text); + + assert_eq!(text, "_1111_2222_3333_4444_5555_6666") + } + + #[test] + fn test_union() { + let mut edit1 = TextEdit::delete(range(7, 11)); + let mut builder = TextEditBuilder::default(); + builder.delete(range(1, 5)); + builder.delete(range(13, 17)); + + let edit2 = builder.finish(); + assert!(edit1.union(edit2).is_ok()); + assert_eq!(edit1.indels.len(), 3); + } + + #[test] + fn test_union_with_duplicates() { + let mut builder1 = TextEditBuilder::default(); + builder1.delete(range(7, 11)); + builder1.delete(range(13, 17)); + + let mut builder2 = TextEditBuilder::default(); + builder2.delete(range(1, 5)); + builder2.delete(range(13, 17)); + + let mut edit1 = builder1.finish(); + let edit2 = builder2.finish(); + assert!(edit1.union(edit2).is_ok()); + assert_eq!(edit1.indels.len(), 3); + } + + #[test] + fn test_union_panics() { + let mut edit1 = TextEdit::delete(range(7, 11)); + let edit2 = TextEdit::delete(range(9, 13)); + assert!(edit1.union(edit2).is_err()); + } + + #[test] + fn test_coalesce_disjoint() { + let mut builder = TextEditBuilder::default(); + builder.replace(range(1, 3), "aa".into()); + builder.replace(range(5, 7), "bb".into()); + let edit = builder.finish(); + + assert_eq!(edit.indels.len(), 2); + } + + #[test] + fn test_coalesce_adjacent() { + let mut builder = TextEditBuilder::default(); + builder.replace(range(1, 3), "aa".into()); + builder.replace(range(3, 5), "bb".into()); + + let edit = builder.finish(); + assert_eq!(edit.indels.len(), 1); + assert_eq!(edit.indels[0].insert, "aabb"); + assert_eq!(edit.indels[0].delete, range(1, 5)); + } + + #[test] + fn test_coalesce_adjacent_series() { + let mut builder = TextEditBuilder::default(); + builder.replace(range(1, 3), "au".into()); + builder.replace(range(3, 5), "www".into()); + builder.replace(range(5, 8), String::new()); + builder.replace(range(8, 9), "ub".into()); + + let edit = builder.finish(); + assert_eq!(edit.indels.len(), 1); + assert_eq!(edit.indels[0].insert, "auwwwub"); + assert_eq!(edit.indels[0].delete, range(1, 9)); + } +} diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs deleted file mode 100644 index 80b2e862..00000000 --- a/crates/ruff_server/src/format.rs +++ /dev/null @@ -1,48 +0,0 @@ -use air_r_parser::RParserOptions; -use workspace::settings::FormatSettings; - -pub(crate) fn format( - source: &str, - formatter_settings: &FormatSettings, -) -> crate::Result> { - let parse = air_r_parser::parse(source, RParserOptions::default()); - - if parse.has_errors() { - return Err(anyhow::anyhow!("Can't format when there are parse errors.")); - } - - // Do we need to check that `doc` is indeed an R file? What about special - // files that don't have extensions like `NAMESPACE`, do we hard-code a - // list? What about unnamed temporary files? - - let format_options = formatter_settings.to_format_options(source); - let formatted = air_r_formatter::format_node(format_options, &parse.syntax())?; - let code = formatted.print()?.into_code(); - - Ok(Some(code)) -} - -// pub(crate) fn format_range( -// document: &TextDocument, -// formatter_settings: &FormatSettings, -// range: TextRange, -// ) -> crate::Result> { -// let format_options = formatter_settings.to_format_options(source_type, document.contents()); -// -// match ruff_python_formatter::format_range(document.contents(), range, format_options) { -// Ok(formatted) => { -// if formatted.as_code() == document.contents() { -// Ok(None) -// } else { -// Ok(Some(formatted)) -// } -// } -// // Special case - syntax/parse errors are handled here instead of -// // being propagated as visible server errors. -// Err(FormatModuleError::ParseError(error)) => { -// tracing::warn!("Unable to format document range: {error}"); -// Ok(None) -// } -// Err(err) => Err(err.into()), -// } -// } diff --git a/crates/ruff_server/src/lib.rs b/crates/ruff_server/src/lib.rs index 68a3e52f..1625033f 100644 --- a/crates/ruff_server/src/lib.rs +++ b/crates/ruff_server/src/lib.rs @@ -7,10 +7,13 @@ mod message; mod crates; mod edit; -mod format; mod logging; +mod proto; mod server; + mod session; +#[cfg(test)] +mod test; pub(crate) const SERVER_NAME: &str = "Air Language Server"; pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/ruff_server/src/logging.rs b/crates/ruff_server/src/logging.rs index 80c7c377..a9960676 100644 --- a/crates/ruff_server/src/logging.rs +++ b/crates/ruff_server/src/logging.rs @@ -45,7 +45,6 @@ use core::str; use lsp_server::Message; use lsp_types::notification::LogMessage; use lsp_types::notification::Notification; -use lsp_types::ClientInfo; use lsp_types::LogMessageParams; use lsp_types::MessageType; use serde::Deserialize; @@ -127,21 +126,30 @@ impl<'a> MakeWriter<'a> for LogWriterMaker { } } +/// We use a special `TestWriter` during tests to be compatible with `cargo test`'s +/// typical output capturing behavior. +/// +/// Important notes: +/// - `cargo test` swallows all logs unless you use `-- --nocapture`. +/// - Tests run in parallel, so logs can be interleaved unless you run `--test-threads 1`. +/// +/// We use `cargo test -- --nocapture --test-threads 1` on CI because of all of this. pub(crate) fn init_logging( client_tx: ClientSender, log_level: Option, dependency_log_levels: Option, - client_info: Option<&ClientInfo>, + client_name: Option, + is_test_client: bool, ) { let log_level = resolve_log_level(log_level); let dependency_log_levels = resolve_dependency_log_levels(dependency_log_levels); - let writer = if client_info.is_some_and(|client_info| { - client_info.name.starts_with("Zed") || client_info.name.starts_with("Visual Studio Code") + let writer = if client_name.is_some_and(|client_name| { + client_name.starts_with("Zed") || client_name.starts_with("Visual Studio Code") }) { // These IDEs are known to support `window/logMessage` well BoxMakeWriter::new(LogWriterMaker::new(client_tx)) - } else if is_test_client(client_info) { + } else if is_test_client { // Ensures a standard `cargo test` captures output unless `-- --nocapture` is used BoxMakeWriter::new(TestWriter::default()) } else { @@ -174,7 +182,7 @@ pub(crate) fn init_logging( let subscriber = tracing_subscriber::Registry::default().with(layer); - if is_test_client(client_info) { + if is_test_client { // During parallel testing, `set_global_default()` gets called multiple times // per process. That causes it to error, but we ignore this. tracing::subscriber::set_global_default(subscriber).ok(); @@ -186,18 +194,6 @@ pub(crate) fn init_logging( tracing::info!("Logging initialized with level: {log_level}"); } -/// We use a special `TestWriter` during tests to be compatible with `cargo test`'s -/// typical output capturing behavior. -/// -/// Important notes: -/// - `cargo test` swallows all logs unless you use `-- --nocapture`. -/// - Tests run in parallel, so logs can be interleaved unless you run `--test-threads 1`. -/// -/// We use `cargo test -- --nocapture --test-threads 1` on CI because of all of this. -fn is_test_client(client_info: Option<&ClientInfo>) -> bool { - client_info.map_or(false, |client_info| client_info.name == "AirTestClient") -} - fn log_filter(log_level: LogLevel, dependency_log_levels: Option) -> filter::Targets { // Initialize `filter` from dependency log levels. // If nothing is supplied, dependency logs are completely off. diff --git a/crates/ruff_server/src/message.rs b/crates/ruff_server/src/message.rs index ecd26ec8..49f7ac67 100644 --- a/crates/ruff_server/src/message.rs +++ b/crates/ruff_server/src/message.rs @@ -10,13 +10,18 @@ use std::sync::OnceLock; use crate::server::ClientSender; -// TODO: This won't work well with tests static MESSENGER: OnceLock = OnceLock::new(); -pub(crate) fn init_messenger(client_sender: ClientSender) { - MESSENGER - .set(client_sender) - .expect("Messenger should only be initialized once"); +pub(crate) fn init_messenger(client_sender: ClientSender, is_test_client: bool) { + let result = MESSENGER.set(client_sender); + + // During testing, `init_messenger()` will be called multiple times + // within the same process, potentially at the same time across threads. + // This probably isn't great, because if we call `show_err_msg!()` from a + // test thread where the `ClientSender` has been shutdown, then we will panic. + if !is_test_client { + result.expect("Messenger should only be initialized once"); + } } pub(crate) fn show_message(message: String, message_type: lsp_types::MessageType) { diff --git a/crates/ruff_server/src/proto.rs b/crates/ruff_server/src/proto.rs new file mode 100644 index 00000000..a53a4e14 --- /dev/null +++ b/crates/ruff_server/src/proto.rs @@ -0,0 +1,4 @@ +mod text_edit; +mod text_range; + +pub(crate) use text_range::TextRangeExt; diff --git a/crates/ruff_server/src/proto/text_edit.rs b/crates/ruff_server/src/proto/text_edit.rs new file mode 100644 index 00000000..711e92a2 --- /dev/null +++ b/crates/ruff_server/src/proto/text_edit.rs @@ -0,0 +1,39 @@ +use ruff_source_file::LineEnding; +use ruff_source_file::LineIndex; + +use crate::edit::Indel; +use crate::edit::PositionEncoding; +use crate::edit::TextEdit; +use crate::proto::TextRangeExt; + +impl TextEdit { + pub(crate) fn into_proto( + self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ending: LineEnding, + ) -> anyhow::Result> { + self.into_iter() + .map(|indel| indel.into_proto(text, index, encoding, ending)) + .collect() + } +} + +impl Indel { + fn into_proto( + self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ending: LineEnding, + ) -> anyhow::Result { + let range = self.delete.into_proto(text, index, encoding); + let new_text = match ending { + LineEnding::Lf => self.insert, + LineEnding::Crlf => self.insert.replace('\n', "\r\n"), + LineEnding::Cr => self.insert.replace('\n', "\r"), + }; + Ok(lsp_types::TextEdit { range, new_text }) + } +} diff --git a/crates/ruff_server/src/edit/range.rs b/crates/ruff_server/src/proto/text_range.rs similarity index 79% rename from crates/ruff_server/src/edit/range.rs rename to crates/ruff_server/src/proto/text_range.rs index 89649fd1..47436c42 100644 --- a/crates/ruff_server/src/edit/range.rs +++ b/crates/ruff_server/src/proto/text_range.rs @@ -4,61 +4,77 @@ // | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | // +------------------------------------------------------------+ -use super::PositionEncoding; +use crate::edit::PositionEncoding; use biome_text_size::{TextRange, TextSize}; use lsp_types as types; use ruff_source_file::OneIndexed; use ruff_source_file::{LineIndex, SourceLocation}; -pub(crate) trait RangeExt { - fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) - -> TextRange; -} +// We don't own this type so we need a helper trait +pub(crate) trait TextRangeExt { + fn into_proto(self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range; -pub(crate) trait ToRangeExt { - fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range; + fn from_proto( + range: &lsp_types::Range, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> Self; } -fn u32_index_to_usize(index: u32) -> usize { - usize::try_from(index).expect("u32 fits in usize") -} +impl TextRangeExt for TextRange { + fn into_proto(self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range { + types::Range { + start: source_location_to_position(&offset_to_source_location( + self.start(), + text, + index, + encoding, + )), + end: source_location_to_position(&offset_to_source_location( + self.end(), + text, + index, + encoding, + )), + } + } -impl RangeExt for lsp_types::Range { - fn to_text_range( - &self, + fn from_proto( + range: &lsp_types::Range, text: &str, index: &LineIndex, encoding: PositionEncoding, - ) -> TextRange { + ) -> Self { let start_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), + OneIndexed::from_zero_indexed(u32_index_to_usize(range.start.line)), text, ); let end_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), + OneIndexed::from_zero_indexed(u32_index_to_usize(range.end.line)), text, ); let (start_column_offset, end_column_offset) = match encoding { PositionEncoding::UTF8 => ( - TextSize::from(self.start.character), - TextSize::from(self.end.character), + TextSize::from(range.start.character), + TextSize::from(range.end.character), ), PositionEncoding::UTF16 => { // Fast path for ASCII only documents if index.is_ascii() { ( - TextSize::from(self.start.character), - TextSize::from(self.end.character), + TextSize::from(range.start.character), + TextSize::from(range.end.character), ) } else { // UTF16 encodes characters either as one or two 16 bit words. // The position in `range` is the 16-bit word offset from the start of the line (and not the character offset) // UTF-16 with a text that may use variable-length characters. ( - utf8_column_offset(self.start.character, &text[start_line]), - utf8_column_offset(self.end.character, &text[end_line]), + utf8_column_offset(range.start.character, &text[start_line]), + utf8_column_offset(range.end.character, &text[end_line]), ) } } @@ -66,13 +82,13 @@ impl RangeExt for lsp_types::Range { // UTF-32 uses 4 bytes for each character. Meaning, the position in range is a character offset. return TextRange::new( index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(self.start.character)), + OneIndexed::from_zero_indexed(u32_index_to_usize(range.start.line)), + OneIndexed::from_zero_indexed(u32_index_to_usize(range.start.character)), text, ), index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(self.end.character)), + OneIndexed::from_zero_indexed(u32_index_to_usize(range.end.line)), + OneIndexed::from_zero_indexed(u32_index_to_usize(range.end.character)), text, ), ); @@ -86,23 +102,8 @@ impl RangeExt for lsp_types::Range { } } -impl ToRangeExt for TextRange { - fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range { - types::Range { - start: source_location_to_position(&offset_to_source_location( - self.start(), - text, - index, - encoding, - )), - end: source_location_to_position(&offset_to_source_location( - self.end(), - text, - index, - encoding, - )), - } - } +fn u32_index_to_usize(index: u32) -> usize { + usize::try_from(index).expect("u32 fits in usize") } /// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index 359901fd..b845ead7 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -22,11 +22,12 @@ use types::TextDocumentSyncOptions; use types::WorkspaceFoldersServerCapabilities; use self::connection::Connection; -use self::connection::ConnectionInitializer; use self::schedule::event_loop_thread; use self::schedule::Scheduler; use self::schedule::Task; use crate::edit::PositionEncoding; +use crate::message::try_show_message; +use crate::server::connection::ConnectionInitializer; use crate::session::ResolvedClientCapabilities; use crate::session::Session; @@ -35,7 +36,6 @@ mod client; mod connection; mod schedule; -use crate::message::try_show_message; pub(crate) use connection::ClientSender; pub(crate) type Result = std::result::Result; @@ -48,17 +48,21 @@ pub struct Server { } impl Server { - pub fn new(worker_threads: NonZeroUsize) -> crate::Result { - let connection = ConnectionInitializer::stdio(); + pub fn new( + worker_threads: NonZeroUsize, + connection: lsp::Connection, + connection_threads: Option, + ) -> crate::Result { + let initializer = ConnectionInitializer::new(connection, connection_threads); - let (id, initialize_params) = connection.initialize_start()?; + let (id, initialize_params) = initializer.initialize_start()?; let client_capabilities = initialize_params.capabilities; let client_capabilities = ResolvedClientCapabilities::new(client_capabilities); let position_encoding = Self::find_best_position_encoding(&client_capabilities); let server_capabilities = Self::server_capabilities(position_encoding); - let connection = connection.initialize_finish( + let connection = initializer.initialize_finish( id, &server_capabilities, crate::SERVER_NAME, @@ -73,6 +77,14 @@ impl Server { let workspace_folders = workspace_folders.unwrap_or_default(); + let client_name = client_info + .as_ref() + .map(|client_info| client_info.name.clone()); + + let is_test_client = client_name + .as_ref() + .map_or(false, |client_name| client_name == "AirTestClient"); + // TODO: Get user specified options from `initialization_options` let log_level = None; let dependency_log_levels = None; @@ -81,10 +93,11 @@ impl Server { connection.make_sender(), log_level, dependency_log_levels, - client_info.as_ref(), + client_name, + is_test_client, ); - crate::message::init_messenger(connection.make_sender()); + crate::message::init_messenger(connection.make_sender(), is_test_client); Ok(Self { connection, diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index d44a038b..0b9c0aa5 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -4,12 +4,13 @@ // | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | // +------------------------------------------------------------+ +use air_r_parser::RParserOptions; +use biome_formatter::LineEnding; use lsp_types::{self as types, request as req}; -use types::TextEdit; +use workspace::settings::FormatSettings; -use ruff_source_file::LineIndex; - -use crate::edit::{PositionEncoding, Replacement, TextDocument, ToRangeExt}; +use crate::edit::TextEdit; +use crate::edit::{PositionEncoding, TextDocument}; use crate::server::api::LSPResult; use crate::server::{client::Notifier, Result}; use crate::session::{DocumentQuery, DocumentSnapshot}; @@ -31,8 +32,7 @@ impl super::BackgroundDocumentRequestHandler for Format { } } -/// Formats either a full text document or an specific notebook cell. If the query within the snapshot is a notebook document -/// with no selected cell, this will throw an error. +/// Formats a full text document pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result { let text_document = snapshot.query().as_single_document(); let query = snapshot.query(); @@ -47,29 +47,97 @@ fn format_text_document( let document_settings = query.settings(); let formatter_settings = &document_settings.format; - let source = text_document.contents(); + let text = text_document.contents(); + let index = text_document.index(); + let ending = text_document.ending(); - let formatted = crate::format::format(source, formatter_settings) + let new_text = format_source(text, formatter_settings) .with_failure_code(lsp_server::ErrorCode::InternalError)?; - let Some(formatted) = formatted else { + + let Some(new_text) = new_text else { return Ok(None); }; - let unformatted_index = text_document.index(); - let formatted_index: LineIndex = LineIndex::from_source_text(&formatted); - - let Replacement { - source_range, - modified_range: formatted_range, - } = Replacement::between( - source, - unformatted_index.line_starts(), - &formatted, - formatted_index.line_starts(), - ); - - Ok(Some(vec![TextEdit { - range: source_range.to_range(source, unformatted_index, encoding), - new_text: formatted[formatted_range].to_owned(), - }])) + let text_edit = TextEdit::diff(text, &new_text); + + let edits = text_edit + .into_proto(text, index, encoding, ending) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + + Ok(Some(edits)) +} + +fn format_source( + source: &str, + formatter_settings: &FormatSettings, +) -> crate::Result> { + let parse = air_r_parser::parse(source, RParserOptions::default()); + + if parse.has_errors() { + return Err(anyhow::anyhow!("Can't format when there are parse errors.")); + } + + // Do we need to check that `doc` is indeed an R file? What about special + // files that don't have extensions like `NAMESPACE`, do we hard-code a + // list? What about unnamed temporary files? + + // Always use `Lf` line endings on the way out from the formatter since we + // internally store all LSP text documents with `Lf` endings + let format_options = formatter_settings + .to_format_options(source) + .with_line_ending(LineEnding::Lf); + + let formatted = air_r_formatter::format_node(format_options, &parse.syntax())?; + let code = formatted.print()?.into_code(); + + Ok(Some(code)) +} + +#[cfg(test)] +mod tests { + use crate::edit::TextDocument; + use crate::{test::init_test_client, test::TestClientExt}; + + #[test] + fn test_format() { + let mut client = init_test_client(); + + #[rustfmt::skip] + let doc = TextDocument::doodle( +" +1 +2+2 +3 + 3 + +3", + ); + + let formatted = client.format_document(&doc); + insta::assert_snapshot!(formatted); + + client.shutdown(); + client.exit(); + } + + // https://github.com/posit-dev/air/issues/61 + #[test] + fn test_format_minimal_diff() { + let mut client = init_test_client(); + + #[rustfmt::skip] + let doc = TextDocument::doodle( +"1 +2+2 +3 +", + ); + + let edits = client.format_document_edits(&doc).unwrap(); + assert!(edits.len() == 1); + + let edit = &edits[0]; + assert_eq!(edit.new_text, " + "); + + client.shutdown(); + client.exit(); + } } diff --git a/crates/ruff_server/src/server/api/requests/format_range.rs b/crates/ruff_server/src/server/api/requests/format_range.rs index 7b5ebda6..fae7a5f1 100644 --- a/crates/ruff_server/src/server/api/requests/format_range.rs +++ b/crates/ruff_server/src/server/api/requests/format_range.rs @@ -4,9 +4,23 @@ // | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | // +------------------------------------------------------------+ +use air_r_parser::RParserOptions; +use air_r_syntax::RExpressionList; +use air_r_syntax::RSyntaxKind; +use air_r_syntax::RSyntaxNode; +use biome_formatter::LineEnding; +use biome_rowan::AstNode; +use biome_rowan::Language; +use biome_rowan::SyntaxElement; +use biome_rowan::WalkEvent; +use biome_text_size::{TextRange, TextSize}; use lsp_types::{self as types, request as req, Range}; +use workspace::settings::FormatSettings; +use crate::edit::TextEdit; use crate::edit::{PositionEncoding, TextDocument}; +use crate::proto::TextRangeExt; +use crate::server::api::LSPResult; use crate::server::{client::Notifier, Result}; use crate::session::{DocumentQuery, DocumentSnapshot}; @@ -28,6 +42,7 @@ impl super::BackgroundDocumentRequestHandler for FormatRange { } /// Formats the specified [`Range`] in the [`DocumentSnapshot`]. +#[tracing::instrument(level = "info", skip_all)] fn format_document_range( snapshot: &DocumentSnapshot, range: Range, @@ -39,33 +54,463 @@ fn format_document_range( /// Formats the specified [`Range`] in the [`TextDocument`]. fn format_text_document_range( - _text_document: &TextDocument, - _range: Range, - _query: &DocumentQuery, - _encoding: PositionEncoding, + text_document: &TextDocument, + range: Range, + query: &DocumentQuery, + encoding: PositionEncoding, ) -> Result { - Ok(None) - // let document_settings = query.settings(); - // let formatter_settings = &document_settings.format; - // - // let text = text_document.contents(); - // let index = text_document.index(); - // let range = range.to_text_range(text, index, encoding); + let document_settings = query.settings(); + let formatter_settings = &document_settings.format; + + let text = text_document.contents(); + let ending = text_document.ending(); + let index = text_document.index(); + let range = TextRange::from_proto(&range, text, index, encoding); + + let Some((new_text, new_range)) = format_source_range(text, formatter_settings, range) + .with_failure_code(lsp_server::ErrorCode::InternalError)? + else { + return Ok(None); + }; + + let text_edit = TextEdit::replace(new_range, new_text); + + let edits = text_edit + .into_proto(text, index, encoding, ending) + .with_failure_code(lsp_server::ErrorCode::InternalError)?; + + Ok(Some(edits)) +} + +fn format_source_range( + source: &str, + formatter_settings: &FormatSettings, + range: TextRange, +) -> crate::Result> { + let parse = air_r_parser::parse(source, RParserOptions::default()); + + if parse.has_errors() { + return Err(anyhow::anyhow!("Can't format when there are parse errors.")); + } + + // Always use `Lf` line endings on the way out from the formatter since we + // internally store all LSP text documents with `Lf` endings + let format_options = formatter_settings + .to_format_options(source) + .with_line_ending(LineEnding::Lf); + + let logical_lines = find_deepest_enclosing_logical_lines(parse.syntax(), range); + if logical_lines.is_empty() { + tracing::warn!("Can't find logical line"); + return Ok(None); + }; + + // Find the overall formatting range by concatenating the ranges of the logical lines. + // We use the "non-whitespace-range" as that corresponds to what Biome will format. + let new_range = logical_lines + .iter() + .map(text_non_whitespace_range) + .reduce(|acc, new| acc.cover(new)) + .expect("`logical_lines` is non-empty"); + + // We need to wrap in an `RRoot` otherwise the comments get attached too + // deep in the tree. See `CommentsBuilderVisitor` in biome_formatter and the + // `is_root` logic. Note that `node` needs to be wrapped in at least two + // other nodes in order to fix this problem, and here we have an `RRoot` and + // `RExpressionList` that do the job. // - // let formatted_range = crate::format::format_range( - // text_document, - // query.source_type(), - // formatter_settings, - // range, - // ) - // .with_failure_code(lsp_server::ErrorCode::InternalError)?; + // Since we only format logical lines, it is fine to wrap in an expression list. + let Some(exprs): Option> = logical_lines + .into_iter() + .map(air_r_syntax::AnyRExpression::cast) + .collect() + else { + tracing::warn!("Can't cast to `AnyRExpression`"); + return Ok(None); + }; + + let list = air_r_factory::r_expression_list(exprs); + let eof = air_r_syntax::RSyntaxToken::new_detached(RSyntaxKind::EOF, "", vec![], vec![]); + let root = air_r_factory::r_root(list, eof).build(); + + let printed = biome_formatter::format_sub_tree( + root.syntax(), + air_r_formatter::RFormatLanguage::new(format_options), + )?; + + if printed.range().is_none() { + // Happens in edge cases when biome returns a `Printed::new_empty()` + return Ok(None); + }; + + let mut new_text = printed.into_code(); + + // Remove last hard break line from our artifical expression list + new_text.pop(); + + Ok(Some((new_text, new_range))) +} + +// From biome_formatter +fn text_non_whitespace_range(elem: &E) -> TextRange +where + E: Into> + Clone, + L: Language, +{ + let elem: SyntaxElement = elem.clone().into(); + + let start = elem + .leading_trivia() + .into_iter() + .flat_map(|trivia| trivia.pieces()) + .find_map(|piece| { + if piece.is_whitespace() || piece.is_newline() { + None + } else { + Some(piece.text_range().start()) + } + }) + .unwrap_or_else(|| elem.text_trimmed_range().start()); + + let end = elem + .trailing_trivia() + .into_iter() + .flat_map(|trivia| trivia.pieces().rev()) + .find_map(|piece| { + if piece.is_whitespace() || piece.is_newline() { + None + } else { + Some(piece.text_range().end()) + } + }) + .unwrap_or_else(|| elem.text_trimmed_range().end()); + + TextRange::new(start, end) +} + +/// Finds consecutive logical lines. Currently that's only expressions at +/// top-level or in a braced list. +fn find_deepest_enclosing_logical_lines(node: RSyntaxNode, range: TextRange) -> Vec { + let start_lists = find_expression_lists(&node, range.start(), false); + let end_lists = find_expression_lists(&node, range.end(), true); + + // Both vectors of lists should have a common prefix, starting from the + // program's expression list. As soon as the lists diverge we stop. + let Some(list) = start_lists + .into_iter() + .zip(end_lists) + .take_while(|pair| pair.0 == pair.1) + .map(|pair| pair.0) + .last() + else { + // Should not happen as the range is always included in the program's expression list + tracing::warn!("Can't find common list parent"); + return vec![]; + }; + + let Some(list) = RExpressionList::cast(list) else { + tracing::warn!("Can't cast to expression list"); + return vec![]; + }; + + let iter = list.into_iter(); + + // We've chosen to be liberal about user selections and always widen the + // range to include the selection bounds. If we wanted to be conservative + // instead, we could use this `filter()` instead of the `skip_while()` and + // `take_while()`: // - // Ok(formatted_range.map(|formatted_range| { - // vec![types::TextEdit { - // range: formatted_range - // .source_range() - // .to_range(text, index, encoding), - // new_text: formatted_range.into_code(), - // }] - // })) + // ```rust + // .filter(|node| range.contains_range(node.text_trimmed_range())) + // ``` + let logical_lines: Vec = iter + .map(|expr| expr.into_syntax()) + .skip_while(|node| !node.text_range().contains(range.start())) + .take_while(|node| node.text_trimmed_range().start() <= range.end()) + .collect(); + + logical_lines +} + +fn find_expression_lists(node: &RSyntaxNode, offset: TextSize, end: bool) -> Vec { + let mut preorder = node.preorder(); + let mut nodes: Vec = vec![]; + + while let Some(event) = preorder.next() { + match event { + WalkEvent::Enter(node) => { + let Some(parent) = node.parent() else { + continue; + }; + + let is_contained = if end { + let trimmed_node_range = node.text_trimmed_range(); + trimmed_node_range.contains_inclusive(offset) + } else { + let node_range = node.text_range(); + node_range.contains(offset) + }; + + if !is_contained { + preorder.skip_subtree(); + continue; + } + + if parent.kind() == RSyntaxKind::R_EXPRESSION_LIST { + nodes.push(parent.clone()); + continue; + } + } + + WalkEvent::Leave(_) => {} + } + } + + nodes +} + +#[cfg(test)] +mod tests { + use crate::edit::TextDocument; + use crate::{test::init_test_client, test::TestClientExt}; + + #[test] + fn test_format_range_none() { + let mut client = init_test_client(); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"<<>>", + ); + + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"<< +>>", + ); + + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"<<1 +>>", + ); + + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + + client.shutdown(); + client.exit(); + } + + #[test] + fn test_format_range_logical_lines() { + let mut client = init_test_client(); + + // 2+2 is the logical line to format + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"1+1 +<<2+2>> +", + ); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"1+1 +# +<<2+2>> +", + ); + + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + + // The element in the braced expression is a logical line + // FIXME: Should this be the whole `{2+2}` instead? + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"1+1 +{<<2+2>>} +", + ); + + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"1+1 +<<{2+2}>> +", + ); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + + // The deepest element in the braced expression is our target + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"1+1 +{ + 2+2 + { + <<3+3>> + } +} +", + ); + + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + + client.shutdown(); + client.exit(); + } + + #[test] + fn test_format_range_mismatched_indent() { + let mut client = init_test_client(); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"1 + <<2+2>> +", + ); + + // We don't change indentation when `2+2` is formatted + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + + // Debatable: Should we make an effort to remove unneeded indentation + // when it's part of the range? + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"1 +<< 2+2>> +", + ); + let output_wide = client.format_document_range(&doc, range); + assert_eq!(output, output_wide); + + client.shutdown(); + client.exit(); + } + + #[test] + fn test_format_range_multiple_lines() { + let mut client = init_test_client(); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"1+1 +<<# +2+2>> +", + ); + + let output1 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output1); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"<<1+1 +# +2+2>> +", + ); + let output2 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output2); + + client.shutdown(); + client.exit(); + } + + #[test] + fn test_format_range_unmatched_lists() { + let mut client = init_test_client(); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"0+0 +<<1+1 +{ + 2+2>> +} +3+3 +", + ); + + let output1 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output1); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"0+0 +<<1+1 +{ +>> 2+2 +} +3+3 +", + ); + let output2 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output2); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"0+0 +<<1+1 +{ + 2+2 +} +>>3+3 +", + ); + let output3 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output3); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"0+0 +1+1 +{ +<< 2+2 +} +>>3+3 +", + ); + let output4 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output4); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"<<1+1>> +2+2 +", + ); + + let output5 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output5); + + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( +"1+1 +<<2+2>> +", + ); + + let output6 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output6); + + client.shutdown(); + client.exit(); + } } diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format__tests__format.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format__tests__format.snap new file mode 100644 index 00000000..f73985a1 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format__tests__format.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff_server/src/server/api/requests/format.rs +expression: formatted +--- +1 +2 + 2 +3 + 3 + 3 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap new file mode 100644 index 00000000..b59f3d58 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +# +2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap new file mode 100644 index 00000000..b32ccf9c --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +{2 + 2} diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap new file mode 100644 index 00000000..ee693476 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap @@ -0,0 +1,8 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +{ + 2 + 2 +} diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap new file mode 100644 index 00000000..e2057f2a --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap @@ -0,0 +1,11 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +{ + 2+2 + { + 3 + 3 + } +} diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines.snap new file mode 100644 index 00000000..c5a4d369 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap new file mode 100644 index 00000000..aacf6963 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output +--- +1 + 2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap new file mode 100644 index 00000000..292171c8 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output2 +--- +1 + 1 +# +2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines.snap new file mode 100644 index 00000000..6f04197c --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines.snap @@ -0,0 +1,7 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output1 +--- +1+1 +# +2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-2.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-2.snap new file mode 100644 index 00000000..ae085c9a --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output +--- + diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-3.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-3.snap new file mode 100644 index 00000000..f9b29214 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-3.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output +--- +1 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none.snap new file mode 100644 index 00000000..ae085c9a --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output +--- + diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap new file mode 100644 index 00000000..1f7eb677 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output2 +--- +0+0 +1 + 1 +{ + 2 + 2 +} +3+3 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap new file mode 100644 index 00000000..846ca65c --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output3 +--- +0+0 +1 + 1 +{ + 2 + 2 +} +3 + 3 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap new file mode 100644 index 00000000..7a3a5c46 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output4 +--- +0+0 +1+1 +{ + 2 + 2 +} +3 + 3 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap new file mode 100644 index 00000000..c896564b --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output5 +--- +1 + 1 +2+2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap new file mode 100644 index 00000000..cfca8f65 --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap @@ -0,0 +1,6 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output6 +--- +1+1 +2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap new file mode 100644 index 00000000..43ee58db --- /dev/null +++ b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap @@ -0,0 +1,10 @@ +--- +source: crates/ruff_server/src/server/api/requests/format_range.rs +expression: output1 +--- +0+0 +1 + 1 +{ + 2 + 2 +} +3+3 diff --git a/crates/ruff_server/src/server/connection.rs b/crates/ruff_server/src/server/connection.rs index aa62de23..29fd737a 100644 --- a/crates/ruff_server/src/server/connection.rs +++ b/crates/ruff_server/src/server/connection.rs @@ -12,22 +12,20 @@ type ConnectionSender = crossbeam::channel::Sender; type ConnectionReceiver = crossbeam::channel::Receiver; /// A builder for `Connection` that handles LSP initialization. -pub(crate) struct ConnectionInitializer { +pub(super) struct ConnectionInitializer { connection: lsp::Connection, - threads: lsp::IoThreads, + threads: Option, } /// Handles inbound and outbound messages with the client. pub(crate) struct Connection { sender: Arc, receiver: ConnectionReceiver, - threads: lsp::IoThreads, + threads: Option, } impl ConnectionInitializer { - /// Create a new LSP server connection over stdin/stdout. - pub(super) fn stdio() -> Self { - let (connection, threads) = lsp::Connection::stdio(); + pub(super) fn new(connection: lsp::Connection, threads: Option) -> Self { Self { connection, threads, @@ -125,7 +123,9 @@ impl Connection { .expect("the client sender shouldn't have more than one strong reference"), ); std::mem::drop(self.receiver); - self.threads.join()?; + if let Some(threads) = self.threads { + threads.join()?; + }; Ok(()) } } diff --git a/crates/ruff_server/src/test.rs b/crates/ruff_server/src/test.rs new file mode 100644 index 00000000..d240fc6b --- /dev/null +++ b/crates/ruff_server/src/test.rs @@ -0,0 +1,7 @@ +mod client; +mod client_ext; +mod utils; + +pub(crate) use client::init_test_client; +pub(crate) use client_ext::TestClientExt; +pub(crate) use utils::extract_marked_range; diff --git a/crates/ruff_server/src/test/client.rs b/crates/ruff_server/src/test/client.rs new file mode 100644 index 00000000..d1b13b18 --- /dev/null +++ b/crates/ruff_server/src/test/client.rs @@ -0,0 +1,78 @@ +use crate::Server; + +pub(crate) fn init_test_client() -> server_test::TestClient { + let mut client = start_test_client(); + + let id = client.initialize(); + let response = client.recv_response(); + assert_eq!(id, response.id); + client.initialized(); + + client +} + +fn start_test_client() -> server_test::TestClient { + server_test::TestClient::new(|worker_threads, connection, connection_threads| { + let server = Server::new(worker_threads, connection, connection_threads).unwrap(); + server.run().unwrap(); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use lsp_types::PositionEncodingKind; + use lsp_types::ServerCapabilities; + use lsp_types::ServerInfo; + use lsp_types::TextDocumentSyncCapability; + use lsp_types::TextDocumentSyncKind; + use lsp_types::TextDocumentSyncOptions; + + #[test] + fn test_init() { + let mut client = start_test_client(); + + let id = client.initialize(); + + let value = client.recv_response(); + assert_eq!(id, value.id); + let value: lsp_types::InitializeResult = + serde_json::from_value(value.result.unwrap().clone()).unwrap(); + + client.initialized(); + + assert_matches!( + value, + lsp_types::InitializeResult { + capabilities, + server_info + } => { + assert_matches!(capabilities, ServerCapabilities { + position_encoding, + text_document_sync, + .. + } => { + assert_eq!(position_encoding, Some(PositionEncodingKind::UTF8)); + assert_eq!(text_document_sync, Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + will_save: Some(false), + will_save_wait_until: Some(false), + ..Default::default() + }, + ))); + }); + + assert_matches!(server_info, Some(ServerInfo { name, version }) => { + assert!(name.contains("Air Language Server")); + assert!(version.is_some()); + }); + } + ); + + client.shutdown(); + client.exit(); + } +} diff --git a/crates/ruff_server/src/test/client_ext.rs b/crates/ruff_server/src/test/client_ext.rs new file mode 100644 index 00000000..a2c55502 --- /dev/null +++ b/crates/ruff_server/src/test/client_ext.rs @@ -0,0 +1,152 @@ +use biome_text_size::TextRange; +use server_test::TestClient; + +use crate::edit::PositionEncoding; +use crate::edit::TextDocument; +use crate::proto::TextRangeExt; + +pub(crate) trait TestClientExt { + fn open_document(&mut self, doc: &TextDocument) -> lsp_types::TextDocumentItem; + + fn format_document(&mut self, doc: &TextDocument) -> String; + fn format_document_range(&mut self, doc: &TextDocument, range: TextRange) -> String; + fn format_document_edits(&mut self, doc: &TextDocument) -> Option>; + fn format_document_range_edits( + &mut self, + doc: &TextDocument, + range: TextRange, + ) -> Option>; + + fn position_encoding(&self) -> PositionEncoding; +} + +impl TestClientExt for TestClient { + fn open_document(&mut self, doc: &TextDocument) -> lsp_types::TextDocumentItem { + let path = format!("test://{}", uuid::Uuid::new_v4()); + let uri = url::Url::parse(&path).unwrap(); + + let text_document = lsp_types::TextDocumentItem { + uri, + language_id: String::from("r"), + version: 0, + text: doc.contents().to_string(), + }; + + let params = lsp_types::DidOpenTextDocumentParams { + text_document: text_document.clone(), + }; + self.did_open_text_document(params); + + text_document + } + + fn format_document(&mut self, doc: &TextDocument) -> String { + let edits = self.format_document_edits(doc).unwrap(); + apply_text_edits(edits, doc, self.position_encoding()).unwrap() + } + + fn format_document_range(&mut self, doc: &TextDocument, range: TextRange) -> String { + let Some(edits) = self.format_document_range_edits(doc, range) else { + return doc.contents().to_string(); + }; + apply_text_edits(edits, doc, self.position_encoding()).unwrap() + } + + fn format_document_edits(&mut self, doc: &TextDocument) -> Option> { + let lsp_doc = self.open_document(doc); + + let options = lsp_types::FormattingOptions { + tab_size: 4, + insert_spaces: false, + ..Default::default() + }; + + self.formatting(lsp_types::DocumentFormattingParams { + text_document: lsp_types::TextDocumentIdentifier { + uri: lsp_doc.uri.clone(), + }, + options, + work_done_progress_params: Default::default(), + }); + + let response = self.recv_response(); + + if let Some(err) = response.error { + panic!("Unexpected error: {}", err.message); + }; + + let value: Option> = + serde_json::from_value(response.result.unwrap().clone()).unwrap(); + + self.close_document(lsp_doc.uri); + + value + } + + fn format_document_range_edits( + &mut self, + doc: &TextDocument, + range: TextRange, + ) -> Option> { + let lsp_doc = self.open_document(doc); + + let options = lsp_types::FormattingOptions { + tab_size: 4, + insert_spaces: false, + ..Default::default() + }; + + let range = range.into_proto(doc.contents(), doc.index(), self.position_encoding()); + + self.range_formatting(lsp_types::DocumentRangeFormattingParams { + text_document: lsp_types::TextDocumentIdentifier { + uri: lsp_doc.uri.clone(), + }, + range, + options, + work_done_progress_params: Default::default(), + }); + + let response = self.recv_response(); + + if let Some(err) = response.error { + panic!("Unexpected error: {}", err.message); + }; + + let value: Option> = + serde_json::from_value(response.result.unwrap().clone()).unwrap(); + + self.close_document(lsp_doc.uri); + + value + } + + fn position_encoding(&self) -> PositionEncoding { + self.encoding().try_into().unwrap() + } +} + +fn apply_text_edits( + mut edits: Vec, + doc: &crate::edit::TextDocument, + encoding: crate::edit::PositionEncoding, +) -> anyhow::Result { + use std::ops::Range; + + let text = doc.contents(); + let mut new_text = text.to_string(); + + let index = doc.index(); + + // Apply edits from bottom to top to avoid inserted newlines to invalidate + // positions in earlier parts of the doc (they are sent in reading order + // accorder to the LSP protocol) + edits.reverse(); + + for edit in edits { + let range: Range = TextRange::from_proto(&edit.range, text, index, encoding).into(); + new_text.replace_range(range, &edit.new_text); + } + + Ok(new_text) +} diff --git a/crates/ruff_server/src/test/utils.rs b/crates/ruff_server/src/test/utils.rs new file mode 100644 index 00000000..fe60c0c9 --- /dev/null +++ b/crates/ruff_server/src/test/utils.rs @@ -0,0 +1,27 @@ +use biome_text_size::{TextRange, TextSize}; + +pub(crate) fn extract_marked_range(input: &str) -> (String, TextRange) { + let mut output = String::new(); + let mut start = None; + let mut end = None; + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '<' && chars.peek() == Some(&'<') { + chars.next(); + start = Some(TextSize::from(output.len() as u32)); + } else if c == '>' && chars.peek() == Some(&'>') { + chars.next(); + end = Some(TextSize::from(output.len() as u32)); + } else { + output.push(c); + } + } + + let range = match (start, end) { + (Some(start), Some(end)) => TextRange::new(start, end), + _ => panic!("Missing range markers"), + }; + + (output, range) +} diff --git a/crates/ruff_source_file/src/lib.rs b/crates/ruff_source_file/src/lib.rs index c36dbcd0..92ef04e1 100644 --- a/crates/ruff_source_file/src/lib.rs +++ b/crates/ruff_source_file/src/lib.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; pub use crate::line_index::LineIndex; -pub use crate::newlines::{find_newline, normalize_crlf_newlines, LineEnding}; +pub use crate::newlines::{find_newline, infer_line_ending, normalize_newlines, LineEnding}; pub use crate::one_indexed::OneIndexed; pub use crate::source_location::SourceLocation; diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/ruff_source_file/src/line_index.rs index 5fd219fd..d85568c6 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/ruff_source_file/src/line_index.rs @@ -236,16 +236,16 @@ impl LineIndex { /// let index = LineIndex::from_source_text(source); /// /// // First line, first column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::new(0)); + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::from(0)); /// /// // Second line, 4th column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(4), source), TextSize::new(10)); + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(4), source), TextSize::from(10)); /// /// // Offset past the end of the first line - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(10), source), TextSize::new(6)); + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(10), source), TextSize::from(6)); /// /// // Offset past the end of the file - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::new(29)); + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::from(29)); /// ``` /// /// ### UTF8 @@ -260,16 +260,16 @@ impl LineIndex { /// let index = LineIndex::from_source_text(source); /// /// // First line, first column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::new(0)); + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::from(0)); /// /// // Third line, 2nd column, after emoji - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(2), OneIndexed::from_zero_indexed(1), source), TextSize::new(20)); + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(2), OneIndexed::from_zero_indexed(1), source), TextSize::from(20)); /// /// // Offset past the end of the second line - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(10), source), TextSize::new(19)); + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(10), source), TextSize::from(19)); /// /// // Offset past the end of the file - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::new(24)); + /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::from(24)); /// ``` /// pub fn offset(&self, line: OneIndexed, column: OneIndexed, contents: &str) -> TextSize { diff --git a/crates/ruff_source_file/src/newlines.rs b/crates/ruff_source_file/src/newlines.rs index 05295af0..23de2e98 100644 --- a/crates/ruff_source_file/src/newlines.rs +++ b/crates/ruff_source_file/src/newlines.rs @@ -4,6 +4,7 @@ use biome_text_size::TextSize; use memchr::memchr2; use memchr::memmem; +static CR_FINDER: LazyLock = LazyLock::new(|| memmem::Finder::new(b"\r")); static CRLF_FINDER: LazyLock = LazyLock::new(|| memmem::Finder::new(b"\r\n")); /// Line ending styles @@ -39,6 +40,18 @@ impl LineEnding { } } +/// Infers the line endings of a document. Defaults to [LineEnding::Lf] when no line +/// endings are detected. +/// +/// If you need the position of the first newline detected, use [find_newline]. +#[inline] +pub fn infer_line_ending(text: &str) -> LineEnding { + match find_newline(text) { + Some((_position, ending)) => ending, + None => LineEnding::Lf, + } +} + /// Finds the next newline character. Returns its position and the [`LineEnding`]. #[inline] pub fn find_newline(text: &str) -> Option<(usize, LineEnding)> { @@ -73,7 +86,16 @@ pub fn find_newline(text: &str) -> Option<(usize, LineEnding)> { /// license = "MIT OR Apache-2.0" /// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/line_index.rs" /// --- -pub fn normalize_crlf_newlines(text: String) -> String { +#[inline] +pub fn normalize_newlines(text: String) -> (String, LineEnding) { + match infer_line_ending(&text) { + LineEnding::Lf => (text, LineEnding::Lf), + LineEnding::Cr => (normalize_cr_newlines(text), LineEnding::Cr), + LineEnding::Crlf => (normalize_crlf_newlines(text), LineEnding::Crlf), + } +} + +fn normalize_crlf_newlines(text: String) -> String { let mut buf = text.into_bytes(); let mut gap_len = 0; let mut tail = buf.as_mut_slice(); @@ -106,6 +128,21 @@ pub fn normalize_crlf_newlines(text: String) -> String { } } +fn normalize_cr_newlines(text: String) -> String { + let mut buf = text.into_bytes(); + + if CR_FINDER.find(&buf).is_some() { + // 1:1 byte replacement, total length does not change + for byte in buf.iter_mut() { + if byte == &b'\r' { + *byte = b'\n'; + } + } + } + + unsafe { String::from_utf8_unchecked(buf) } +} + #[cfg(test)] mod tests { use super::*; @@ -114,7 +151,10 @@ mod tests { fn unix() { let src = "a\nb\nc\n\n\n\n"; assert_eq!(find_newline(src), Some((1, LineEnding::Lf))); - assert_eq!(normalize_crlf_newlines(src.to_string()), src); + assert_eq!( + normalize_newlines(src.to_string()), + (src.to_string(), LineEnding::Lf) + ); } #[test] @@ -122,8 +162,8 @@ mod tests { let src = "\r\na\r\n\r\nb\r\nc\r\n\r\n\r\n\r\n"; assert_eq!(find_newline(src), Some((0, LineEnding::Crlf))); assert_eq!( - normalize_crlf_newlines(src.to_string()), - "\na\n\nb\nc\n\n\n\n" + normalize_newlines(src.to_string()), + (String::from("\na\n\nb\nc\n\n\n\n"), LineEnding::Crlf) ); } @@ -131,13 +171,19 @@ mod tests { fn mixed() { let src = "a\r\nb\r\nc\r\n\n\r\n\n"; assert_eq!(find_newline(src), Some((1, LineEnding::Crlf))); - assert_eq!(normalize_crlf_newlines(src.to_string()), "a\nb\nc\n\n\n\n"); + assert_eq!( + normalize_newlines(src.to_string()), + (String::from("a\nb\nc\n\n\n\n"), LineEnding::Crlf) + ); } #[test] fn none() { let src = "abc"; assert_eq!(find_newline(src), None); - assert_eq!(normalize_crlf_newlines(src.to_string()), src); + assert_eq!( + normalize_newlines(src.to_string()), + (src.to_string(), LineEnding::Lf) + ); } } diff --git a/crates/server_test/Cargo.toml b/crates/server_test/Cargo.toml new file mode 100644 index 00000000..ed524bd4 --- /dev/null +++ b/crates/server_test/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "server_test" +version = "0.0.0" +publish = false +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +lsp-types.workspace = true +lsp-server.workspace = true +url.workspace = true + +[lints] +workspace = true diff --git a/crates/server_test/src/lib.rs b/crates/server_test/src/lib.rs new file mode 100644 index 00000000..ac986b8d --- /dev/null +++ b/crates/server_test/src/lib.rs @@ -0,0 +1,3 @@ +mod lsp_client; + +pub use lsp_client::TestClient; diff --git a/crates/server_test/src/lsp_client.rs b/crates/server_test/src/lsp_client.rs new file mode 100644 index 00000000..f3d7edff --- /dev/null +++ b/crates/server_test/src/lsp_client.rs @@ -0,0 +1,185 @@ +// +// lsp_client.rs +// +// Copyright (C) 2024 Posit Software, PBC. All rights reserved. +// +// + +use lsp_server::Connection; +use lsp_server::RequestId; +use lsp_types::ClientInfo; +use lsp_types::GeneralClientCapabilities; +use lsp_types::PositionEncodingKind; +use std::num::NonZeroUsize; +use std::thread::JoinHandle; + +pub const TEST_CLIENT_NAME: &str = "AirTestClient"; + +pub struct TestClient { + client: Connection, + server_handle: Option>, + request_id: i32, + encoding: PositionEncodingKind, + init_params: Option, +} + +impl TestClient { + pub fn new(start_server: F) -> Self + where + F: FnOnce(NonZeroUsize, lsp_server::Connection, Option) + + Send + + 'static, + { + let worker_threads = NonZeroUsize::new(4).unwrap(); + let (server, client) = lsp_server::Connection::memory(); + + let server_handle = std::thread::spawn(move || { + start_server(worker_threads, server, None); + }); + + Self { + client, + server_handle: Some(server_handle), + request_id: 0, + encoding: PositionEncodingKind::UTF8, + init_params: None, + } + } + + pub fn encoding(&self) -> &PositionEncodingKind { + &self.encoding + } + + fn id(&mut self) -> RequestId { + let id = self.request_id; + self.request_id = id + 1; + RequestId::from(id) + } + + pub fn recv_response(&mut self) -> lsp_server::Response { + // Unwrap: Result (Err if stream closed) + let message = self.client.receiver.recv().unwrap(); + + match message { + lsp_server::Message::Request(request) => panic!("Expected response, got {request:?}"), + lsp_server::Message::Response(response) => response, + lsp_server::Message::Notification(notification) => { + panic!("Expected response, got {notification:?}") + } + } + } + + pub fn notify(&mut self, params: N::Params) + where + N: lsp_types::notification::Notification, + { + let method = N::METHOD.to_string(); + let notification = lsp_server::Notification::new(method, params); + let message = lsp_server::Message::Notification(notification); + + // Unwrap: For this test client it's fine to panic if we can't send + self.client.sender.send(message).unwrap() + } + + pub fn request(&mut self, params: R::Params) -> RequestId + where + R: lsp_types::request::Request, + { + let id = self.id(); + let method = R::METHOD.to_string(); + let request = lsp_server::Request::new(id.clone(), method, params); + let message = lsp_server::Message::Request(request); + + // Unwrap: For this test client it's fine to panic if we can't send + self.client.sender.send(message).unwrap(); + + id + } + + pub fn initialize(&mut self) -> RequestId { + let params: Option = std::mem::take(&mut self.init_params); + let params = params.unwrap_or_default(); + let params = Self::with_client_info(params); + let params = Self::with_utf8(params); + self.request::(params) + } + + // Regardless of how we got the params, ensure the client name is set to + // `AirTestClient` so we can recognize it when we set up global logging. + fn with_client_info( + mut init_params: lsp_types::InitializeParams, + ) -> lsp_types::InitializeParams { + init_params.client_info = Some(ClientInfo { + name: String::from(TEST_CLIENT_NAME), + version: None, + }); + init_params + } + + // Regardless of how we got the params, ensure we use UTF-8 encodings + fn with_utf8(mut init_params: lsp_types::InitializeParams) -> lsp_types::InitializeParams { + init_params.capabilities.general = Some(GeneralClientCapabilities { + position_encodings: Some(vec![ + PositionEncodingKind::UTF8, + PositionEncodingKind::UTF16, + ]), + ..Default::default() + }); + init_params + } + + pub fn initialized(&mut self) { + let params = lsp_types::InitializedParams {}; + self.notify::(params) + } + + pub fn with_initialize_params(&mut self, init_params: lsp_types::InitializeParams) { + self.init_params = Some(init_params); + } + + pub fn close_document(&mut self, uri: url::Url) { + let params = lsp_types::DidCloseTextDocumentParams { + text_document: lsp_types::TextDocumentIdentifier { uri }, + }; + self.did_close_text_document(params) + } + + pub fn shutdown(&mut self) { + // TODO: Check that no messages are incoming + self.request::(()); + self.recv_response(); + } + + pub fn exit(&mut self) { + // Unwrap: Can only exit once + let server_handle = + std::mem::take(&mut self.server_handle).expect("`exit()` can only be called once"); + + self.notify::(()); + + // Now wait for the server task to complete. + // Unwrap: Panics if task can't shut down as expected + server_handle + .join() + .expect("Couldn't join on the server thread."); + } + + pub fn did_open_text_document(&mut self, params: lsp_types::DidOpenTextDocumentParams) { + self.notify::(params) + } + + pub fn did_close_text_document(&mut self, params: lsp_types::DidCloseTextDocumentParams) { + self.notify::(params) + } + + pub fn formatting(&mut self, params: lsp_types::DocumentFormattingParams) -> RequestId { + self.request::(params) + } + + pub fn range_formatting( + &mut self, + params: lsp_types::DocumentRangeFormattingParams, + ) -> RequestId { + self.request::(params) + } +} From f94e4930e6ca37e03b4c694948bc6dd5e32933f6 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 09:57:32 -0500 Subject: [PATCH 08/44] Extract out `TextSize` proto bits --- crates/ruff_server/src/edit/text_document.rs | 2 +- crates/ruff_server/src/proto.rs | 2 + crates/ruff_server/src/proto/text_range.rs | 144 +----------------- crates/ruff_server/src/proto/text_size.rs | 141 +++++++++++++++++ .../src/server/api/requests/format_range.rs | 2 +- crates/ruff_server/src/test/client_ext.rs | 2 +- 6 files changed, 154 insertions(+), 139 deletions(-) create mode 100644 crates/ruff_server/src/proto/text_size.rs diff --git a/crates/ruff_server/src/edit/text_document.rs b/crates/ruff_server/src/edit/text_document.rs index 6bf3f1f5..04150a48 100644 --- a/crates/ruff_server/src/edit/text_document.rs +++ b/crates/ruff_server/src/edit/text_document.rs @@ -142,7 +142,7 @@ impl TextDocument { } in changes { if let Some(range) = range { - let range = TextRange::from_proto(&range, &new_contents, &active_index, encoding); + let range = TextRange::from_proto(range, &new_contents, &active_index, encoding); new_contents.replace_range( usize::from(range.start())..usize::from(range.end()), diff --git a/crates/ruff_server/src/proto.rs b/crates/ruff_server/src/proto.rs index a53a4e14..28e08c0c 100644 --- a/crates/ruff_server/src/proto.rs +++ b/crates/ruff_server/src/proto.rs @@ -1,4 +1,6 @@ mod text_edit; mod text_range; +mod text_size; pub(crate) use text_range::TextRangeExt; +pub(crate) use text_size::TextSizeExt; diff --git a/crates/ruff_server/src/proto/text_range.rs b/crates/ruff_server/src/proto/text_range.rs index 47436c42..8a6a4fb7 100644 --- a/crates/ruff_server/src/proto/text_range.rs +++ b/crates/ruff_server/src/proto/text_range.rs @@ -5,17 +5,17 @@ // +------------------------------------------------------------+ use crate::edit::PositionEncoding; +use crate::proto::TextSizeExt; use biome_text_size::{TextRange, TextSize}; use lsp_types as types; -use ruff_source_file::OneIndexed; -use ruff_source_file::{LineIndex, SourceLocation}; +use ruff_source_file::LineIndex; // We don't own this type so we need a helper trait pub(crate) trait TextRangeExt { fn into_proto(self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range; fn from_proto( - range: &lsp_types::Range, + range: types::Range, text: &str, index: &LineIndex, encoding: PositionEncoding, @@ -25,148 +25,20 @@ pub(crate) trait TextRangeExt { impl TextRangeExt for TextRange { fn into_proto(self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range { types::Range { - start: source_location_to_position(&offset_to_source_location( - self.start(), - text, - index, - encoding, - )), - end: source_location_to_position(&offset_to_source_location( - self.end(), - text, - index, - encoding, - )), + start: self.start().into_proto(text, index, encoding), + end: self.end().into_proto(text, index, encoding), } } fn from_proto( - range: &lsp_types::Range, + range: types::Range, text: &str, index: &LineIndex, encoding: PositionEncoding, ) -> Self { - let start_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(range.start.line)), - text, - ); - let end_line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(range.end.line)), - text, - ); - - let (start_column_offset, end_column_offset) = match encoding { - PositionEncoding::UTF8 => ( - TextSize::from(range.start.character), - TextSize::from(range.end.character), - ), - - PositionEncoding::UTF16 => { - // Fast path for ASCII only documents - if index.is_ascii() { - ( - TextSize::from(range.start.character), - TextSize::from(range.end.character), - ) - } else { - // UTF16 encodes characters either as one or two 16 bit words. - // The position in `range` is the 16-bit word offset from the start of the line (and not the character offset) - // UTF-16 with a text that may use variable-length characters. - ( - utf8_column_offset(range.start.character, &text[start_line]), - utf8_column_offset(range.end.character, &text[end_line]), - ) - } - } - PositionEncoding::UTF32 => { - // UTF-32 uses 4 bytes for each character. Meaning, the position in range is a character offset. - return TextRange::new( - index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(range.start.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(range.start.character)), - text, - ), - index.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(range.end.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(range.end.character)), - text, - ), - ); - } - }; - TextRange::new( - start_line.start() + start_column_offset.clamp(TextSize::from(0), start_line.end()), - end_line.start() + end_column_offset.clamp(TextSize::from(0), end_line.end()), + TextSize::from_proto(range.start, text, index, encoding), + TextSize::from_proto(range.end, text, index, encoding), ) } } - -fn u32_index_to_usize(index: u32) -> usize { - usize::try_from(index).expect("u32 fits in usize") -} - -/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. -fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { - let mut utf8_code_unit_offset = TextSize::from(0); - - let mut i = 0u32; - - for c in line.chars() { - if i >= utf16_code_unit_offset { - break; - } - - // Count characters encoded as two 16 bit words as 2 characters. - { - utf8_code_unit_offset += - TextSize::from(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); - i += u32::try_from(c.len_utf16()).expect("utf16 len always <=2"); - } - } - - utf8_code_unit_offset -} - -fn offset_to_source_location( - offset: TextSize, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, -) -> SourceLocation { - match encoding { - PositionEncoding::UTF8 => { - let row = index.line_index(offset); - let column = offset - index.line_start(row, text); - - SourceLocation { - column: OneIndexed::from_zero_indexed(column.into()), - row, - } - } - PositionEncoding::UTF16 => { - let row = index.line_index(offset); - - let column = if index.is_ascii() { - (offset - index.line_start(row, text)).into() - } else { - let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)]; - up_to_line.encode_utf16().count() - }; - - SourceLocation { - column: OneIndexed::from_zero_indexed(column), - row, - } - } - PositionEncoding::UTF32 => index.source_location(offset, text), - } -} - -fn source_location_to_position(location: &SourceLocation) -> types::Position { - types::Position { - line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"), - character: u32::try_from(location.column.to_zero_indexed()) - .expect("character usize fits in u32"), - } -} diff --git a/crates/ruff_server/src/proto/text_size.rs b/crates/ruff_server/src/proto/text_size.rs new file mode 100644 index 00000000..218984bf --- /dev/null +++ b/crates/ruff_server/src/proto/text_size.rs @@ -0,0 +1,141 @@ +use crate::edit::PositionEncoding; +use biome_rowan::TextRange; +use biome_text_size::TextSize; +use lsp_types as types; +use ruff_source_file::OneIndexed; +use ruff_source_file::{LineIndex, SourceLocation}; + +// We don't own this type so we need a helper trait +pub(crate) trait TextSizeExt { + fn into_proto( + self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> types::Position; + + fn from_proto( + position: types::Position, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> Self; +} + +impl TextSizeExt for TextSize { + fn into_proto( + self, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> types::Position { + source_location_to_position(&offset_to_source_location(self, text, index, encoding)) + } + + fn from_proto( + position: types::Position, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, + ) -> Self { + let line = index.line_range( + OneIndexed::from_zero_indexed(u32_index_to_usize(position.line)), + text, + ); + + let column_offset = match encoding { + PositionEncoding::UTF8 => TextSize::from(position.character), + + PositionEncoding::UTF16 => { + // Fast path for ASCII only documents + if index.is_ascii() { + TextSize::from(position.character) + } else { + // UTF-16 encodes characters either as one or two 16 bit words. + // The `position` is the 16-bit word offset from the start of the line (and not the character offset) + utf8_column_offset(position.character, &text[line]) + } + } + + PositionEncoding::UTF32 => { + // UTF-32 uses 4 bytes for each character. Meaning, the position is a character offset. + return index.offset( + OneIndexed::from_zero_indexed(u32_index_to_usize(position.line)), + OneIndexed::from_zero_indexed(u32_index_to_usize(position.character)), + text, + ); + } + }; + + line.start() + column_offset.clamp(TextSize::from(0), line.end()) + } +} + +fn u32_index_to_usize(index: u32) -> usize { + usize::try_from(index).expect("u32 fits in usize") +} + +/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. +fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { + let mut utf8_code_unit_offset = TextSize::from(0); + + let mut i = 0u32; + + for c in line.chars() { + if i >= utf16_code_unit_offset { + break; + } + + // Count characters encoded as two 16 bit words as 2 characters. + { + utf8_code_unit_offset += + TextSize::from(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); + i += u32::try_from(c.len_utf16()).expect("utf16 len always <=2"); + } + } + + utf8_code_unit_offset +} + +fn offset_to_source_location( + offset: TextSize, + text: &str, + index: &LineIndex, + encoding: PositionEncoding, +) -> SourceLocation { + match encoding { + PositionEncoding::UTF8 => { + let row = index.line_index(offset); + let column = offset - index.line_start(row, text); + + SourceLocation { + column: OneIndexed::from_zero_indexed(column.into()), + row, + } + } + PositionEncoding::UTF16 => { + let row = index.line_index(offset); + + let column = if index.is_ascii() { + (offset - index.line_start(row, text)).into() + } else { + let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)]; + up_to_line.encode_utf16().count() + }; + + SourceLocation { + column: OneIndexed::from_zero_indexed(column), + row, + } + } + PositionEncoding::UTF32 => index.source_location(offset, text), + } +} + +fn source_location_to_position(location: &SourceLocation) -> types::Position { + types::Position { + line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"), + character: u32::try_from(location.column.to_zero_indexed()) + .expect("character usize fits in u32"), + } +} diff --git a/crates/ruff_server/src/server/api/requests/format_range.rs b/crates/ruff_server/src/server/api/requests/format_range.rs index fae7a5f1..6bbcf837 100644 --- a/crates/ruff_server/src/server/api/requests/format_range.rs +++ b/crates/ruff_server/src/server/api/requests/format_range.rs @@ -65,7 +65,7 @@ fn format_text_document_range( let text = text_document.contents(); let ending = text_document.ending(); let index = text_document.index(); - let range = TextRange::from_proto(&range, text, index, encoding); + let range = TextRange::from_proto(range, text, index, encoding); let Some((new_text, new_range)) = format_source_range(text, formatter_settings, range) .with_failure_code(lsp_server::ErrorCode::InternalError)? diff --git a/crates/ruff_server/src/test/client_ext.rs b/crates/ruff_server/src/test/client_ext.rs index a2c55502..68047115 100644 --- a/crates/ruff_server/src/test/client_ext.rs +++ b/crates/ruff_server/src/test/client_ext.rs @@ -144,7 +144,7 @@ fn apply_text_edits( edits.reverse(); for edit in edits { - let range: Range = TextRange::from_proto(&edit.range, text, index, encoding).into(); + let range: Range = TextRange::from_proto(edit.range, text, index, encoding).into(); new_text.replace_range(range, &edit.new_text); } From 1f4605ab4a8f19b85a2f0f05e814d454dca23522 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:04:55 -0500 Subject: [PATCH 09/44] Check for incoming messages during `shutdown()` --- crates/server_test/src/lsp_client.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/server_test/src/lsp_client.rs b/crates/server_test/src/lsp_client.rs index f3d7edff..e722c139 100644 --- a/crates/server_test/src/lsp_client.rs +++ b/crates/server_test/src/lsp_client.rs @@ -145,11 +145,23 @@ impl TestClient { } pub fn shutdown(&mut self) { - // TODO: Check that no messages are incoming + self.check_no_incoming(); self.request::(()); self.recv_response(); } + fn check_no_incoming(&self) { + let mut messages = Vec::new(); + + while let Ok(message) = self.client.receiver.try_recv() { + messages.push(message); + } + + if !messages.is_empty() { + panic!("Must handle all messages before shutdown. Found the following unhandled incoming messages:\n{messages:?}"); + } + } + pub fn exit(&mut self) { // Unwrap: Can only exit once let server_handle = From fd6615918b19a9bd4fdff427dd543db6399d36b4 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:06:09 -0500 Subject: [PATCH 10/44] Assert same `RequestId` on shutdown --- crates/server_test/src/lsp_client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/server_test/src/lsp_client.rs b/crates/server_test/src/lsp_client.rs index e722c139..be8a3044 100644 --- a/crates/server_test/src/lsp_client.rs +++ b/crates/server_test/src/lsp_client.rs @@ -146,8 +146,8 @@ impl TestClient { pub fn shutdown(&mut self) { self.check_no_incoming(); - self.request::(()); - self.recv_response(); + let id = self.request::(()); + assert_eq!(id, self.recv_response().id); } fn check_no_incoming(&self) { From 11fbbb2dfa067ebc13992bb6a7a735f5151c0721 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:18:40 -0500 Subject: [PATCH 11/44] Use `ErrorVec` approach in `DidChangeWorkspace` --- crates/ruff_server/src/error.rs | 52 +++++++++++++++++++ crates/ruff_server/src/lib.rs | 1 + .../api/notifications/did_change_workspace.rs | 17 +++--- crates/ruff_server/src/session.rs | 2 +- crates/ruff_server/src/session/index.rs | 4 +- 5 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 crates/ruff_server/src/error.rs diff --git a/crates/ruff_server/src/error.rs b/crates/ruff_server/src/error.rs new file mode 100644 index 00000000..fe19c650 --- /dev/null +++ b/crates/ruff_server/src/error.rs @@ -0,0 +1,52 @@ +/// A tool for collecting multiple anyhow errors into a single [`anyhow::Result`] +/// +/// Only applicable if the intended `Ok()` value at the end is `()`. +#[derive(Debug, Default)] +pub(crate) struct ErrorVec { + errors: Vec, +} + +impl ErrorVec { + pub(crate) fn new() -> Self { + Self::default() + } + + /// Conditionally push to the error vector if the `result` is an `Err` case + pub(crate) fn push_err(&mut self, result: anyhow::Result) { + match result { + Ok(_) => (), + Err(error) => self.push(error), + } + } + + /// Push a new error to the error vector + pub(crate) fn push(&mut self, error: anyhow::Error) { + self.errors.push(error); + } + + /// Convert a error vector into a single [`anyhow::Result`] that knows how to print + /// each of the individual errors + pub(crate) fn into_result(self) -> anyhow::Result<()> { + if self.errors.is_empty() { + Ok(()) + } else { + Err(anyhow::anyhow!(self)) + } + } +} + +impl std::error::Error for ErrorVec {} + +impl std::fmt::Display for ErrorVec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.errors.len() > 1 { + f.write_str("Multiple errors:\n")?; + } + + for error in &self.errors { + std::fmt::Display::fmt(error, f)?; + } + + Ok(()) + } +} diff --git a/crates/ruff_server/src/lib.rs b/crates/ruff_server/src/lib.rs index 1625033f..56867ac5 100644 --- a/crates/ruff_server/src/lib.rs +++ b/crates/ruff_server/src/lib.rs @@ -7,6 +7,7 @@ mod message; mod crates; mod edit; +mod error; mod logging; mod proto; mod server; diff --git a/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs b/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs index 39786a91..ba3506d3 100644 --- a/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs +++ b/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs @@ -4,6 +4,7 @@ // | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | // +------------------------------------------------------------+ +use crate::error::ErrorVec; use crate::server::api::LSPResult; use crate::server::client::{Notifier, Requester}; use crate::server::Result; @@ -24,16 +25,18 @@ impl super::SyncNotificationHandler for DidChangeWorkspace { _requester: &mut Requester, params: types::DidChangeWorkspaceFoldersParams, ) -> Result<()> { + // Collect all `errors` to ensure we don't drop any events if we encounter an error + let mut errors = ErrorVec::new(); + for types::WorkspaceFolder { uri, .. } in params.event.added { - session - .open_workspace_folder(uri) - .with_failure_code(lsp_server::ErrorCode::InvalidParams)?; + errors.push_err(session.open_workspace_folder(&uri)); } for types::WorkspaceFolder { uri, .. } in params.event.removed { - session - .close_workspace_folder(&uri) - .with_failure_code(lsp_server::ErrorCode::InvalidParams)?; + errors.push_err(session.close_workspace_folder(&uri)); } - Ok(()) + + errors + .into_result() + .with_failure_code(lsp_server::ErrorCode::InvalidParams) } } diff --git a/crates/ruff_server/src/session.rs b/crates/ruff_server/src/session.rs index dd8e86ab..9b9cc837 100644 --- a/crates/ruff_server/src/session.rs +++ b/crates/ruff_server/src/session.rs @@ -106,7 +106,7 @@ impl Session { } /// Open a workspace folder at the given `url`. - pub(crate) fn open_workspace_folder(&mut self, url: Url) -> crate::Result<()> { + pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { self.index.open_workspace_folder(url) } diff --git a/crates/ruff_server/src/session/index.rs b/crates/ruff_server/src/session/index.rs index c9e3a06e..8366afaa 100644 --- a/crates/ruff_server/src/session/index.rs +++ b/crates/ruff_server/src/session/index.rs @@ -87,8 +87,8 @@ impl Index { DocumentKey::Text(url) } - pub(super) fn open_workspace_folder(&mut self, url: Url) -> crate::Result<()> { - self.settings.open_workspace_folder(&url) + pub(super) fn open_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { + self.settings.open_workspace_folder(url) } #[allow(dead_code)] From 3c58e80bda6e2b4b874909f614868349692a4ff9 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:20:45 -0500 Subject: [PATCH 12/44] Add tracing --- crates/ruff_server/src/server/api/requests/format.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index 0b9c0aa5..a534858a 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -33,6 +33,7 @@ impl super::BackgroundDocumentRequestHandler for Format { } /// Formats a full text document +#[tracing::instrument(level = "info", skip_all)] pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result { let text_document = snapshot.query().as_single_document(); let query = snapshot.query(); From 10fe1462a352d26d93f910c5557f64798638bfdf Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:22:31 -0500 Subject: [PATCH 13/44] Move `FormatResponse` around --- crates/ruff_server/src/server/api/requests.rs | 2 -- crates/ruff_server/src/server/api/requests/format.rs | 8 +++++--- .../src/server/api/requests/format_range.rs | 11 +++++------ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/ruff_server/src/server/api/requests.rs b/crates/ruff_server/src/server/api/requests.rs index c8e35fee..7bb6f1b6 100644 --- a/crates/ruff_server/src/server/api/requests.rs +++ b/crates/ruff_server/src/server/api/requests.rs @@ -9,5 +9,3 @@ use super::{ pub(super) use format::Format; pub(super) use format_range::FormatRange; pub(super) use view_file::ViewFile; - -type FormatResponse = Option>; diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index a534858a..a9681599 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -15,6 +15,8 @@ use crate::server::api::LSPResult; use crate::server::{client::Notifier, Result}; use crate::session::{DocumentQuery, DocumentSnapshot}; +type FormatResponse = Option>; + pub(crate) struct Format; impl super::RequestHandler for Format { @@ -27,14 +29,14 @@ impl super::BackgroundDocumentRequestHandler for Format { snapshot: DocumentSnapshot, _notifier: Notifier, _params: types::DocumentFormattingParams, - ) -> Result { + ) -> Result { format_document(&snapshot) } } /// Formats a full text document #[tracing::instrument(level = "info", skip_all)] -pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result { +pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result { let text_document = snapshot.query().as_single_document(); let query = snapshot.query(); format_text_document(text_document, query, snapshot.encoding()) @@ -44,7 +46,7 @@ fn format_text_document( text_document: &TextDocument, query: &DocumentQuery, encoding: PositionEncoding, -) -> Result { +) -> Result { let document_settings = query.settings(); let formatter_settings = &document_settings.format; diff --git a/crates/ruff_server/src/server/api/requests/format_range.rs b/crates/ruff_server/src/server/api/requests/format_range.rs index 6bbcf837..3e2a86da 100644 --- a/crates/ruff_server/src/server/api/requests/format_range.rs +++ b/crates/ruff_server/src/server/api/requests/format_range.rs @@ -24,6 +24,8 @@ use crate::server::api::LSPResult; use crate::server::{client::Notifier, Result}; use crate::session::{DocumentQuery, DocumentSnapshot}; +type FormatRangeResponse = Option>; + pub(crate) struct FormatRange; impl super::RequestHandler for FormatRange { @@ -36,17 +38,14 @@ impl super::BackgroundDocumentRequestHandler for FormatRange { snapshot: DocumentSnapshot, _notifier: Notifier, params: types::DocumentRangeFormattingParams, - ) -> Result { + ) -> Result { format_document_range(&snapshot, params.range) } } /// Formats the specified [`Range`] in the [`DocumentSnapshot`]. #[tracing::instrument(level = "info", skip_all)] -fn format_document_range( - snapshot: &DocumentSnapshot, - range: Range, -) -> Result { +fn format_document_range(snapshot: &DocumentSnapshot, range: Range) -> Result { let text_document = snapshot.query().as_single_document(); let query = snapshot.query(); format_text_document_range(text_document, range, query, snapshot.encoding()) @@ -58,7 +57,7 @@ fn format_text_document_range( range: Range, query: &DocumentQuery, encoding: PositionEncoding, -) -> Result { +) -> Result { let document_settings = query.settings(); let formatter_settings = &document_settings.format; From d3212bdcbf94c658d00a33f52bb613bf7d112fff Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:25:58 -0500 Subject: [PATCH 14/44] Make `define_document_url` less magical --- crates/ruff_server/src/server/api.rs | 12 ------------ crates/ruff_server/src/server/api/requests.rs | 5 +---- crates/ruff_server/src/server/api/requests/format.rs | 5 ++++- .../src/server/api/requests/format_range.rs | 7 ++++++- .../ruff_server/src/server/api/requests/view_file.rs | 5 ++++- crates/ruff_server/src/server/api/traits.rs | 6 ------ 6 files changed, 15 insertions(+), 25 deletions(-) diff --git a/crates/ruff_server/src/server/api.rs b/crates/ruff_server/src/server/api.rs index beab501c..12a5a93f 100644 --- a/crates/ruff_server/src/server/api.rs +++ b/crates/ruff_server/src/server/api.rs @@ -18,18 +18,6 @@ use self::traits::{NotificationHandler, RequestHandler}; use super::{client::Responder, schedule::BackgroundSchedule, Result}; -/// Defines the `document_url` method for implementers of [`traits::Notification`] and [`traits::Request`], -/// given the parameter type used by the implementer. -macro_rules! define_document_url { - ($params:ident: &$p:ty) => { - fn document_url($params: &$p) -> std::borrow::Cow { - std::borrow::Cow::Borrowed(&$params.text_document.uri) - } - }; -} - -use define_document_url; - pub(super) fn request<'a>(req: server::Request) -> Task<'a> { let id = req.id.clone(); diff --git a/crates/ruff_server/src/server/api/requests.rs b/crates/ruff_server/src/server/api/requests.rs index 7bb6f1b6..e05300c4 100644 --- a/crates/ruff_server/src/server/api/requests.rs +++ b/crates/ruff_server/src/server/api/requests.rs @@ -2,10 +2,7 @@ mod format; mod format_range; mod view_file; -use super::{ - define_document_url, - traits::{BackgroundDocumentRequestHandler, RequestHandler}, -}; +use super::traits::{BackgroundDocumentRequestHandler, RequestHandler}; pub(super) use format::Format; pub(super) use format_range::FormatRange; pub(super) use view_file::ViewFile; diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index a9681599..38edcf40 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -24,7 +24,10 @@ impl super::RequestHandler for Format { } impl super::BackgroundDocumentRequestHandler for Format { - super::define_document_url!(params: &types::DocumentFormattingParams); + fn document_url(params: &types::DocumentFormattingParams) -> std::borrow::Cow { + std::borrow::Cow::Borrowed(¶ms.text_document.uri) + } + fn run_with_snapshot( snapshot: DocumentSnapshot, _notifier: Notifier, diff --git a/crates/ruff_server/src/server/api/requests/format_range.rs b/crates/ruff_server/src/server/api/requests/format_range.rs index 3e2a86da..f347fd29 100644 --- a/crates/ruff_server/src/server/api/requests/format_range.rs +++ b/crates/ruff_server/src/server/api/requests/format_range.rs @@ -33,7 +33,12 @@ impl super::RequestHandler for FormatRange { } impl super::BackgroundDocumentRequestHandler for FormatRange { - super::define_document_url!(params: &types::DocumentRangeFormattingParams); + fn document_url( + params: &types::DocumentRangeFormattingParams, + ) -> std::borrow::Cow { + std::borrow::Cow::Borrowed(¶ms.text_document.uri) + } + fn run_with_snapshot( snapshot: DocumentSnapshot, _notifier: Notifier, diff --git a/crates/ruff_server/src/server/api/requests/view_file.rs b/crates/ruff_server/src/server/api/requests/view_file.rs index 759ba297..fceb6c35 100644 --- a/crates/ruff_server/src/server/api/requests/view_file.rs +++ b/crates/ruff_server/src/server/api/requests/view_file.rs @@ -41,7 +41,10 @@ impl super::RequestHandler for ViewFile { } impl super::BackgroundDocumentRequestHandler for ViewFile { - super::define_document_url!(params: &ViewFileParams); + fn document_url(params: &ViewFileParams) -> std::borrow::Cow { + std::borrow::Cow::Borrowed(¶ms.text_document.uri) + } + fn run_with_snapshot( snapshot: DocumentSnapshot, _notifier: Notifier, diff --git a/crates/ruff_server/src/server/api/traits.rs b/crates/ruff_server/src/server/api/traits.rs index d86889ad..1d4bd543 100644 --- a/crates/ruff_server/src/server/api/traits.rs +++ b/crates/ruff_server/src/server/api/traits.rs @@ -33,9 +33,6 @@ pub(super) trait SyncRequestHandler: RequestHandler { /// A request handler that can be run on a background thread. pub(super) trait BackgroundDocumentRequestHandler: RequestHandler { - /// `document_url` can be implemented automatically with - /// `define_document_url!(params: &)` in the trait - /// implementation. fn document_url( params: &<::RequestType as Request>::Params, ) -> std::borrow::Cow; @@ -69,9 +66,6 @@ pub(super) trait SyncNotificationHandler: NotificationHandler { /// A notification handler that can be run on a background thread. pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler { - /// `document_url` can be implemented automatically with - /// `define_document_url!(params: &)` in the trait - /// implementation. fn document_url( params: &<::NotificationType as LSPNotification>::Params, ) -> std::borrow::Cow; From 4bb77d30993ecc01c40db329a37f35690c0eac9e Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:37:20 -0500 Subject: [PATCH 15/44] Nuke the old lsp --- Cargo.lock | 511 +---------------- Cargo.toml | 2 - crates/line_ending/Cargo.toml | 20 - crates/line_ending/src/lib.rs | 107 ---- crates/lsp/Cargo.toml | 62 --- crates/lsp/build.rs | 34 -- crates/lsp/src/capabilities.rs | 40 -- crates/lsp/src/config.rs | 129 ----- crates/lsp/src/crates.rs | 3 - crates/lsp/src/documents.rs | 250 --------- crates/lsp/src/encoding.rs | 61 --- crates/lsp/src/error.rs | 52 -- crates/lsp/src/from_proto.rs | 37 -- crates/lsp/src/handlers.rs | 95 ---- crates/lsp/src/handlers_ext.rs | 94 ---- crates/lsp/src/handlers_format.rs | 509 ----------------- crates/lsp/src/handlers_state.rs | 348 ------------ crates/lsp/src/lib.rs | 28 - crates/lsp/src/logging.rs | 318 ----------- crates/lsp/src/main_loop.rs | 515 ------------------ crates/lsp/src/rust_analyzer/diff.rs | 44 -- crates/lsp/src/rust_analyzer/line_index.rs | 19 - crates/lsp/src/rust_analyzer/mod.rs | 5 - crates/lsp/src/rust_analyzer/text_edit.rs | 338 ------------ crates/lsp/src/rust_analyzer/to_proto.rs | 60 -- crates/lsp/src/rust_analyzer/utils.rs | 67 --- ...__documents__tests__document_syntax-2.snap | 23 - ...sp__documents__tests__document_syntax.snap | 19 - .../lsp__handlers_format__tests__format.snap | 7 - ...__tests__format_range_logical_lines-2.snap | 7 - ...__tests__format_range_logical_lines-3.snap | 6 - ...__tests__format_range_logical_lines-4.snap | 8 - ...__tests__format_range_logical_lines-5.snap | 11 - ...at__tests__format_range_logical_lines.snap | 6 - ...tests__format_range_mismatched_indent.snap | 6 - ..._tests__format_range_multiple_lines-2.snap | 7 - ...t__tests__format_range_multiple_lines.snap | 7 - ...rs_format__tests__format_range_none-2.snap | 5 - ...rs_format__tests__format_range_none-3.snap | 5 - ...lers_format__tests__format_range_none.snap | 5 - ...tests__format_range_unmatched_lists-2.snap | 10 - ...tests__format_range_unmatched_lists-3.snap | 10 - ...tests__format_range_unmatched_lists-4.snap | 10 - ...tests__format_range_unmatched_lists-5.snap | 6 - ...tests__format_range_unmatched_lists-6.snap | 6 - ...__tests__format_range_unmatched_lists.snap | 10 - crates/lsp/src/state.rs | 67 --- crates/lsp/src/test_utils.rs | 27 - crates/lsp/src/to_proto.rs | 51 -- crates/lsp/src/tower_lsp.rs | 361 ------------ crates/lsp/src/tower_lsp_test_client.rs | 122 ----- crates/lsp/src/workspaces.rs | 174 ------ crates/lsp_test/Cargo.toml | 29 - crates/lsp_test/src/lib.rs | 3 - crates/lsp_test/src/lsp_client.rs | 163 ------ crates/lsp_test/src/tower_lsp/codec.rs | 398 -------------- crates/lsp_test/src/tower_lsp/mod.rs | 4 - crates/lsp_test/src/tower_lsp/request.rs | 217 -------- crates/ruff_server/src/server.rs | 1 - crates/tests_macros/src/lib.rs | 21 - 60 files changed, 3 insertions(+), 5557 deletions(-) delete mode 100644 crates/line_ending/Cargo.toml delete mode 100644 crates/line_ending/src/lib.rs delete mode 100644 crates/lsp/Cargo.toml delete mode 100644 crates/lsp/build.rs delete mode 100644 crates/lsp/src/capabilities.rs delete mode 100644 crates/lsp/src/config.rs delete mode 100644 crates/lsp/src/crates.rs delete mode 100644 crates/lsp/src/documents.rs delete mode 100644 crates/lsp/src/encoding.rs delete mode 100644 crates/lsp/src/error.rs delete mode 100644 crates/lsp/src/from_proto.rs delete mode 100644 crates/lsp/src/handlers.rs delete mode 100644 crates/lsp/src/handlers_ext.rs delete mode 100644 crates/lsp/src/handlers_format.rs delete mode 100644 crates/lsp/src/handlers_state.rs delete mode 100644 crates/lsp/src/lib.rs delete mode 100644 crates/lsp/src/logging.rs delete mode 100644 crates/lsp/src/main_loop.rs delete mode 100644 crates/lsp/src/rust_analyzer/diff.rs delete mode 100644 crates/lsp/src/rust_analyzer/line_index.rs delete mode 100644 crates/lsp/src/rust_analyzer/mod.rs delete mode 100644 crates/lsp/src/rust_analyzer/text_edit.rs delete mode 100644 crates/lsp/src/rust_analyzer/to_proto.rs delete mode 100644 crates/lsp/src/rust_analyzer/utils.rs delete mode 100644 crates/lsp/src/snapshots/lsp__documents__tests__document_syntax-2.snap delete mode 100644 crates/lsp/src/snapshots/lsp__documents__tests__document_syntax.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-2.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-3.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-5.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_mismatched_indent.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines-2.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-2.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-3.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-5.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-6.snap delete mode 100644 crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap delete mode 100644 crates/lsp/src/state.rs delete mode 100644 crates/lsp/src/test_utils.rs delete mode 100644 crates/lsp/src/to_proto.rs delete mode 100644 crates/lsp/src/tower_lsp.rs delete mode 100644 crates/lsp/src/tower_lsp_test_client.rs delete mode 100644 crates/lsp/src/workspaces.rs delete mode 100644 crates/lsp_test/Cargo.toml delete mode 100644 crates/lsp_test/src/lib.rs delete mode 100644 crates/lsp_test/src/lsp_client.rs delete mode 100644 crates/lsp_test/src/tower_lsp/codec.rs delete mode 100644 crates/lsp_test/src/tower_lsp/mod.rs delete mode 100644 crates/lsp_test/src/tower_lsp/request.rs diff --git a/Cargo.lock b/Cargo.lock index 6ec2d28f..55efb6f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,28 +179,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" -[[package]] -name = "async-trait" -version = "0.1.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "auto_impl" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - [[package]] name = "autocfg" version = "1.4.0" @@ -369,17 +347,6 @@ dependencies = [ "serde", ] -[[package]] -name = "biome_lsp_converters" -version = "0.1.0" -source = "git+https://github.com/biomejs/biome?rev=2648fa4201be4afd26f44eca1a4e77aac0a67272#2648fa4201be4afd26f44eca1a4e77aac0a67272" -dependencies = [ - "anyhow", - "biome_rowan", - "rustc-hash", - "tower-lsp 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "biome_markup" version = "0.5.7" @@ -494,12 +461,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bytes" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" - [[package]] name = "camino" version = "1.1.9" @@ -685,19 +646,6 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -836,95 +784,6 @@ dependencies = [ "path-absolutize", ] -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -997,18 +856,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "httparse" -version = "1.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" - [[package]] name = "icu_collections" version = "1.5.0" @@ -1270,13 +1117,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "line_ending" -version = "0.0.0" -dependencies = [ - "memchr", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1311,52 +1151,6 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -[[package]] -name = "lsp" -version = "0.0.0" -dependencies = [ - "air_r_factory", - "air_r_formatter", - "air_r_parser", - "air_r_syntax", - "anyhow", - "assert_matches", - "biome_formatter", - "biome_lsp_converters", - "biome_parser", - "biome_rowan", - "biome_text_size", - "bytes", - "cargo_metadata", - "crossbeam", - "dissimilar", - "futures", - "futures-util", - "httparse", - "insta", - "itertools", - "line_ending", - "lsp_test", - "memchr", - "serde", - "serde_json", - "struct-field-names-as-array", - "strum", - "tests_macros", - "time", - "tokio", - "tokio-util", - "tower-lsp 0.20.0 (git+https://github.com/lionel-/tower-lsp?branch=bugfix%2Fpatches)", - "tracing", - "tracing-subscriber", - "tree-sitter", - "tree-sitter-r", - "triomphe", - "url", - "uuid", - "workspace", -] - [[package]] name = "lsp-server" version = "0.7.8" @@ -1370,19 +1164,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "lsp-types" -version = "0.94.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" -dependencies = [ - "bitflags 1.3.2", - "serde", - "serde_json", - "serde_repr", - "url", -] - [[package]] name = "lsp-types" version = "0.95.1" @@ -1396,24 +1177,6 @@ dependencies = [ "url", ] -[[package]] -name = "lsp_test" -version = "0.0.0" -dependencies = [ - "bytes", - "futures", - "futures-util", - "httparse", - "memchr", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tower-lsp 0.20.0 (git+https://github.com/lionel-/tower-lsp?branch=bugfix%2Fpatches)", - "tracing", - "url", -] - [[package]] name = "memchr" version = "2.7.4" @@ -1429,18 +1192,6 @@ dependencies = [ "adler2", ] -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi", - "libc", - "wasi", - "windows-sys 0.52.0", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1494,7 +1245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20bb345f290c46058ba650fef7ca2b579612cf2786b927ebad7b8bec0845a7" dependencies = [ "cfg-if", - "dashmap 6.1.0", + "dashmap", "dunce", "indexmap", "json-strip-comments", @@ -1507,16 +1258,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - [[package]] name = "parking_lot_core" version = "0.9.10" @@ -1554,38 +1295,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pin-project" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - [[package]] name = "pin-project-lite" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.31" @@ -1721,7 +1436,7 @@ dependencies = [ "jod-thread", "libc", "lsp-server", - "lsp-types 0.95.1", + "lsp-types", "regex", "ruff_source_file", "rustc-hash", @@ -1804,12 +1519,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "rustversion" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" - [[package]] name = "ryu" version = "1.0.18" @@ -1946,7 +1655,7 @@ name = "server_test" version = "0.0.0" dependencies = [ "lsp-server", - "lsp-types 0.95.1", + "lsp-types", "url", ] @@ -1965,15 +1674,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "simdutf8" version = "0.1.5" @@ -2000,31 +1700,12 @@ dependencies = [ "similar", ] -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "spin" version = "0.9.8" @@ -2043,48 +1724,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "struct-field-names-as-array" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ba4bae771f9cc992c4f403636c54d2ef13acde6367583e99d06bb336674dd9" -dependencies = [ - "struct-field-names-as-array-derive", -] - -[[package]] -name = "struct-field-names-as-array-derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2dbf8b57f3ce20e4bb171a11822b283bdfab6c4bb0fe64fa729f045f23a0938" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.90", -] - [[package]] name = "subtle" version = "2.6.1" @@ -2262,48 +1901,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tokio" -version = "1.41.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "tokio-util" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - [[package]] name = "toml" version = "0.8.19" @@ -2338,98 +1935,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-lsp" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" -dependencies = [ - "async-trait", - "auto_impl", - "bytes", - "dashmap 5.5.3", - "futures", - "httparse", - "lsp-types 0.94.1", - "memchr", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tower", - "tower-lsp-macros 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tracing", -] - -[[package]] -name = "tower-lsp" -version = "0.20.0" -source = "git+https://github.com/lionel-/tower-lsp?branch=bugfix%2Fpatches#49ef549eaa9f74b71e212cf513283af4ad748a81" -dependencies = [ - "async-trait", - "auto_impl", - "bytes", - "dashmap 5.5.3", - "futures", - "httparse", - "lsp-types 0.94.1", - "memchr", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tower", - "tower-lsp-macros 0.9.0 (git+https://github.com/lionel-/tower-lsp?branch=bugfix%2Fpatches)", - "tracing", -] - -[[package]] -name = "tower-lsp-macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "tower-lsp-macros" -version = "0.9.0" -source = "git+https://github.com/lionel-/tower-lsp?branch=bugfix%2Fpatches#49ef549eaa9f74b71e212cf513283af4ad748a81" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.40" @@ -2515,16 +2020,6 @@ dependencies = [ "tree-sitter-language", ] -[[package]] -name = "triomphe" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" -dependencies = [ - "serde", - "stable_deref_trait", -] - [[package]] name = "unicode-bom" version = "2.0.3" diff --git a/Cargo.toml b/Cargo.toml index a05bfb8f..2ae6bbcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,6 @@ air_r_parser = { path = "./crates/air_r_parser" } air_r_syntax = { path = "./crates/air_r_syntax" } biome_ungrammar = { path = "./crates/biome_ungrammar" } fs = { path = "./crates/fs" } -line_ending = { path = "./crates/line_ending" } -lsp_test = { path = "./crates/lsp_test" } tests_macros = { path = "./crates/tests_macros" } ruff_server = { path = "./crates/ruff_server" } ruff_source_file = { path = "./crates/ruff_source_file" } diff --git a/crates/line_ending/Cargo.toml b/crates/line_ending/Cargo.toml deleted file mode 100644 index b29eeb6e..00000000 --- a/crates/line_ending/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "line_ending" -description = "Utilities for normalizing line endings" -version = "0.0.0" -publish = false -authors.workspace = true -categories.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -memchr.workspace = true - -[dev-dependencies] - -[lints] -workspace = true diff --git a/crates/line_ending/src/lib.rs b/crates/line_ending/src/lib.rs deleted file mode 100644 index af3de4eb..00000000 --- a/crates/line_ending/src/lib.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! We maintain the invariant that all internal strings use `\n` as line separator. -//! This module does line ending conversion and detection (so that we can -//! convert back to `\r\n` on the way out as needed). - -use std::sync::LazyLock; - -use memchr::memmem; - -static FINDER: LazyLock = LazyLock::new(|| memmem::Finder::new(b"\r\n")); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum LineEnding { - /// Line Feed only (\n), common on Linux and macOS as well as inside git repos - Lf, - - /// Carriage Return + Line Feed characters (\r\n), common on Windows - Crlf, -} - -pub fn infer(x: &str) -> LineEnding { - match FINDER.find(x.as_bytes()) { - // Saw `\r\n` - Some(_) => LineEnding::Crlf, - // No `\r\n`, or empty file - None => LineEnding::Lf, - } -} - -/// Normalize line endings within a string -/// -/// We replace `\r\n` with `\n` in-place, which doesn't break utf-8 encoding. -/// While we *can* call `as_mut_vec` and do surgery on the live string -/// directly, let's rather steal the contents of `x`. This makes the code -/// safe even if a panic occurs. -/// -/// # Source -/// -/// --- -/// authors = ["rust-analyzer team"] -/// license = "MIT OR Apache-2.0" -/// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/line_index.rs" -/// --- -pub fn normalize(x: String) -> String { - let mut buf = x.into_bytes(); - let mut gap_len = 0; - let mut tail = buf.as_mut_slice(); - let mut crlf_seen = false; - - loop { - let idx = match FINDER.find(&tail[gap_len..]) { - None if crlf_seen => tail.len(), - // SAFETY: buf is unchanged and therefore still contains utf8 data - None => return unsafe { String::from_utf8_unchecked(buf) }, - Some(idx) => { - crlf_seen = true; - idx + gap_len - } - }; - tail.copy_within(gap_len..idx, 0); - tail = &mut tail[idx - gap_len..]; - if tail.len() == gap_len { - break; - } - gap_len += 1; - } - - // Account for removed `\r`. - // After `set_len`, `buf` is guaranteed to contain utf-8 again. - unsafe { - let new_len = buf.len() - gap_len; - buf.set_len(new_len); - String::from_utf8_unchecked(buf) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn unix() { - let src = "a\nb\nc\n\n\n\n"; - assert_eq!(infer(src), LineEnding::Lf); - assert_eq!(normalize(src.to_string()), src); - } - - #[test] - fn dos() { - let src = "\r\na\r\n\r\nb\r\nc\r\n\r\n\r\n\r\n"; - assert_eq!(infer(src), LineEnding::Crlf); - assert_eq!(normalize(src.to_string()), "\na\n\nb\nc\n\n\n\n"); - } - - #[test] - fn mixed() { - let src = "a\r\nb\r\nc\r\n\n\r\n\n"; - assert_eq!(infer(src), LineEnding::Crlf); - assert_eq!(normalize(src.to_string()), "a\nb\nc\n\n\n\n"); - } - - #[test] - fn none() { - let src = "abc"; - assert_eq!(infer(src), LineEnding::Lf); - assert_eq!(normalize(src.to_string()), src); - } -} diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml deleted file mode 100644 index 6a356eb2..00000000 --- a/crates/lsp/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "lsp" -version = "0.0.0" -publish = false -authors.workspace = true -categories.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true - -[dependencies] -air_r_factory.workspace = true -air_r_formatter.workspace = true -air_r_parser.workspace = true -air_r_syntax.workspace = true -anyhow.workspace = true -biome_formatter.workspace = true -biome_lsp_converters.workspace = true -biome_parser.workspace = true -biome_rowan.workspace = true -biome_text_size.workspace = true -crossbeam.workspace = true -dissimilar.workspace = true -futures.workspace = true -itertools.workspace = true -line_ending.workspace = true -memchr.workspace = true -serde.workspace = true -serde_json.workspace = true -struct-field-names-as-array.workspace = true -strum = { workspace = true, features = ["derive"] } -time = { workspace = true } -tokio = { workspace = true, features = ["full"] } -tower-lsp.workspace = true -tracing.workspace = true -tracing-subscriber = { workspace = true, features = ["ansi", "local-time"] } -tree-sitter.workspace = true -tree-sitter-r.workspace = true -triomphe.workspace = true -url.workspace = true -uuid = { workspace = true, features = ["v4"] } -workspace = { workspace = true } - -[dev-dependencies] -assert_matches.workspace = true -bytes.workspace = true -futures-util.workspace = true -httparse.workspace = true -insta.workspace = true -lsp_test.workspace = true -memchr.workspace = true -tests_macros.workspace = true -tokio-util.workspace = true - -[build-dependencies] -cargo_metadata.workspace = true - -[lints] -workspace = true diff --git a/crates/lsp/build.rs b/crates/lsp/build.rs deleted file mode 100644 index 17cde13a..00000000 --- a/crates/lsp/build.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::env; -use std::fs; -use std::path::Path; - -extern crate cargo_metadata; - -fn main() { - write_workspace_crate_names(); -} - -/// Write out a constant array of air crate names as `AIR_CRATE_NAMES` at build time -fn write_workspace_crate_names() { - let dir = env::var_os("OUT_DIR").unwrap(); - let path = Path::new(&dir).join("crates.rs"); - - // Equivalent to `cargo metadata --no-deps` - let mut cmd = cargo_metadata::MetadataCommand::new(); - cmd.no_deps(); - let metadata = cmd.exec().unwrap(); - - let packages: Vec = metadata - .workspace_packages() - .iter() - .map(|package| package.name.clone()) - .map(|package| String::from("\"") + package.as_str() + "\",") - .collect(); - - let packages = packages.join(" "); - - let contents = format!("pub(crate) const AIR_CRATE_NAMES: &[&str] = &[{packages}];"); - - fs::write(&path, contents).unwrap(); - println!("cargo::rerun-if-changed=build.rs"); -} diff --git a/crates/lsp/src/capabilities.rs b/crates/lsp/src/capabilities.rs deleted file mode 100644 index 9b46941f..00000000 --- a/crates/lsp/src/capabilities.rs +++ /dev/null @@ -1,40 +0,0 @@ -use tower_lsp::lsp_types::ClientCapabilities; -use tower_lsp::lsp_types::PositionEncodingKind; - -/// A resolved representation of the [ClientCapabilities] the Client sends over that we -/// actually do something with -#[derive(Debug, Default)] -pub(crate) struct ResolvedClientCapabilities { - pub(crate) position_encodings: Vec, - pub(crate) dynamic_registration_for_did_change_configuration: bool, - pub(crate) dynamic_registration_for_did_change_watched_files: bool, -} - -impl ResolvedClientCapabilities { - pub(crate) fn new(capabilities: ClientCapabilities) -> Self { - let position_encodings = capabilities - .general - .and_then(|general_client_capabilities| general_client_capabilities.position_encodings) - .unwrap_or(vec![PositionEncodingKind::UTF16]); - - let dynamic_registration_for_did_change_configuration = capabilities - .workspace - .as_ref() - .and_then(|workspace| workspace.did_change_configuration) - .and_then(|did_change_configuration| did_change_configuration.dynamic_registration) - .unwrap_or(false); - - let dynamic_registration_for_did_change_watched_files = capabilities - .workspace - .as_ref() - .and_then(|workspace| workspace.did_change_watched_files) - .and_then(|watched_files| watched_files.dynamic_registration) - .unwrap_or_default(); - - Self { - position_encodings, - dynamic_registration_for_did_change_configuration, - dynamic_registration_for_did_change_watched_files, - } - } -} diff --git a/crates/lsp/src/config.rs b/crates/lsp/src/config.rs deleted file mode 100644 index 3c2fa069..00000000 --- a/crates/lsp/src/config.rs +++ /dev/null @@ -1,129 +0,0 @@ -// -// config.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -use serde::Deserialize; -use serde::Serialize; -use struct_field_names_as_array::FieldNamesAsArray; - -/// Configuration of the LSP -#[derive(Clone, Debug, Default)] -pub(crate) struct LspConfig {} - -/// Configuration of a document. -/// -/// The naming follows where possible. -#[derive(Serialize, Deserialize, Clone, Debug, Default)] -pub struct DocumentConfig { - pub indent: IndentationConfig, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct IndentationConfig { - /// Whether to insert spaces of tabs for one level of indentation. - pub indent_style: IndentStyle, - - /// The number of spaces for one level of indentation. - pub indent_size: usize, - - /// The width of a tab. There may be projects with an `indent_size` of 4 and - /// a `tab_width` of 8 (e.g. GNU R). - pub tab_width: usize, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub enum IndentStyle { - Tab, - Space, -} - -/// VS Code representation of a document configuration -#[derive(Serialize, Deserialize, FieldNamesAsArray, Clone, Debug)] -pub(crate) struct VscDocumentConfig { - // DEV NOTE: Update `section_from_key()` method after adding a field - pub insert_spaces: bool, - pub indent_size: VscIndentSize, - pub tab_size: usize, -} - -#[derive(Serialize, Deserialize, FieldNamesAsArray, Clone, Debug)] -pub(crate) struct VscDiagnosticsConfig { - // DEV NOTE: Update `section_from_key()` method after adding a field - pub enable: bool, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(untagged)] -pub(crate) enum VscIndentSize { - Alias(String), - Size(usize), -} - -impl Default for IndentationConfig { - fn default() -> Self { - Self { - indent_style: IndentStyle::Space, - indent_size: 2, - tab_width: 2, - } - } -} - -impl VscDocumentConfig { - pub(crate) fn section_from_key(key: &str) -> &str { - match key { - "insert_spaces" => "editor.insertSpaces", - "indent_size" => "editor.indentSize", - "tab_size" => "editor.tabSize", - _ => "unknown", // To be caught via downstream errors - } - } -} - -/// Convert from VS Code representation of a document config to our own -/// representation. Currently one-to-one. -impl From for DocumentConfig { - fn from(x: VscDocumentConfig) -> Self { - let indent_style = indent_style_from_lsp(x.insert_spaces); - - let indent_size = match x.indent_size { - VscIndentSize::Size(size) => size, - VscIndentSize::Alias(var) => { - if var == "tabSize" { - x.tab_size - } else { - tracing::warn!("Unknown indent alias {var}, using default"); - 2 - } - } - }; - - Self { - indent: IndentationConfig { - indent_style, - indent_size, - tab_width: x.tab_size, - }, - } - } -} - -impl VscDiagnosticsConfig { - pub(crate) fn section_from_key(key: &str) -> &str { - match key { - "enable" => "positron.r.diagnostics.enable", - _ => "unknown", // To be caught via downstream errors - } - } -} - -pub(crate) fn indent_style_from_lsp(insert_spaces: bool) -> IndentStyle { - if insert_spaces { - IndentStyle::Space - } else { - IndentStyle::Tab - } -} diff --git a/crates/lsp/src/crates.rs b/crates/lsp/src/crates.rs deleted file mode 100644 index e79951a1..00000000 --- a/crates/lsp/src/crates.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Generates `AIR_CRATE_NAMES`, a const array of the crate names in the air workspace, -// see `lsp/src/build.rs` -include!(concat!(env!("OUT_DIR"), "/crates.rs")); diff --git a/crates/lsp/src/documents.rs b/crates/lsp/src/documents.rs deleted file mode 100644 index d2406dae..00000000 --- a/crates/lsp/src/documents.rs +++ /dev/null @@ -1,250 +0,0 @@ -// -// documents.rs -// -// Copyright (C) 2022-2024 Posit Software, PBC. All rights reserved. -// -// - -use biome_lsp_converters::{line_index, PositionEncoding}; -use line_ending::LineEnding; -use tower_lsp::lsp_types; - -use crate::config::DocumentConfig; -use crate::rust_analyzer::line_index::LineIndex; -use crate::rust_analyzer::utils::apply_document_changes; - -#[derive(Clone)] -pub struct Document { - /// The normalized current contents of the document. UTF-8 Rust string with - /// Unix line endings. - pub contents: String, - - /// Map of new lines in `contents`. Also contains line endings type in the - /// original document (we only store Unix lines) and the position encoding - /// type of the session. This provides all that is needed to send data back - /// to the client with positions in the correct coordinate space and - /// correctly formatted text. - pub line_index: LineIndex, - - /// We store the syntax tree in the document for now. - /// We will think about laziness and incrementality in the future. - pub parse: biome_parser::AnyParse, - - /// The version of the document we last synchronized with. - /// None if the document hasn't been synchronized yet. - pub version: Option, - - /// Configuration of the document, such as indentation settings. - pub config: DocumentConfig, -} - -impl std::fmt::Debug for Document { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Document") - .field("syntax", &self.parse) - .finish() - } -} - -impl Document { - pub fn new( - contents: String, - version: Option, - position_encoding: PositionEncoding, - ) -> Self { - // Detect existing endings - let endings = line_ending::infer(&contents); - - // Normalize to Unix line endings - let contents = match endings { - LineEnding::Lf => contents, - LineEnding::Crlf => line_ending::normalize(contents), - }; - - // TODO: Handle user requested line ending preference here - // by potentially overwriting `endings` if the user didn't - // select `LineEndings::Auto`, and then pass that to `LineIndex`. - - // Create line index to keep track of newline offsets - let line_index = LineIndex { - index: triomphe::Arc::new(line_index::LineIndex::new(&contents)), - endings, - encoding: position_encoding, - }; - - // Parse document immediately for now - let parse = air_r_parser::parse(&contents, Default::default()).into(); - - Self { - contents, - line_index, - parse, - version, - config: Default::default(), - } - } - - /// For unit tests - pub fn doodle(contents: &str) -> Self { - Self::new(contents.into(), None, PositionEncoding::Utf8) - } - - #[cfg(test)] - pub fn doodle_and_range(contents: &str) -> (Self, biome_text_size::TextRange) { - let (contents, range) = crate::test_utils::extract_marked_range(contents); - let doc = Self::new(contents, None, PositionEncoding::Utf8); - (doc, range) - } - - pub fn on_did_change(&mut self, mut params: lsp_types::DidChangeTextDocumentParams) { - let new_version = params.text_document.version; - - // Check for out-of-order change notifications - if let Some(old_version) = self.version { - // According to the spec, versions might not be consecutive but they must be monotonically - // increasing. If that's not the case this is a hard nope as we - // can't maintain our state integrity. Currently panicking but in - // principle we should shut down the LSP in an orderly fashion. - if new_version < old_version { - panic!( - "out-of-sync change notification: currently at {old_version}, got {new_version}" - ); - } - } - - // Normalize line endings. Changing the line length of inserted or - // replaced text can't invalidate the text change events, even those - // applied subsequently, since those changes are specified with [line, - // col] coordinates. - for event in &mut params.content_changes { - let text = std::mem::take(&mut event.text); - event.text = line_ending::normalize(text); - } - - let contents = apply_document_changes( - self.line_index.encoding, - &self.contents, - params.content_changes, - ); - - // No incrementality for now - let parse = air_r_parser::parse(&contents, Default::default()).into(); - - self.parse = parse; - self.contents = contents; - self.line_index.index = triomphe::Arc::new(line_index::LineIndex::new(&self.contents)); - self.version = Some(new_version); - } - - /// Convenient accessor that returns an annotated `SyntaxNode` type - pub fn syntax(&self) -> air_r_syntax::RSyntaxNode { - self.parse.syntax() - } -} - -#[cfg(test)] -mod tests { - use air_r_syntax::RSyntaxNode; - use biome_text_size::{TextRange, TextSize}; - - use crate::rust_analyzer::text_edit::TextEdit; - use crate::to_proto; - - use super::*; - - fn dummy_versioned_doc() -> lsp_types::VersionedTextDocumentIdentifier { - lsp_types::VersionedTextDocumentIdentifier { - uri: url::Url::parse("file:///foo").unwrap(), - version: 1, - } - } - - #[test] - fn test_document_starts_at_0_with_leading_whitespace() { - let document = Document::doodle("\n\n# hi there"); - let root = document.syntax(); - assert_eq!( - root.text_range(), - TextRange::new(TextSize::from(0), TextSize::from(12)) - ); - } - - #[test] - fn test_document_syntax() { - let mut doc = Document::doodle("foo(bar)"); - - let original_syntax: RSyntaxNode = doc.parse.syntax(); - insta::assert_debug_snapshot!(original_syntax); - - let edit = TextEdit::replace( - TextRange::new(TextSize::from(4_u32), TextSize::from(7)), - String::from("1 + 2"), - ); - let edits = to_proto::doc_edit_vec(&doc.line_index, edit).unwrap(); - - let params = lsp_types::DidChangeTextDocumentParams { - text_document: dummy_versioned_doc(), - content_changes: edits, - }; - doc.on_did_change(params); - - let updated_syntax: RSyntaxNode = doc.parse.syntax(); - insta::assert_debug_snapshot!(updated_syntax); - } - - #[test] - fn test_document_position_encoding() { - // Replace `b` after `𐐀` which is at position 5 in UTF-8 - let utf8_range = lsp_types::Range { - start: lsp_types::Position { - line: 0, - character: 5, - }, - end: lsp_types::Position { - line: 0, - character: 6, - }, - }; - - // `b` is at position 3 in UTF-16 - let utf16_range = lsp_types::Range { - start: lsp_types::Position { - line: 0, - character: 3, - }, - end: lsp_types::Position { - line: 0, - character: 4, - }, - }; - - let mut utf8_replace_params = lsp_types::DidChangeTextDocumentParams { - text_document: dummy_versioned_doc(), - content_changes: vec![], - }; - let mut utf16_replace_params = utf8_replace_params.clone(); - - utf8_replace_params.content_changes = vec![lsp_types::TextDocumentContentChangeEvent { - range: Some(utf8_range), - range_length: None, - text: String::from("bar"), - }]; - utf16_replace_params.content_changes = vec![lsp_types::TextDocumentContentChangeEvent { - range: Some(utf16_range), - range_length: None, - text: String::from("bar"), - }]; - - let mut document = Document::new("a𐐀b".into(), None, PositionEncoding::Utf8); - document.on_did_change(utf8_replace_params); - assert_eq!(document.contents, "a𐐀bar"); - - let mut document = Document::new( - "a𐐀b".into(), - None, - PositionEncoding::Wide(biome_lsp_converters::WideEncoding::Utf16), - ); - document.on_did_change(utf16_replace_params); - assert_eq!(document.contents, "a𐐀bar"); - } -} diff --git a/crates/lsp/src/encoding.rs b/crates/lsp/src/encoding.rs deleted file mode 100644 index be8062a6..00000000 --- a/crates/lsp/src/encoding.rs +++ /dev/null @@ -1,61 +0,0 @@ -// -// encoding.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -/// Converts a character offset into a particular line from UTF-16 to UTF-8 -fn convert_character_from_utf16_to_utf8(x: &str, character: usize) -> usize { - if x.is_ascii() { - // Fast pass - return character; - } - - // Initial check, since loop would skip this case - if character == 0 { - return character; - } - - let mut n = 0; - - // For each `u32` sized `char`, figure out the equivalent size in UTF-16 - // world of that `char`. Once we hit the requested number of `character`s, - // that means we have indexed into `x` to the correct position, at which - // point we can take the current bytes based `pos` that marks the start of - // this `char`, and add on its UTF-8 based size to return an adjusted column - // offset. We use `==` because I'm fairly certain they should always align - // exactly, and it would be good to log if that isn't the case. - for (pos, char) in x.char_indices() { - n += char.len_utf16(); - - if n == character { - return pos + char.len_utf8(); - } - } - - tracing::error!("Failed to locate UTF-16 offset of {character}. Line: '{x}'."); - 0 -} - -/// Converts a character offset into a particular line from UTF-8 to UTF-16 -fn convert_character_from_utf8_to_utf16(x: &str, character: usize) -> usize { - if x.is_ascii() { - // Fast pass - return character; - } - - // The UTF-8 -> UTF-16 case is slightly simpler. We just slice into `x` - // using our existing UTF-8 offset, reencode the slice as a UTF-16 based - // iterator, and count up the pieces. - match x.get(..character) { - Some(x) => x.encode_utf16().count(), - None => { - let n = x.len(); - tracing::error!( - "Tried to take UTF-8 character {character}, but only {n} characters exist. Line: '{x}'." - ); - 0 - } - } -} diff --git a/crates/lsp/src/error.rs b/crates/lsp/src/error.rs deleted file mode 100644 index fe19c650..00000000 --- a/crates/lsp/src/error.rs +++ /dev/null @@ -1,52 +0,0 @@ -/// A tool for collecting multiple anyhow errors into a single [`anyhow::Result`] -/// -/// Only applicable if the intended `Ok()` value at the end is `()`. -#[derive(Debug, Default)] -pub(crate) struct ErrorVec { - errors: Vec, -} - -impl ErrorVec { - pub(crate) fn new() -> Self { - Self::default() - } - - /// Conditionally push to the error vector if the `result` is an `Err` case - pub(crate) fn push_err(&mut self, result: anyhow::Result) { - match result { - Ok(_) => (), - Err(error) => self.push(error), - } - } - - /// Push a new error to the error vector - pub(crate) fn push(&mut self, error: anyhow::Error) { - self.errors.push(error); - } - - /// Convert a error vector into a single [`anyhow::Result`] that knows how to print - /// each of the individual errors - pub(crate) fn into_result(self) -> anyhow::Result<()> { - if self.errors.is_empty() { - Ok(()) - } else { - Err(anyhow::anyhow!(self)) - } - } -} - -impl std::error::Error for ErrorVec {} - -impl std::fmt::Display for ErrorVec { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.errors.len() > 1 { - f.write_str("Multiple errors:\n")?; - } - - for error in &self.errors { - std::fmt::Display::fmt(error, f)?; - } - - Ok(()) - } -} diff --git a/crates/lsp/src/from_proto.rs b/crates/lsp/src/from_proto.rs deleted file mode 100644 index a3329d30..00000000 --- a/crates/lsp/src/from_proto.rs +++ /dev/null @@ -1,37 +0,0 @@ -pub(crate) use biome_lsp_converters::from_proto::offset; -pub(crate) use biome_lsp_converters::from_proto::text_range; - -use tower_lsp::lsp_types; - -use crate::documents::Document; - -pub fn apply_text_edits( - doc: &Document, - mut edits: Vec, -) -> anyhow::Result { - let mut text = doc.contents.clone(); - - // Apply edits from bottom to top to avoid inserted newlines to invalidate - // positions in earlier parts of the doc (they are sent in reading order - // accorder to the LSP protocol) - edits.reverse(); - - for edit in edits { - let start: usize = offset( - &doc.line_index.index, - edit.range.start, - doc.line_index.encoding, - )? - .into(); - let end: usize = offset( - &doc.line_index.index, - edit.range.end, - doc.line_index.encoding, - )? - .into(); - - text.replace_range(start..end, &edit.new_text); - } - - Ok(text) -} diff --git a/crates/lsp/src/handlers.rs b/crates/lsp/src/handlers.rs deleted file mode 100644 index e7c54902..00000000 --- a/crates/lsp/src/handlers.rs +++ /dev/null @@ -1,95 +0,0 @@ -// -// handlers.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -use struct_field_names_as_array::FieldNamesAsArray; -use tower_lsp::lsp_types; -use tower_lsp::lsp_types::DidChangeWatchedFilesRegistrationOptions; -use tower_lsp::lsp_types::FileSystemWatcher; -use tower_lsp::Client; -use tracing::Instrument; - -use crate::config::VscDiagnosticsConfig; -use crate::config::VscDocumentConfig; -use crate::main_loop::LspState; - -// Handlers that do not mutate the world state. They take a sharing reference or -// a clone of the state. - -pub(crate) async fn handle_initialized( - client: &Client, - lsp_state: &LspState, -) -> anyhow::Result<()> { - let span = tracing::info_span!("handle_initialized").entered(); - - // Register capabilities to the client - let mut registrations: Vec = vec![]; - - if lsp_state - .capabilities - .dynamic_registration_for_did_change_configuration - { - // The `didChangeConfiguration` request instructs the client to send - // a notification when the tracked settings have changed. - // - // Note that some settings, such as editor indentation properties, may be - // changed by extensions or by the user without changing the actual - // underlying setting. Unfortunately we don't receive updates in that case. - let mut config_document_registrations = collect_regs( - VscDocumentConfig::FIELD_NAMES_AS_ARRAY.to_vec(), - VscDocumentConfig::section_from_key, - ); - let mut config_diagnostics_registrations: Vec = collect_regs( - VscDiagnosticsConfig::FIELD_NAMES_AS_ARRAY.to_vec(), - VscDiagnosticsConfig::section_from_key, - ); - - registrations.append(&mut config_document_registrations); - registrations.append(&mut config_diagnostics_registrations); - } - - if lsp_state - .capabilities - .dynamic_registration_for_did_change_watched_files - { - // Watch for changes in `air.toml` files so we can react dynamically - let watch_air_toml_registration = lsp_types::Registration { - id: uuid::Uuid::new_v4().to_string(), - method: "workspace/didChangeWatchedFiles".into(), - register_options: Some( - serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { - watchers: vec![FileSystemWatcher { - glob_pattern: lsp_types::GlobPattern::String("**/air.toml".into()), - kind: None, - }], - }) - .unwrap(), - ), - }; - - registrations.push(watch_air_toml_registration); - } - - client - .register_capability(registrations) - .instrument(span.exit()) - .await?; - Ok(()) -} - -fn collect_regs( - fields: Vec<&str>, - into_section: impl Fn(&str) -> &str, -) -> Vec { - fields - .into_iter() - .map(|field| lsp_types::Registration { - id: uuid::Uuid::new_v4().to_string(), - method: String::from("workspace/didChangeConfiguration"), - register_options: Some(serde_json::json!({ "section": into_section(field) })), - }) - .collect() -} diff --git a/crates/lsp/src/handlers_ext.rs b/crates/lsp/src/handlers_ext.rs deleted file mode 100644 index 45125222..00000000 --- a/crates/lsp/src/handlers_ext.rs +++ /dev/null @@ -1,94 +0,0 @@ -use air_r_formatter::{context::RFormatOptions, format_node}; -use biome_formatter::{IndentStyle, LineWidth}; -use tower_lsp::lsp_types; - -use crate::state::WorldState; - -#[derive(Debug, Eq, PartialEq, Clone, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct ViewFileParams { - /// From `lsp_types::TextDocumentPositionParams` - pub(crate) text_document: lsp_types::TextDocumentIdentifier, - pub(crate) position: lsp_types::Position, - - /// Viewer type - pub(crate) kind: ViewFileKind, -} - -#[derive(Debug, Eq, PartialEq, Clone, serde::Deserialize, serde::Serialize)] -pub(crate) enum ViewFileKind { - TreeSitter, - SyntaxTree, - FormatTree, -} - -pub(crate) fn view_file(params: ViewFileParams, state: &WorldState) -> anyhow::Result { - let doc = state.get_document(¶ms.text_document.uri)?; - - match params.kind { - ViewFileKind::TreeSitter => { - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_r::LANGUAGE.into()) - .unwrap(); - - let ast = parser.parse(&doc.contents, None).unwrap(); - - if ast.root_node().has_error() { - return Ok(String::from("*Parse error*")); - } - - let mut output = String::new(); - let mut cursor = ast.root_node().walk(); - format_ts_node(&mut cursor, 0, &mut output); - - Ok(output) - } - - ViewFileKind::SyntaxTree => { - let syntax = doc.syntax(); - Ok(format!("{syntax:#?}")) - } - - ViewFileKind::FormatTree => { - let line_width = LineWidth::try_from(80).map_err(|err| anyhow::anyhow!("{err}"))?; - - let options = RFormatOptions::default() - .with_indent_style(IndentStyle::Space) - .with_line_width(line_width); - - let formatted = format_node(options.clone(), &doc.parse.syntax())?; - Ok(format!("{}", formatted.into_document())) - } - } -} - -fn format_ts_node(cursor: &mut tree_sitter::TreeCursor, depth: usize, output: &mut String) { - let node = cursor.node(); - let field_name = match cursor.field_name() { - Some(name) => format!("{name}: "), - None => String::new(), - }; - - let start = node.start_position(); - let end = node.end_position(); - let node_type = node.kind(); - - let indent = " ".repeat(depth * 4); - let start = format!("{}, {}", start.row, start.column); - let end = format!("{}, {}", end.row, end.column); - - output.push_str(&format!( - "{indent}{field_name}{node_type} [{start}] - [{end}]\n", - )); - - if cursor.goto_first_child() { - loop { - format_ts_node(cursor, depth + 1, output); - if !cursor.goto_next_sibling() { - break; - } - } - cursor.goto_parent(); - } -} diff --git a/crates/lsp/src/handlers_format.rs b/crates/lsp/src/handlers_format.rs deleted file mode 100644 index 3a635843..00000000 --- a/crates/lsp/src/handlers_format.rs +++ /dev/null @@ -1,509 +0,0 @@ -// -// handlers_format.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -use air_r_formatter::format_node; -use air_r_syntax::{RExpressionList, RSyntaxKind, RSyntaxNode, WalkEvent}; -use biome_rowan::{AstNode, Language, SyntaxElement}; -use biome_text_size::{TextRange, TextSize}; -use tower_lsp::lsp_types; - -use crate::main_loop::LspState; -use crate::state::WorldState; -use crate::{from_proto, to_proto}; - -#[tracing::instrument(level = "info", skip_all)] -pub(crate) fn document_formatting( - params: lsp_types::DocumentFormattingParams, - lsp_state: &LspState, - state: &WorldState, -) -> anyhow::Result>> { - let doc = state.get_document(¶ms.text_document.uri)?; - - let settings = lsp_state.document_settings(¶ms.text_document.uri); - let format_options = settings.format.to_format_options(&doc.contents); - - if doc.parse.has_errors() { - return Err(anyhow::anyhow!("Can't format when there are parse errors.")); - } - - let formatted = format_node(format_options, &doc.parse.syntax())?; - let output = formatted.print()?.into_code(); - - // Do we need to check that `doc` is indeed an R file? What about special - // files that don't have extensions like `NAMESPACE`, do we hard-code a - // list? What about unnamed temporary files? - - let edits = to_proto::replace_all_edit(&doc.line_index, &doc.contents, &output)?; - Ok(Some(edits)) -} - -#[tracing::instrument(level = "info", skip_all)] -pub(crate) fn document_range_formatting( - params: lsp_types::DocumentRangeFormattingParams, - lsp_state: &LspState, - state: &WorldState, -) -> anyhow::Result>> { - let doc = state.get_document(¶ms.text_document.uri)?; - - let range = - from_proto::text_range(&doc.line_index.index, params.range, doc.line_index.encoding)?; - - let settings = lsp_state.document_settings(¶ms.text_document.uri); - let format_options = settings.format.to_format_options(&doc.contents); - - let logical_lines = find_deepest_enclosing_logical_lines(doc.parse.syntax(), range); - if logical_lines.is_empty() { - tracing::warn!("Can't find logical line"); - return Ok(None); - }; - - // Find the overall formatting range by concatenating the ranges of the logical lines. - // We use the "non-whitespace-range" as that corresponds to what Biome will format. - let format_range = logical_lines - .iter() - .map(text_non_whitespace_range) - .reduce(|acc, new| acc.cover(new)) - .expect("`logical_lines` is non-empty"); - - // We need to wrap in an `RRoot` otherwise the comments get attached too - // deep in the tree. See `CommentsBuilderVisitor` in biome_formatter and the - // `is_root` logic. Note that `node` needs to be wrapped in at least two - // other nodes in order to fix this problem, and here we have an `RRoot` and - // `RExpressionList` that do the job. - // - // Since we only format logical lines, it is fine to wrap in an expression list. - let Some(exprs): Option> = logical_lines - .into_iter() - .map(air_r_syntax::AnyRExpression::cast) - .collect() - else { - tracing::warn!("Can't cast to `AnyRExpression`"); - return Ok(None); - }; - - let list = air_r_factory::r_expression_list(exprs); - let eof = air_r_syntax::RSyntaxToken::new_detached(RSyntaxKind::EOF, "", vec![], vec![]); - let root = air_r_factory::r_root(list, eof).build(); - - let format_info = biome_formatter::format_sub_tree( - root.syntax(), - air_r_formatter::RFormatLanguage::new(format_options), - )?; - - if format_info.range().is_none() { - // Happens in edge cases when biome returns a `Printed::new_empty()` - return Ok(None); - }; - - let mut format_text = format_info.into_code(); - - // Remove last hard break line from our artifical expression list - format_text.pop(); - let edits = to_proto::replace_range_edit(&doc.line_index, format_range, format_text)?; - - Ok(Some(edits)) -} - -// From biome_formatter -fn text_non_whitespace_range(elem: &E) -> TextRange -where - E: Into> + Clone, - L: Language, -{ - let elem: SyntaxElement = elem.clone().into(); - - let start = elem - .leading_trivia() - .into_iter() - .flat_map(|trivia| trivia.pieces()) - .find_map(|piece| { - if piece.is_whitespace() || piece.is_newline() { - None - } else { - Some(piece.text_range().start()) - } - }) - .unwrap_or_else(|| elem.text_trimmed_range().start()); - - let end = elem - .trailing_trivia() - .into_iter() - .flat_map(|trivia| trivia.pieces().rev()) - .find_map(|piece| { - if piece.is_whitespace() || piece.is_newline() { - None - } else { - Some(piece.text_range().end()) - } - }) - .unwrap_or_else(|| elem.text_trimmed_range().end()); - - TextRange::new(start, end) -} - -/// Finds consecutive logical lines. Currently that's only expressions at -/// top-level or in a braced list. -fn find_deepest_enclosing_logical_lines(node: RSyntaxNode, range: TextRange) -> Vec { - let start_lists = find_expression_lists(&node, range.start(), false); - let end_lists = find_expression_lists(&node, range.end(), true); - - // Both vectors of lists should have a common prefix, starting from the - // program's expression list. As soon as the lists diverge we stop. - let Some(list) = start_lists - .into_iter() - .zip(end_lists) - .take_while(|pair| pair.0 == pair.1) - .map(|pair| pair.0) - .last() - else { - // Should not happen as the range is always included in the program's expression list - tracing::warn!("Can't find common list parent"); - return vec![]; - }; - - let Some(list) = RExpressionList::cast(list) else { - tracing::warn!("Can't cast to expression list"); - return vec![]; - }; - - let iter = list.into_iter(); - - // We've chosen to be liberal about user selections and always widen the - // range to include the selection bounds. If we wanted to be conservative - // instead, we could use this `filter()` instead of the `skip_while()` and - // `take_while()`: - // - // ```rust - // .filter(|node| range.contains_range(node.text_trimmed_range())) - // ``` - let logical_lines: Vec = iter - .map(|expr| expr.into_syntax()) - .skip_while(|node| !node.text_range().contains(range.start())) - .take_while(|node| node.text_trimmed_range().start() <= range.end()) - .collect(); - - logical_lines -} - -fn find_expression_lists(node: &RSyntaxNode, offset: TextSize, end: bool) -> Vec { - let mut preorder = node.preorder(); - let mut nodes: Vec = vec![]; - - while let Some(event) = preorder.next() { - match event { - WalkEvent::Enter(node) => { - let Some(parent) = node.parent() else { - continue; - }; - - let is_contained = if end { - let trimmed_node_range = node.text_trimmed_range(); - trimmed_node_range.contains_inclusive(offset) - } else { - let node_range = node.text_range(); - node_range.contains(offset) - }; - - if !is_contained { - preorder.skip_subtree(); - continue; - } - - if parent.kind() == RSyntaxKind::R_EXPRESSION_LIST { - nodes.push(parent.clone()); - continue; - } - } - - WalkEvent::Leave(_) => {} - } - } - - nodes -} - -#[cfg(test)] -mod tests { - use crate::{ - documents::Document, tower_lsp::init_test_client, tower_lsp_test_client::TestClientExt, - }; - - #[tests_macros::lsp_test] - async fn test_format() { - let mut client = init_test_client().await; - - #[rustfmt::skip] - let doc = Document::doodle( -" -1 -2+2 -3 + 3 + -3", - ); - - let formatted = client.format_document(&doc).await; - insta::assert_snapshot!(formatted); - - client - } - - // https://github.com/posit-dev/air/issues/61 - #[tests_macros::lsp_test] - async fn test_format_minimal_diff() { - let mut client = init_test_client().await; - - #[rustfmt::skip] - let doc = Document::doodle( -"1 -2+2 -3 -", - ); - - let edits = client.format_document_edits(&doc).await.unwrap(); - assert!(edits.len() == 1); - - let edit = &edits[0]; - assert_eq!(edit.new_text, " + "); - - client - } - - #[tests_macros::lsp_test] - async fn test_format_range_none() { - let mut client = init_test_client().await; - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"<<>>", - ); - - let output = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"<< ->>", - ); - - let output = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"<<1 ->>", - ); - - let output = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output); - - client - } - - #[tests_macros::lsp_test] - async fn test_format_range_logical_lines() { - let mut client = init_test_client().await; - - // 2+2 is the logical line to format - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"1+1 -<<2+2>> -", - ); - let output = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"1+1 -# -<<2+2>> -", - ); - - let output = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output); - - // The element in the braced expression is a logical line - // FIXME: Should this be the whole `{2+2}` instead? - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"1+1 -{<<2+2>>} -", - ); - - let output = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"1+1 -<<{2+2}>> -", - ); - let output = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output); - - // The deepest element in the braced expression is our target - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"1+1 -{ - 2+2 - { - <<3+3>> - } -} -", - ); - - let output = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output); - client - } - - #[tests_macros::lsp_test] - async fn test_format_range_mismatched_indent() { - let mut client = init_test_client().await; - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"1 - <<2+2>> -", - ); - - // We don't change indentation when `2+2` is formatted - let output = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output); - - // Debatable: Should we make an effort to remove unneeded indentation - // when it's part of the range? - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"1 -<< 2+2>> -", - ); - let output_wide = client.format_document_range(&doc, range).await; - assert_eq!(output, output_wide); - - client - } - - #[tests_macros::lsp_test] - async fn test_format_range_multiple_lines() { - let mut client = init_test_client().await; - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"1+1 -<<# -2+2>> -", - ); - - let output1 = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output1); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"<<1+1 -# -2+2>> -", - ); - let output2 = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output2); - - client - } - - #[tests_macros::lsp_test] - async fn test_format_range_unmatched_lists() { - let mut client = init_test_client().await; - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"0+0 -<<1+1 -{ - 2+2>> -} -3+3 -", - ); - - let output1 = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output1); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"0+0 -<<1+1 -{ ->> 2+2 -} -3+3 -", - ); - let output2 = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output2); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"0+0 -<<1+1 -{ - 2+2 -} ->>3+3 -", - ); - let output3 = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output3); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"0+0 -1+1 -{ -<< 2+2 -} ->>3+3 -", - ); - let output4 = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output4); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"<<1+1>> -2+2 -", - ); - - let output5 = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output5); - - #[rustfmt::skip] - let (doc, range) = Document::doodle_and_range( -"1+1 -<<2+2>> -", - ); - - let output6 = client.format_document_range(&doc, range).await; - insta::assert_snapshot!(output6); - - client - } -} diff --git a/crates/lsp/src/handlers_state.rs b/crates/lsp/src/handlers_state.rs deleted file mode 100644 index 24b1bcdd..00000000 --- a/crates/lsp/src/handlers_state.rs +++ /dev/null @@ -1,348 +0,0 @@ -// -// handlers_state.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -use anyhow::anyhow; -use biome_lsp_converters::PositionEncoding; -use biome_lsp_converters::WideEncoding; -use serde_json::Value; -use struct_field_names_as_array::FieldNamesAsArray; -use tower_lsp::lsp_types; -use tower_lsp::lsp_types::ConfigurationItem; -use tower_lsp::lsp_types::DidChangeConfigurationParams; -use tower_lsp::lsp_types::DidChangeTextDocumentParams; -use tower_lsp::lsp_types::DidChangeWatchedFilesParams; -use tower_lsp::lsp_types::DidChangeWorkspaceFoldersParams; -use tower_lsp::lsp_types::DidCloseTextDocumentParams; -use tower_lsp::lsp_types::DidOpenTextDocumentParams; -use tower_lsp::lsp_types::FormattingOptions; -use tower_lsp::lsp_types::InitializeParams; -use tower_lsp::lsp_types::InitializeResult; -use tower_lsp::lsp_types::OneOf; -use tower_lsp::lsp_types::ServerCapabilities; -use tower_lsp::lsp_types::ServerInfo; -use tower_lsp::lsp_types::TextDocumentSyncCapability; -use tower_lsp::lsp_types::TextDocumentSyncKind; -use tower_lsp::lsp_types::WorkspaceFoldersServerCapabilities; -use tower_lsp::lsp_types::WorkspaceServerCapabilities; -use tracing::Instrument; -use url::Url; -use workspace::settings::Settings; - -use crate::capabilities::ResolvedClientCapabilities; -use crate::config::indent_style_from_lsp; -use crate::config::DocumentConfig; -use crate::config::VscDiagnosticsConfig; -use crate::config::VscDocumentConfig; -use crate::documents::Document; -use crate::error::ErrorVec; -use crate::logging; -use crate::logging::LogMessageSender; -use crate::main_loop::LspState; -use crate::state::workspace_uris; -use crate::state::WorldState; -use crate::workspaces::WorkspaceSettingsResolver; - -// Handlers that mutate the world state - -/// Information sent from the kernel to the LSP after each top-level evaluation. -#[derive(Debug)] -pub struct ConsoleInputs { - /// List of console scopes, from innermost (global or debug) to outermost - /// scope. Currently the scopes are vectors of symbol names. TODO: In the - /// future, we should send structural information like search path, and let - /// the LSP query us for the contents so that the LSP can cache the - /// information. - pub console_scopes: Vec>, - - /// Packages currently installed in the library path. TODO: Should send - /// library paths instead and inspect and cache package information in the LSP. - pub installed_packages: Vec, -} - -// Handlers taking exclusive references to global state - -#[tracing::instrument(level = "info", skip_all)] -pub(crate) fn initialize( - params: InitializeParams, - lsp_state: &mut LspState, - log_tx: LogMessageSender, -) -> anyhow::Result { - // TODO: Get user specified options from `params.initialization_options` - let log_level = None; - let dependency_log_levels = None; - - logging::init_logging( - log_tx, - log_level, - dependency_log_levels, - params.client_info.as_ref(), - ); - - // Initialize the workspace settings resolver using the initial set of client provided `workspace_folders` - lsp_state.workspace_settings_resolver = WorkspaceSettingsResolver::from_workspace_folders( - params.workspace_folders.unwrap_or_default(), - Settings::default(), - ); - - lsp_state.capabilities = ResolvedClientCapabilities::new(params.capabilities); - - // If the client supports UTF-8 we use that, even if it's not its - // preferred encoding (at position 0). Otherwise we use the mandatory - // UTF-16 encoding that all clients and servers must support, even if - // the client would have preferred UTF-32. Note that VSCode and Positron - // only support UTF-16. - let position_encoding = if lsp_state - .capabilities - .position_encodings - .contains(&lsp_types::PositionEncodingKind::UTF8) - { - lsp_state.position_encoding = PositionEncoding::Utf8; - Some(lsp_types::PositionEncodingKind::UTF8) - } else { - lsp_state.position_encoding = PositionEncoding::Wide(WideEncoding::Utf16); - Some(lsp_types::PositionEncodingKind::UTF16) - }; - - Ok(InitializeResult { - server_info: Some(ServerInfo { - name: "Air Language Server".to_string(), - version: Some(env!("CARGO_PKG_VERSION").to_string()), - }), - capabilities: ServerCapabilities { - position_encoding, - text_document_sync: Some(TextDocumentSyncCapability::Kind( - TextDocumentSyncKind::INCREMENTAL, - )), - workspace: Some(WorkspaceServerCapabilities { - workspace_folders: Some(WorkspaceFoldersServerCapabilities { - supported: Some(true), - change_notifications: Some(OneOf::Left(true)), - }), - file_operations: None, - }), - document_formatting_provider: Some(OneOf::Left(true)), - document_range_formatting_provider: Some(OneOf::Left(true)), - ..ServerCapabilities::default() - }, - }) -} - -#[tracing::instrument(level = "info", skip_all)] -pub(crate) fn did_open( - params: DidOpenTextDocumentParams, - lsp_state: &LspState, - state: &mut WorldState, -) -> anyhow::Result<()> { - let contents = params.text_document.text; - let uri = params.text_document.uri; - let version = params.text_document.version; - - let document = Document::new(contents, Some(version), lsp_state.position_encoding); - state.documents.insert(uri, document); - - Ok(()) -} - -#[tracing::instrument(level = "info", skip_all)] -pub(crate) fn did_change( - params: DidChangeTextDocumentParams, - state: &mut WorldState, -) -> anyhow::Result<()> { - let uri = ¶ms.text_document.uri; - let doc = state.get_document_mut(uri)?; - doc.on_did_change(params); - - Ok(()) -} - -#[tracing::instrument(level = "info", skip_all)] -pub(crate) fn did_close( - params: DidCloseTextDocumentParams, - state: &mut WorldState, -) -> anyhow::Result<()> { - let uri = params.text_document.uri; - - // Publish empty set of diagnostics to clear them - // lsp::publish_diagnostics(uri.clone(), Vec::new(), None); - - state - .documents - .remove(&uri) - .ok_or(anyhow!("Failed to remove document for URI: {uri}"))?; - - Ok(()) -} - -pub(crate) async fn did_change_configuration( - _params: DidChangeConfigurationParams, - client: &tower_lsp::Client, - state: &mut WorldState, -) -> anyhow::Result<()> { - // The notification params sometimes contain data but it seems in practice - // we should just ignore it. Instead we need to pull the settings again for - // all URI of interest. - - update_config(workspace_uris(state), client, state) - .instrument(tracing::info_span!("did_change_configuration")) - .await -} - -pub(crate) fn did_change_workspace_folders( - params: DidChangeWorkspaceFoldersParams, - lsp_state: &mut LspState, -) -> anyhow::Result<()> { - // Collect all `errors` to ensure we don't drop events after a first error - let mut errors = ErrorVec::new(); - - for lsp_types::WorkspaceFolder { uri, .. } in params.event.added { - errors.push_err(lsp_state.open_workspace_folder(&uri, Settings::default())); - } - for lsp_types::WorkspaceFolder { uri, .. } in params.event.removed { - errors.push_err(lsp_state.close_workspace_folder(&uri)); - } - - errors.into_result() -} - -pub(crate) fn did_change_watched_files( - params: DidChangeWatchedFilesParams, - lsp_state: &mut LspState, -) -> anyhow::Result<()> { - for change in ¶ms.changes { - lsp_state - .workspace_settings_resolver - .reload_workspaces_matched_by_url(&change.uri); - } - - Ok(()) -} - -#[tracing::instrument(level = "info", skip_all)] -pub(crate) fn did_change_formatting_options( - uri: &Url, - opts: &FormattingOptions, - state: &mut WorldState, -) { - let Ok(doc) = state.get_document_mut(uri) else { - return; - }; - - // The information provided in formatting requests is more up-to-date - // than the user settings because it also includes changes made to the - // configuration of particular editors. However the former is less rich - // than the latter: it does not allow the tab size to differ from the - // indent size, as in the R core sources. So we just ignore the less - // rich updates in this case. - if doc.config.indent.indent_size != doc.config.indent.tab_width { - return; - } - - doc.config.indent.indent_size = opts.tab_size as usize; - doc.config.indent.tab_width = opts.tab_size as usize; - doc.config.indent.indent_style = indent_style_from_lsp(opts.insert_spaces); - - // TODO: - // `trim_trailing_whitespace` - // `trim_final_newlines` - // `insert_final_newline` -} - -async fn update_config( - uris: Vec, - client: &tower_lsp::Client, - state: &mut WorldState, -) -> anyhow::Result<()> { - let mut items: Vec = vec![]; - - let diagnostics_keys = VscDiagnosticsConfig::FIELD_NAMES_AS_ARRAY; - let mut diagnostics_items: Vec = diagnostics_keys - .iter() - .map(|key| ConfigurationItem { - scope_uri: None, - section: Some(VscDiagnosticsConfig::section_from_key(key).into()), - }) - .collect(); - items.append(&mut diagnostics_items); - - // For document configs we collect all pairs of URIs and config keys of - // interest in a flat vector - let document_keys = VscDocumentConfig::FIELD_NAMES_AS_ARRAY; - let mut document_items: Vec = - itertools::iproduct!(uris.iter(), document_keys.iter()) - .map(|(uri, key)| ConfigurationItem { - scope_uri: Some(uri.clone()), - section: Some(VscDocumentConfig::section_from_key(key).into()), - }) - .collect(); - items.append(&mut document_items); - - let configs = client.configuration(items).await?; - - // We got the config items in a flat vector that's guaranteed to be - // ordered in the same way it was sent in. Be defensive and check that - // we've got the expected number of items before we process them chunk - // by chunk - let n_document_items = document_keys.len(); - let n_diagnostics_items = diagnostics_keys.len(); - let n_items = n_diagnostics_items + (n_document_items * uris.len()); - - if configs.len() != n_items { - return Err(anyhow!( - "Unexpected number of retrieved configurations: {}/{}", - configs.len(), - n_items - )); - } - - let mut configs = configs.into_iter(); - - // --- Diagnostics - let keys = diagnostics_keys.into_iter(); - let items: Vec = configs.by_ref().take(n_diagnostics_items).collect(); - - // Create a new `serde_json::Value::Object` manually to convert it - // to a `VscDocumentConfig` with `from_value()`. This way serde_json - // can type-check the dynamic JSON value we got from the client. - let mut map = serde_json::Map::new(); - std::iter::zip(keys, items).for_each(|(key, item)| { - map.insert(key.into(), item); - }); - - // TODO: Deserialise the VS Code configuration - // let config: VscDiagnosticsConfig = serde_json::from_value(serde_json::Value::Object(map))?; - // let config: DiagnosticsConfig = config.into(); - - // let changed = state.config.diagnostics != config; - // state.config.diagnostics = config; - - // if changed { - // lsp::spawn_diagnostics_refresh_all(state.clone()); - // } - - // --- Documents - // For each document, deserialise the vector of JSON values into a typed config - for uri in uris { - let keys = document_keys.into_iter(); - let items: Vec = configs.by_ref().take(n_document_items).collect(); - - let mut map = serde_json::Map::new(); - std::iter::zip(keys, items).for_each(|(key, item)| { - map.insert(key.into(), item); - }); - - // Deserialise the VS Code configuration - let config: VscDocumentConfig = serde_json::from_value(serde_json::Value::Object(map))?; - - // Now convert the VS Code specific type into our own type - let config: DocumentConfig = config.into(); - - // Finally, update the document's config - state.get_document_mut(&uri)?.config = config; - } - - Ok(()) -} diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs deleted file mode 100644 index adb21047..00000000 --- a/crates/lsp/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -// TODO: Remove this -#![allow(dead_code)] - -pub use tower_lsp::start_lsp; - -pub mod capabilities; -pub mod config; -pub mod crates; -pub mod documents; -pub mod encoding; -pub mod error; -pub mod from_proto; -pub mod handlers; -pub mod handlers_ext; -pub mod handlers_format; -pub mod handlers_state; -pub mod logging; -pub mod main_loop; -pub mod rust_analyzer; -pub mod state; -pub mod to_proto; -pub mod tower_lsp; -pub mod workspaces; - -#[cfg(test)] -pub mod test_utils; -#[cfg(test)] -pub mod tower_lsp_test_client; diff --git a/crates/lsp/src/logging.rs b/crates/lsp/src/logging.rs deleted file mode 100644 index 78db0373..00000000 --- a/crates/lsp/src/logging.rs +++ /dev/null @@ -1,318 +0,0 @@ -// --- source -// authors = ["Charlie Marsh"] -// license = "MIT" -// origin = "https://github.com/astral-sh/ruff/blob/03fb2e5ac1481e498f84474800b42a966e9843e1/crates/ruff_server/src/trace.rs" -// --- - -//! The logging system for `air lsp`. -//! -//! ## Air crate logs -//! -//! For air crates, a single log level is supplied as one of: error, warn, info, debug, -//! or trace, which is applied to all air crates that log. -//! -//! Resolution strategy: -//! -//! - The environment variable `AIR_LOG_LEVEL` is consulted. -//! -//! - The LSP `InitializeParams.initializationOptions.logLevel` option is consulted. This -//! can be set in VS Code / Positron using `air.logLevel`, or in Zed by supplying -//! `initialization_options`. -//! -//! - If neither are supplied, we fallback to `"info"`. -//! -//! ## Dependency crate logs -//! -//! For dependency crates, either a single log level can be supplied, or comma separated -//! `target=level` pairs can be supplied, like `tower_lsp=debug,tokio=info`. -//! -//! Resolution strategy: -//! -//! - The environment variable `AIR_DEPENDENCY_LOG_LEVELS` is consulted. -//! -//! - The LSP `InitializeParams.initializationOptions.dependencyLogLevels` option is -//! consulted. This can be set in VS Code / Positron using `air.dependencyLogLevel`, or -//! in Zed by supplying `initialization_options`. -//! -//! - If neither are supplied, we fallback to no logging for dependency crates. -//! -//! ## IDE support -//! -//! For VS Code and Zed, which are known to support `window/logMessage` well, logging will -//! emit a `window/logMessage` message. Otherwise, logging will write to `stderr`, -//! which should appear in the logs for most LSP clients. -use core::str; -use serde::Deserialize; -use std::fmt::Display; -use std::io::{Error as IoError, ErrorKind, Write}; -use std::str::FromStr; -use tokio::sync::mpsc::unbounded_channel; -use tower_lsp::lsp_types::ClientInfo; -use tower_lsp::lsp_types::MessageType; -use tower_lsp::Client; -use tracing_subscriber::filter; -use tracing_subscriber::fmt::time::LocalTime; -use tracing_subscriber::fmt::TestWriter; -use tracing_subscriber::{ - fmt::{writer::BoxMakeWriter, MakeWriter}, - layer::SubscriberExt, - Layer, -}; - -use crate::crates; - -// TODO: -// - Add `air.logLevel` and `air.dependencyLogLevels` as VS Code extension options that set -// the log levels, and pass them through the arbitrary `initializationOptions` field of -// `InitializeParams`. - -const AIR_LOG_LEVEL: &str = "AIR_LOG_LEVEL"; -const AIR_DEPENDENCY_LOG_LEVELS: &str = "AIR_DEPENDENCY_LOG_LEVELS"; - -pub(crate) struct LogMessage { - contents: String, -} - -pub(crate) type LogMessageSender = tokio::sync::mpsc::UnboundedSender; -pub(crate) type LogMessageReceiver = tokio::sync::mpsc::UnboundedReceiver; - -pub(crate) struct LogState { - client: Client, - log_rx: LogMessageReceiver, -} - -// Needed for spawning the loop -unsafe impl Sync for LogState {} - -impl LogState { - pub(crate) fn new(client: Client) -> (Self, LogMessageSender) { - let (log_tx, log_rx) = unbounded_channel::(); - let state = Self { client, log_rx }; - (state, log_tx) - } - - /// Start the log loop - /// - /// Takes ownership of log state and start the low-latency log loop. - /// - /// We use `MessageType::LOG` to prevent the middleware from adding its own - /// timestamp and log level labels. We add that ourselves through tracing. - pub(crate) async fn start(mut self) { - while let Some(message) = self.log_rx.recv().await { - self.client - .log_message(MessageType::LOG, message.contents) - .await - } - - // Channel has been closed. - // All senders have been dropped or `close()` was called. - } -} - -// A log writer that uses LSPs logMessage method. -struct LogWriter<'a> { - log_tx: &'a LogMessageSender, -} - -impl<'a> LogWriter<'a> { - fn new(log_tx: &'a LogMessageSender) -> Self { - Self { log_tx } - } -} - -impl Write for LogWriter<'_> { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let contents = str::from_utf8(buf).map_err(|e| IoError::new(ErrorKind::InvalidData, e))?; - let contents = contents.to_string(); - - // Forward the log message to the latency sensitive log thread, - // which is in charge of forwarding to the client in an async manner. - self.log_tx - .send(LogMessage { contents }) - .map_err(|e| IoError::new(ErrorKind::Other, e))?; - - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -struct LogWriterMaker { - log_tx: LogMessageSender, -} - -impl LogWriterMaker { - fn new(log_tx: LogMessageSender) -> Self { - Self { log_tx } - } -} - -impl<'a> MakeWriter<'a> for LogWriterMaker { - type Writer = LogWriter<'a>; - - fn make_writer(&'a self) -> Self::Writer { - LogWriter::new(&self.log_tx) - } -} - -pub(crate) fn init_logging( - log_tx: LogMessageSender, - log_level: Option, - dependency_log_levels: Option, - client_info: Option<&ClientInfo>, -) { - let log_level = resolve_log_level(log_level); - let dependency_log_levels = resolve_dependency_log_levels(dependency_log_levels); - - let writer = if client_info.is_some_and(|client_info| { - client_info.name.starts_with("Zed") || client_info.name.starts_with("Visual Studio Code") - }) { - // These IDEs are known to support `window/logMessage` well - BoxMakeWriter::new(LogWriterMaker::new(log_tx)) - } else if is_test_client(client_info) { - // Ensures a standard `cargo test` captures output unless `-- --nocapture` is used - BoxMakeWriter::new(TestWriter::default()) - } else { - // Fallback for other editors / IDEs - BoxMakeWriter::new(std::io::stderr) - }; - - let layer = tracing_subscriber::fmt::layer() - // Spend the effort cleaning up the logs before writing them. - // Particularly useful for instrumented logs with spans. - .pretty() - // Disable ANSI escapes, those are not supported in Code - .with_ansi(false) - // Display source code file paths - .with_file(true) - // Display source code line numbers - .with_line_number(true) - // Don't display the thread ID or thread name - .with_thread_ids(false) - .with_thread_names(false) - // Don't display the event's target (module path). - // Mostly redundant with file paths. - .with_target(false) - // Display local time rather than UTC - .with_timer(LocalTime::rfc_3339()) - // Display the log level - .with_level(true) - .with_writer(writer) - .with_filter(log_filter(log_level, dependency_log_levels)); - - let subscriber = tracing_subscriber::Registry::default().with(layer); - - if is_test_client(client_info) { - // During parallel testing, `set_global_default()` gets called multiple times - // per process. That causes it to error, but we ignore this. - tracing::subscriber::set_global_default(subscriber).ok(); - } else { - tracing::subscriber::set_global_default(subscriber) - .expect("Should be able to set the global subscriber exactly once."); - } - - tracing::info!("Logging initialized with level: {log_level}"); -} - -/// We use a special `TestWriter` during tests to be compatible with `cargo test`'s -/// typical output capturing behavior. -/// -/// Important notes: -/// - `cargo test` swallows all logs unless you use `-- --nocapture`. -/// - Tests run in parallel, so logs can be interleaved unless you run `--test-threads 1`. -/// -/// We use `cargo test -- --nocapture --test-threads 1` on CI because of all of this. -fn is_test_client(client_info: Option<&ClientInfo>) -> bool { - client_info.map_or(false, |client_info| client_info.name == "AirTestClient") -} - -fn log_filter(log_level: LogLevel, dependency_log_levels: Option) -> filter::Targets { - // Initialize `filter` from dependency log levels. - // If nothing is supplied, dependency logs are completely off. - let mut filter = match dependency_log_levels { - Some(dependency_log_levels) => match filter::Targets::from_str(&dependency_log_levels) { - Ok(level) => level, - Err(_) => filter::Targets::new(), - }, - None => filter::Targets::new(), - }; - - let log_level = log_level.tracing_level(); - - // Apply the air log level to each air crate that logs - for target in crates::AIR_CRATE_NAMES { - filter = filter.with_target(*target, log_level); - } - - filter -} - -fn resolve_log_level(log_level: Option) -> LogLevel { - // Check log environment variable, this overrides any Client options - if let Some(log_level) = std::env::var(AIR_LOG_LEVEL) - .ok() - .and_then(|level| serde_json::from_value(serde_json::Value::String(level)).ok()) - { - return log_level; - } - - // Client specified log level through initialization parameters - if let Some(log_level) = log_level { - return log_level; - } - - // Default to info logs for air crates - LogLevel::Info -} - -fn resolve_dependency_log_levels(dependency_log_levels: Option) -> Option { - // Check dependency log environment variable, this overrides any Client options - if let Ok(dependency_log_levels) = std::env::var(AIR_DEPENDENCY_LOG_LEVELS) { - return Some(dependency_log_levels); - } - - // Client specified log level through initialization parameters - if dependency_log_levels.is_some() { - return dependency_log_levels; - } - - // Default to no logs for dependency crates - None -} - -#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "lowercase")] -pub(crate) enum LogLevel { - Error, - Warn, - #[default] - Info, - Debug, - Trace, -} - -impl LogLevel { - fn tracing_level(self) -> tracing::Level { - match self { - Self::Error => tracing::Level::ERROR, - Self::Warn => tracing::Level::WARN, - Self::Info => tracing::Level::INFO, - Self::Debug => tracing::Level::DEBUG, - Self::Trace => tracing::Level::TRACE, - } - } -} - -impl Display for LogLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Error => f.write_str("Error"), - Self::Warn => f.write_str("Warn"), - Self::Info => f.write_str("Info"), - Self::Debug => f.write_str("Debug"), - Self::Trace => f.write_str("Trace"), - } - } -} diff --git a/crates/lsp/src/main_loop.rs b/crates/lsp/src/main_loop.rs deleted file mode 100644 index dd0037df..00000000 --- a/crates/lsp/src/main_loop.rs +++ /dev/null @@ -1,515 +0,0 @@ -// -// main_loop.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -use std::collections::HashMap; -use std::future; -use std::pin::Pin; - -use anyhow::anyhow; -use biome_lsp_converters::PositionEncoding; -use biome_lsp_converters::WideEncoding; -use futures::StreamExt; -use tokio::sync::mpsc::unbounded_channel as tokio_unbounded_channel; -use tokio::task::JoinHandle; -use tower_lsp::lsp_types::Diagnostic; -use tower_lsp::Client; -use url::Url; -use workspace::resolve::SettingsResolver; -use workspace::settings::Settings; - -use crate::capabilities::ResolvedClientCapabilities; -use crate::handlers; -use crate::handlers_ext; -use crate::handlers_format; -use crate::handlers_state; -use crate::handlers_state::ConsoleInputs; -use crate::logging::LogMessageSender; -use crate::logging::LogState; -use crate::state::WorldState; -use crate::tower_lsp::LspMessage; -use crate::tower_lsp::LspNotification; -use crate::tower_lsp::LspRequest; -use crate::tower_lsp::LspResponse; -use crate::workspaces::WorkspaceSettingsResolver; - -pub(crate) type TokioUnboundedSender = tokio::sync::mpsc::UnboundedSender; -pub(crate) type TokioUnboundedReceiver = tokio::sync::mpsc::UnboundedReceiver; - -// This is the syntax for trait aliases until an official one is stabilised. -// This alias is for the future of a `JoinHandle>` -trait AnyhowJoinHandleFut: - future::Future, tokio::task::JoinError>> -{ -} -impl AnyhowJoinHandleFut for F where - F: future::Future, tokio::task::JoinError>> -{ -} - -// Alias for a list of join handle futures -type TaskList = futures::stream::FuturesUnordered + Send>>>; - -#[derive(Debug)] -pub(crate) enum Event { - Lsp(LspMessage), - Kernel(KernelNotification), -} - -#[derive(Debug)] -pub(crate) enum KernelNotification { - DidChangeConsoleInputs(ConsoleInputs), -} - -#[derive(Debug)] -pub(crate) enum AuxiliaryEvent { - PublishDiagnostics(Url, Vec, Option), - SpawnedTask(JoinHandle>>), -} - -#[derive(Debug, Clone)] -pub(crate) struct AuxiliaryEventSender { - inner: TokioUnboundedSender, -} - -impl AuxiliaryEventSender { - pub(crate) fn new(tx: TokioUnboundedSender) -> Self { - Self { inner: tx } - } - - /// Passthrough `send()` method to the underlying sender - pub(crate) fn send( - &self, - message: AuxiliaryEvent, - ) -> Result<(), tokio::sync::mpsc::error::SendError> { - self.inner.send(message) - } - - /// Spawn a blocking task - /// - /// This runs tasks that do semantic analysis on a separate thread pool to avoid - /// blocking the main loop. - /// - /// Can optionally return an event for the auxiliary loop (i.e. diagnostics publication). - pub(crate) fn spawn_blocking_task(&self, handler: Handler) - where - Handler: FnOnce() -> anyhow::Result>, - Handler: Send + 'static, - { - let handle = tokio::task::spawn_blocking(handler); - - // Send the join handle to the auxiliary loop so it can log any errors - // or panics - if let Err(err) = self.send(AuxiliaryEvent::SpawnedTask(handle)) { - tracing::warn!("Failed to send task to auxiliary loop due to {err}"); - } - } -} - -/// Global state for the main loop -/// -/// This is a singleton that fully owns the source of truth for `WorldState` -/// which contains the inputs of all LSP methods. The `main_loop()` method is -/// the heart of the LSP. The tower-lsp backend and the Jupyter kernel -/// communicate with the main loop through the `Event` channel that is passed on -/// construction. -pub(crate) struct GlobalState { - /// The global world state containing all inputs for LSP analysis lives - /// here. The dispatcher provides refs, exclusive refs, or snapshots - /// (clones) to handlers. - world: WorldState, - - /// The state containing LSP configuration and tree-sitter parsers for - /// documents contained in the `WorldState`. Only used in exclusive ref - /// handlers, and is not cloneable. - lsp_state: LspState, - - /// LSP client shared with tower-lsp and the log loop - client: Client, - - /// Event receiver channel for the main loop. The tower-lsp methods forward - /// notifications and requests here via `Event::Lsp`. We also receive - /// messages from the kernel via `Event::Kernel`, and from ourselves via - /// `Event::Task`. - events_rx: TokioUnboundedReceiver, - - /// Auxiliary state that gets moved to the auxiliary thread, - /// and our channel for communicating with that thread. - /// Used for sending latency sensitive events like tasks and diagnostics. - auxiliary_state: Option, - auxiliary_event_tx: AuxiliaryEventSender, - - /// Log state that gets moved to the log thread, - /// and a channel for communicating with that thread which we - /// pass on to `init_logging()` during `initialize()`. - log_state: Option, - log_tx: Option, -} - -/// Unlike `WorldState`, `ParserState` cannot be cloned and is only accessed by -/// exclusive handlers. -pub(crate) struct LspState { - /// Resolver to look up [`Settings`] given a document [`Url`] - pub(crate) workspace_settings_resolver: WorkspaceSettingsResolver, - - /// The negociated encoding for document positions. Note that documents are - /// always stored as UTF-8 in Rust Strings. This encoding is only used to - /// translate UTF-16 positions sent by the client to UTF-8 ones. - pub(crate) position_encoding: PositionEncoding, - - /// The set of tree-sitter document parsers managed by the `GlobalState`. - pub(crate) parsers: HashMap, - - /// List of client capabilities that we care about - pub(crate) capabilities: ResolvedClientCapabilities, -} - -impl Default for LspState { - fn default() -> Self { - Self { - workspace_settings_resolver: WorkspaceSettingsResolver::default(), - // Default encoding specified in the LSP protocol - position_encoding: PositionEncoding::Wide(WideEncoding::Utf16), - parsers: Default::default(), - capabilities: ResolvedClientCapabilities::default(), - } - } -} - -impl LspState { - pub(crate) fn document_settings(&self, url: &Url) -> &Settings { - self.workspace_settings_resolver.settings_for_url(url) - } - - pub(crate) fn open_workspace_folder( - &mut self, - url: &Url, - fallback: Settings, - ) -> anyhow::Result<()> { - self.workspace_settings_resolver - .open_workspace_folder(url, fallback) - } - - pub(crate) fn close_workspace_folder( - &mut self, - url: &Url, - ) -> anyhow::Result> { - self.workspace_settings_resolver.close_workspace_folder(url) - } -} - -enum LoopControl { - Shutdown, - None, -} - -/// State for the auxiliary loop -/// -/// The auxiliary loop handles latency-sensitive events such as log messages. A -/// main loop tick might takes many milliseconds and might have a lot of events -/// in queue, so it's not appropriate for events that need immediate handling. -/// -/// The auxiliary loop currently handles: -/// - Log messages. -/// - Joining of spawned blocking tasks to relay any errors or panics to the LSP log. -struct AuxiliaryState { - client: Client, - auxiliary_event_rx: TokioUnboundedReceiver, - tasks: TaskList>, -} - -impl GlobalState { - /// Create a new global state - /// - /// # Arguments - /// - /// * `client`: The tower-lsp client shared with the tower-lsp backend - /// and auxiliary loop. - pub(crate) fn new(client: Client) -> (Self, TokioUnboundedSender) { - // Transmission channel for the main loop events. Shared with the - // tower-lsp backend and the Jupyter kernel. - let (events_tx, events_rx) = tokio_unbounded_channel::(); - - let (log_state, log_tx) = LogState::new(client.clone()); - let (auxiliary_state, auxiliary_event_tx) = AuxiliaryState::new(client.clone()); - - let state = Self { - world: WorldState::default(), - lsp_state: LspState::default(), - client, - events_rx, - auxiliary_state: Some(auxiliary_state), - auxiliary_event_tx, - log_state: Some(log_state), - log_tx: Some(log_tx), - }; - - (state, events_tx) - } - - /// Start the main and auxiliary loops - /// - /// Returns a `JoinSet` that holds onto all tasks and state owned by the - /// event loop. Drop it to cancel everything and shut down the service. - pub(crate) fn start(self) -> tokio::task::JoinSet<()> { - let mut set = tokio::task::JoinSet::<()>::new(); - - // Spawn main loop - set.spawn(async move { self.main_loop().await }); - - set - } - - /// Run main loop - /// - /// This takes ownership of all global state and handles one by one LSP - /// requests, notifications, and other internal events. - async fn main_loop(mut self) { - // Spawn latency-sensitive auxiliary and log threads. - let mut set = tokio::task::JoinSet::<()>::new(); - - // Take ownership over `log_state` and start the log thread. - // Unwrap: `start()` should only be called once. - let log_state = self.log_state.take().unwrap(); - set.spawn(async move { log_state.start().await }); - - // Take ownership over `auxiliary_state` and start the auxiliary thread. - // Unwrap: `start()` should only be called once. - let auxiliary_state = self.auxiliary_state.take().unwrap(); - set.spawn(async move { auxiliary_state.start().await }); - - loop { - let event = self.next_event().await; - match self.handle_event(event).await { - Err(err) => tracing::error!("Failure while handling event:\n{err:?}"), - Ok(LoopControl::Shutdown) => break, - _ => {} - } - } - - tracing::trace!("Main loop closed. Shutting down auxiliary and log loop."); - set.shutdown().await; - } - - async fn next_event(&mut self) -> Event { - self.events_rx.recv().await.unwrap() - } - - #[rustfmt::skip] - /// Handle event of main loop - /// - /// The events are attached to _exclusive_, _sharing_, or _concurrent_ - /// handlers. - /// - /// - Exclusive handlers are passed an `&mut` to the world state so they can - /// update it. - /// - Sharing handlers are passed a simple reference. In principle we could - /// run these concurrently but we run these one handler at a time for simplicity. - /// - When concurrent handlers are needed for performance reason (one tick - /// of the main loop should be as fast as possible to increase throughput) - /// they are spawned on blocking threads and provided a snapshot (clone) of - /// the state. - async fn handle_event(&mut self, event: Event) -> anyhow::Result { - let loop_tick = std::time::Instant::now(); - let mut out = LoopControl::None; - - match event { - Event::Lsp(msg) => match msg { - LspMessage::Notification(notif) => { - match notif { - LspNotification::Initialized(_params) => { - handlers::handle_initialized(&self.client, &self.lsp_state).await?; - }, - LspNotification::DidChangeWorkspaceFolders(params) => { - handlers_state::did_change_workspace_folders(params, &mut self.lsp_state)?; - }, - LspNotification::DidChangeConfiguration(params) => { - handlers_state::did_change_configuration(params, &self.client, &mut self.world).await?; - }, - LspNotification::DidChangeWatchedFiles(params) => { - handlers_state::did_change_watched_files(params, &mut self.lsp_state)?; - }, - LspNotification::DidOpenTextDocument(params) => { - handlers_state::did_open(params, &self.lsp_state, &mut self.world)?; - }, - LspNotification::DidChangeTextDocument(params) => { - handlers_state::did_change(params, &mut self.world)?; - }, - LspNotification::DidSaveTextDocument(_params) => { - // Currently ignored - }, - LspNotification::DidCloseTextDocument(params) => { - handlers_state::did_close(params, &mut self.world)?; - }, - } - }, - - LspMessage::Request(request, tx) => { - match request { - LspRequest::Initialize(params) => { - // Unwrap: `Initialize` method should only be called once. - let log_tx = self.log_tx.take().unwrap(); - respond(tx, handlers_state::initialize(params, &mut self.lsp_state, log_tx), LspResponse::Initialize)?; - }, - LspRequest::Shutdown => { - out = LoopControl::Shutdown; - respond(tx, Ok(()), LspResponse::Shutdown)?; - }, - LspRequest::DocumentFormatting(params) => { - respond(tx, handlers_format::document_formatting(params, &self.lsp_state, &self.world), LspResponse::DocumentFormatting)?; - }, - LspRequest::DocumentRangeFormatting(params) => { - respond(tx, handlers_format::document_range_formatting(params, &self.lsp_state, &self.world), LspResponse::DocumentRangeFormatting)?; - }, - LspRequest::AirViewFile(params) => { - respond(tx, handlers_ext::view_file(params, &self.world), LspResponse::AirViewFile)?; - }, - }; - }, - }, - - Event::Kernel(notif) => match notif { - KernelNotification::DidChangeConsoleInputs(_inputs) => { - // TODO - }, - }, - } - - // TODO Make this threshold configurable by the client - if loop_tick.elapsed() > std::time::Duration::from_millis(50) { - tracing::trace!("Handler took {}ms", loop_tick.elapsed().as_millis()); - } - - Ok(out) - } - - #[allow(dead_code)] // Currently unused - /// Spawn blocking thread for LSP request handler - /// - /// Use this for handlers that might take too long to handle on the main - /// loop and negatively affect throughput. - /// - /// The LSP protocol allows concurrent handling as long as it doesn't affect - /// correctness of responses. For instance handlers that only inspect the - /// world state could be run concurrently. On the other hand, handlers that - /// manipulate documents (e.g. formatting or refactoring) should not. - fn spawn_handler( - &self, - response_tx: TokioUnboundedSender>, - handler: Handler, - into_lsp_response: impl FnOnce(T) -> LspResponse + Send + 'static, - ) where - Handler: FnOnce() -> anyhow::Result, - Handler: Send + 'static, - { - self.auxiliary_event_tx.spawn_blocking_task(move || { - respond(response_tx, handler(), into_lsp_response).and(Ok(None)) - }); - } -} - -/// Respond to a request from the LSP -/// -/// We receive requests from the LSP client with a response channel. Once we -/// have a response, we send it to tower-lsp which will forward it to the -/// client. -/// -/// The response channel will be closed if the request has been cancelled on -/// the tower-lsp side. In that case the future of the async request method -/// has been dropped, along with the receiving side of this channel. It's -/// unclear whether we want to support this sort of client-side cancellation -/// better. We should probably focus on cancellation of expensive tasks -/// running on side threads when the world state has changed. -/// -/// # Arguments -/// -/// * - `response_tx`: A response channel for the tower-lsp request handler. -/// * - `response`: The response wrapped in a `anyhow::Result`. Errors are logged. -/// * - `into_lsp_response`: A constructor for the relevant `LspResponse` variant. -fn respond( - response_tx: TokioUnboundedSender>, - response: anyhow::Result, - into_lsp_response: impl FnOnce(T) -> LspResponse, -) -> anyhow::Result<()> { - let out = match response { - Ok(_) => Ok(()), - Err(ref err) => Err(anyhow!("Error while handling request:\n{err:?}")), - }; - - let response = response.map(into_lsp_response); - - // Ignore errors from a closed channel. This indicates the request has - // been cancelled on the tower-lsp side. - let _ = response_tx.send(response); - - out -} - -// Needed for spawning the loop -unsafe impl Sync for AuxiliaryState {} - -impl AuxiliaryState { - fn new(client: Client) -> (Self, AuxiliaryEventSender) { - // Channels for communication with the auxiliary loop - let (auxiliary_event_tx, auxiliary_event_rx) = tokio_unbounded_channel::(); - let auxiliary_event_tx = AuxiliaryEventSender::new(auxiliary_event_tx); - - // List of pending tasks for which we manage the lifecycle (mainly relay - // errors and panics) - let tasks = futures::stream::FuturesUnordered::new(); - - // Prevent the stream from ever being empty so that `tasks.next()` never - // resolves to `None` - let pending = - tokio::task::spawn(future::pending::>>()); - let pending = - Box::pin(pending) as Pin> + Send>>; - tasks.push(pending); - - let state = Self { - client, - auxiliary_event_rx, - tasks, - }; - - (state, auxiliary_event_tx) - } - - /// Start the auxiliary loop - /// - /// Takes ownership of auxiliary state and start the low-latency auxiliary - /// loop. - async fn start(mut self) -> ! { - loop { - match self.next_event().await { - AuxiliaryEvent::SpawnedTask(handle) => self.tasks.push(Box::pin(handle)), - AuxiliaryEvent::PublishDiagnostics(uri, diagnostics, version) => { - self.client - .publish_diagnostics(uri, diagnostics, version) - .await - } - } - } - } - - async fn next_event(&mut self) -> AuxiliaryEvent { - loop { - tokio::select! { - event = self.auxiliary_event_rx.recv() => return event.unwrap(), - - handle = self.tasks.next() => match handle.unwrap() { - // A joined task returned an event for us, handle it - Ok(Ok(Some(event))) => return event, - - // Otherwise relay any errors and loop back into select - Err(err) => tracing::error!("A task panicked:\n{err:?}"), - Ok(Err(err)) => tracing::error!("A task failed:\n{err:?}"), - _ => (), - }, - } - } - } -} diff --git a/crates/lsp/src/rust_analyzer/diff.rs b/crates/lsp/src/rust_analyzer/diff.rs deleted file mode 100644 index 6587def8..00000000 --- a/crates/lsp/src/rust_analyzer/diff.rs +++ /dev/null @@ -1,44 +0,0 @@ -// --- source -// authors = ["rust-analyzer team"] -// license = "MIT OR Apache-2.0" -// origin = "https://github.com/rust-lang/rust-analyzer/blob/8d5e91c9/crates/rust-analyzer/src/handlers/request.rs#L2483" -// --- - -use biome_text_size::{TextRange, TextSize}; - -use super::text_edit::TextEdit; - -pub(crate) fn diff(left: &str, right: &str) -> TextEdit { - use dissimilar::Chunk; - - let chunks = dissimilar::diff(left, right); - - let mut builder = TextEdit::builder(); - let mut pos = TextSize::default(); - - let mut chunks = chunks.into_iter().peekable(); - while let Some(chunk) = chunks.next() { - if let (Chunk::Delete(deleted), Some(&Chunk::Insert(inserted))) = (chunk, chunks.peek()) { - chunks.next().unwrap(); - let deleted_len = TextSize::of(deleted); - builder.replace(TextRange::at(pos, deleted_len), inserted.into()); - pos += deleted_len; - continue; - } - - match chunk { - Chunk::Equal(text) => { - pos += TextSize::of(text); - } - Chunk::Delete(deleted) => { - let deleted_len = TextSize::of(deleted); - builder.delete(TextRange::at(pos, deleted_len)); - pos += deleted_len; - } - Chunk::Insert(inserted) => { - builder.insert(pos, inserted.into()); - } - } - } - builder.finish() -} diff --git a/crates/lsp/src/rust_analyzer/line_index.rs b/crates/lsp/src/rust_analyzer/line_index.rs deleted file mode 100644 index f1634d17..00000000 --- a/crates/lsp/src/rust_analyzer/line_index.rs +++ /dev/null @@ -1,19 +0,0 @@ -// --- source -// authors = ["rust-analyzer team"] -// license = "MIT OR Apache-2.0" -// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/line_index.rs" -// --- - -//! Enhances `ide::LineIndex` with additional info required to convert offsets -//! into lsp positions. - -use biome_lsp_converters::line_index; -use line_ending::LineEnding; -use triomphe::Arc; - -#[derive(Debug, Clone)] -pub struct LineIndex { - pub index: Arc, - pub endings: LineEnding, - pub encoding: biome_lsp_converters::PositionEncoding, -} diff --git a/crates/lsp/src/rust_analyzer/mod.rs b/crates/lsp/src/rust_analyzer/mod.rs deleted file mode 100644 index bfb231cc..00000000 --- a/crates/lsp/src/rust_analyzer/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod diff; -pub mod line_index; -pub mod text_edit; -pub mod to_proto; -pub mod utils; diff --git a/crates/lsp/src/rust_analyzer/text_edit.rs b/crates/lsp/src/rust_analyzer/text_edit.rs deleted file mode 100644 index fa777e50..00000000 --- a/crates/lsp/src/rust_analyzer/text_edit.rs +++ /dev/null @@ -1,338 +0,0 @@ -// --- source -// authors = ["rust-analyzer team"] -// license = "MIT OR Apache-2.0" -// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/ide-db/src/text_edit.rs" -// --- - -//! Representation of a `TextEdit`. -//! -//! `rust-analyzer` never mutates text itself and only sends diffs to clients, -//! so `TextEdit` is the ultimate representation of the work done by -//! rust-analyzer. - -use biome_text_size::{TextRange, TextSize}; -use itertools::Itertools; -use std::cmp::max; - -/// `InsertDelete` -- a single "atomic" change to text -/// -/// Must not overlap with other `InDel`s -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Indel { - pub insert: String, - /// Refers to offsets in the original text - pub delete: TextRange, -} - -#[derive(Default, Debug, Clone)] -pub struct TextEdit { - /// Invariant: disjoint and sorted by `delete`. - indels: Vec, -} - -#[derive(Debug, Default, Clone)] -pub struct TextEditBuilder { - indels: Vec, -} - -impl Indel { - pub fn insert(offset: TextSize, text: String) -> Indel { - Indel::replace(TextRange::empty(offset), text) - } - pub fn delete(range: TextRange) -> Indel { - Indel::replace(range, String::new()) - } - pub fn replace(range: TextRange, replace_with: String) -> Indel { - Indel { - delete: range, - insert: replace_with, - } - } - - pub fn apply(&self, text: &mut String) { - let start: usize = self.delete.start().into(); - let end: usize = self.delete.end().into(); - text.replace_range(start..end, &self.insert); - } -} - -impl TextEdit { - pub fn builder() -> TextEditBuilder { - TextEditBuilder::default() - } - - pub fn insert(offset: TextSize, text: String) -> TextEdit { - let mut builder = TextEdit::builder(); - builder.insert(offset, text); - builder.finish() - } - - pub fn delete(range: TextRange) -> TextEdit { - let mut builder = TextEdit::builder(); - builder.delete(range); - builder.finish() - } - - pub fn replace(range: TextRange, replace_with: String) -> TextEdit { - let mut builder = TextEdit::builder(); - builder.replace(range, replace_with); - builder.finish() - } - - // --- Start Posit - pub fn diff(text: &str, replace_with: &str) -> TextEdit { - super::diff::diff(text, replace_with) - } - // --- End Posit - - pub fn len(&self) -> usize { - self.indels.len() - } - - pub fn is_empty(&self) -> bool { - self.indels.is_empty() - } - - pub fn iter(&self) -> std::slice::Iter<'_, Indel> { - self.into_iter() - } - - pub fn apply(&self, text: &mut String) { - match self.len() { - 0 => return, - 1 => { - self.indels[0].apply(text); - return; - } - _ => (), - } - - let text_size = TextSize::of(&*text); - let mut total_len = text_size; - let mut max_total_len = text_size; - for indel in &self.indels { - total_len += TextSize::of(&indel.insert); - total_len -= indel.delete.len(); - max_total_len = max(max_total_len, total_len); - } - - if let Some(additional) = max_total_len.checked_sub(text_size) { - text.reserve(additional.into()); - } - - for indel in self.indels.iter().rev() { - indel.apply(text); - } - - assert_eq!(TextSize::of(&*text), total_len); - } - - pub fn union(&mut self, other: TextEdit) -> Result<(), TextEdit> { - let iter_merge = self - .iter() - .merge_by(other.iter(), |l, r| l.delete.start() <= r.delete.start()); - if !check_disjoint(&mut iter_merge.clone()) { - return Err(other); - } - - // Only dedup deletions and replacements, keep all insertions - self.indels = iter_merge - .dedup_by(|a, b| a == b && !a.delete.is_empty()) - .cloned() - .collect(); - Ok(()) - } - - pub fn apply_to_offset(&self, offset: TextSize) -> Option { - let mut res = offset; - for indel in &self.indels { - if indel.delete.start() >= offset { - break; - } - if offset < indel.delete.end() { - return None; - } - res += TextSize::of(&indel.insert); - res -= indel.delete.len(); - } - Some(res) - } -} - -impl IntoIterator for TextEdit { - type Item = Indel; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.indels.into_iter() - } -} - -impl<'a> IntoIterator for &'a TextEdit { - type Item = &'a Indel; - type IntoIter = std::slice::Iter<'a, Indel>; - - fn into_iter(self) -> Self::IntoIter { - self.indels.iter() - } -} - -impl TextEditBuilder { - pub fn is_empty(&self) -> bool { - self.indels.is_empty() - } - pub fn replace(&mut self, range: TextRange, replace_with: String) { - self.indel(Indel::replace(range, replace_with)); - } - pub fn delete(&mut self, range: TextRange) { - self.indel(Indel::delete(range)); - } - pub fn insert(&mut self, offset: TextSize, text: String) { - self.indel(Indel::insert(offset, text)); - } - pub fn finish(self) -> TextEdit { - let mut indels = self.indels; - assert_disjoint_or_equal(&mut indels); - indels = coalesce_indels(indels); - TextEdit { indels } - } - pub fn invalidates_offset(&self, offset: TextSize) -> bool { - self.indels - .iter() - .any(|indel| indel.delete.contains_inclusive(offset)) - } - fn indel(&mut self, indel: Indel) { - self.indels.push(indel); - if self.indels.len() <= 16 { - assert_disjoint_or_equal(&mut self.indels); - } - } -} - -fn assert_disjoint_or_equal(indels: &mut [Indel]) { - assert!(check_disjoint_and_sort(indels)); -} - -fn check_disjoint_and_sort(indels: &mut [Indel]) -> bool { - indels.sort_by_key(|indel| (indel.delete.start(), indel.delete.end())); - check_disjoint(&mut indels.iter()) -} - -fn check_disjoint<'a, I>(indels: &mut I) -> bool -where - I: std::iter::Iterator + Clone, -{ - indels - .clone() - .zip(indels.skip(1)) - .all(|(l, r)| l.delete.end() <= r.delete.start() || l == r) -} - -fn coalesce_indels(indels: Vec) -> Vec { - indels - .into_iter() - .coalesce(|mut a, b| { - if a.delete.end() == b.delete.start() { - a.insert.push_str(&b.insert); - a.delete = TextRange::new(a.delete.start(), b.delete.end()); - Ok(a) - } else { - Err((a, b)) - } - }) - .collect_vec() -} - -#[cfg(test)] -mod tests { - use super::{TextEdit, TextEditBuilder, TextRange}; - - fn range(start: u32, end: u32) -> TextRange { - TextRange::new(start.into(), end.into()) - } - - #[test] - fn test_apply() { - let mut text = "_11h1_2222_xx3333_4444_6666".to_owned(); - let mut builder = TextEditBuilder::default(); - builder.replace(range(3, 4), "1".to_owned()); - builder.delete(range(11, 13)); - builder.insert(22.into(), "_5555".to_owned()); - - let text_edit = builder.finish(); - text_edit.apply(&mut text); - - assert_eq!(text, "_1111_2222_3333_4444_5555_6666") - } - - #[test] - fn test_union() { - let mut edit1 = TextEdit::delete(range(7, 11)); - let mut builder = TextEditBuilder::default(); - builder.delete(range(1, 5)); - builder.delete(range(13, 17)); - - let edit2 = builder.finish(); - assert!(edit1.union(edit2).is_ok()); - assert_eq!(edit1.indels.len(), 3); - } - - #[test] - fn test_union_with_duplicates() { - let mut builder1 = TextEditBuilder::default(); - builder1.delete(range(7, 11)); - builder1.delete(range(13, 17)); - - let mut builder2 = TextEditBuilder::default(); - builder2.delete(range(1, 5)); - builder2.delete(range(13, 17)); - - let mut edit1 = builder1.finish(); - let edit2 = builder2.finish(); - assert!(edit1.union(edit2).is_ok()); - assert_eq!(edit1.indels.len(), 3); - } - - #[test] - fn test_union_panics() { - let mut edit1 = TextEdit::delete(range(7, 11)); - let edit2 = TextEdit::delete(range(9, 13)); - assert!(edit1.union(edit2).is_err()); - } - - #[test] - fn test_coalesce_disjoint() { - let mut builder = TextEditBuilder::default(); - builder.replace(range(1, 3), "aa".into()); - builder.replace(range(5, 7), "bb".into()); - let edit = builder.finish(); - - assert_eq!(edit.indels.len(), 2); - } - - #[test] - fn test_coalesce_adjacent() { - let mut builder = TextEditBuilder::default(); - builder.replace(range(1, 3), "aa".into()); - builder.replace(range(3, 5), "bb".into()); - - let edit = builder.finish(); - assert_eq!(edit.indels.len(), 1); - assert_eq!(edit.indels[0].insert, "aabb"); - assert_eq!(edit.indels[0].delete, range(1, 5)); - } - - #[test] - fn test_coalesce_adjacent_series() { - let mut builder = TextEditBuilder::default(); - builder.replace(range(1, 3), "au".into()); - builder.replace(range(3, 5), "www".into()); - builder.replace(range(5, 8), String::new()); - builder.replace(range(8, 9), "ub".into()); - - let edit = builder.finish(); - assert_eq!(edit.indels.len(), 1); - assert_eq!(edit.indels[0].insert, "auwwwub"); - assert_eq!(edit.indels[0].delete, range(1, 9)); - } -} diff --git a/crates/lsp/src/rust_analyzer/to_proto.rs b/crates/lsp/src/rust_analyzer/to_proto.rs deleted file mode 100644 index 1eab5351..00000000 --- a/crates/lsp/src/rust_analyzer/to_proto.rs +++ /dev/null @@ -1,60 +0,0 @@ -// --- source -// authors = ["rust-analyzer team"] -// license = "MIT OR Apache-2.0" -// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/lsp/to_proto.rs" -// --- - -//! Conversion of rust-analyzer specific types to lsp_types equivalents. - -use super::{ - line_index::LineIndex, - text_edit::{Indel, TextEdit}, -}; -use line_ending::LineEnding; -use tower_lsp::lsp_types; - -pub(crate) fn text_edit( - line_index: &LineIndex, - indel: Indel, -) -> anyhow::Result { - let range = biome_lsp_converters::to_proto::range( - &line_index.index, - indel.delete, - line_index.encoding, - )?; - let new_text = match line_index.endings { - LineEnding::Lf => indel.insert, - LineEnding::Crlf => indel.insert.replace('\n', "\r\n"), - }; - Ok(lsp_types::TextEdit { range, new_text }) -} - -pub(crate) fn completion_text_edit( - line_index: &LineIndex, - insert_replace_support: Option, - indel: Indel, -) -> anyhow::Result { - let text_edit = text_edit(line_index, indel)?; - Ok(match insert_replace_support { - Some(cursor_pos) => lsp_types::InsertReplaceEdit { - new_text: text_edit.new_text, - insert: lsp_types::Range { - start: text_edit.range.start, - end: cursor_pos, - }, - replace: text_edit.range, - } - .into(), - None => text_edit.into(), - }) -} - -pub(crate) fn text_edit_vec( - line_index: &LineIndex, - text_edit: TextEdit, -) -> anyhow::Result> { - text_edit - .into_iter() - .map(|indel| self::text_edit(line_index, indel)) - .collect() -} diff --git a/crates/lsp/src/rust_analyzer/utils.rs b/crates/lsp/src/rust_analyzer/utils.rs deleted file mode 100644 index 0e7856d8..00000000 --- a/crates/lsp/src/rust_analyzer/utils.rs +++ /dev/null @@ -1,67 +0,0 @@ -// --- source -// authors = ["rust-analyzer team"] -// license = "MIT OR Apache-2.0" -// origin = "https://github.com/rust-lang/rust-analyzer/blob/master/crates/rust-analyzer/src/lsp/utils.rs" -// --- - -use std::ops::Range; - -use biome_lsp_converters::line_index; -use line_ending::LineEnding; -use tower_lsp::lsp_types; -use triomphe::Arc; - -use crate::from_proto; - -use super::line_index::LineIndex; - -pub(crate) fn apply_document_changes( - encoding: biome_lsp_converters::PositionEncoding, - file_contents: &str, - mut content_changes: Vec, -) -> String { - // If at least one of the changes is a full document change, use the last - // of them as the starting point and ignore all previous changes. - let (mut text, content_changes) = match content_changes - .iter() - .rposition(|change| change.range.is_none()) - { - Some(idx) => { - let text = std::mem::take(&mut content_changes[idx].text); - (text, &content_changes[idx + 1..]) - } - None => (file_contents.to_owned(), &content_changes[..]), - }; - if content_changes.is_empty() { - return text; - } - - let mut line_index = LineIndex { - // the index will be overwritten in the bottom loop's first iteration - index: Arc::new(line_index::LineIndex::new(&text)), - // We don't care about line endings here. - endings: LineEnding::Lf, - encoding, - }; - - // The changes we got must be applied sequentially, but can cross lines so we - // have to keep our line index updated. - // Some clients (e.g. Code) sort the ranges in reverse. As an optimization, we - // remember the last valid line in the index and only rebuild it if needed. - // The VFS will normalize the end of lines to `\n`. - let mut index_valid = !0u32; - for change in content_changes { - // The None case can't happen as we have handled it above already - if let Some(range) = change.range { - if index_valid <= range.end.line { - *Arc::make_mut(&mut line_index.index) = line_index::LineIndex::new(&text); - } - index_valid = range.start.line; - if let Ok(range) = from_proto::text_range(&line_index.index, range, line_index.encoding) - { - text.replace_range(Range::::from(range), &change.text); - } - } - } - text -} diff --git a/crates/lsp/src/snapshots/lsp__documents__tests__document_syntax-2.snap b/crates/lsp/src/snapshots/lsp__documents__tests__document_syntax-2.snap deleted file mode 100644 index 035eeb75..00000000 --- a/crates/lsp/src/snapshots/lsp__documents__tests__document_syntax-2.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: crates/lsp/src/documents.rs -expression: updated_syntax ---- -0: R_ROOT@0..10 - 0: (empty) - 1: R_EXPRESSION_LIST@0..10 - 0: R_CALL@0..10 - 0: R_IDENTIFIER@0..3 - 0: IDENT@0..3 "foo" [] [] - 1: R_CALL_ARGUMENTS@3..10 - 0: L_PAREN@3..4 "(" [] [] - 1: R_ARGUMENT_LIST@4..9 - 0: R_ARGUMENT@4..9 - 0: (empty) - 1: R_BINARY_EXPRESSION@4..9 - 0: R_DOUBLE_VALUE@4..5 - 0: R_DOUBLE_LITERAL@4..5 "1" [] [] - 1: PLUS@5..7 "+" [Whitespace(" ")] [] - 2: R_DOUBLE_VALUE@7..9 - 0: R_DOUBLE_LITERAL@7..9 "2" [Whitespace(" ")] [] - 2: R_PAREN@9..10 ")" [] [] - 2: EOF@10..10 "" [] [] diff --git a/crates/lsp/src/snapshots/lsp__documents__tests__document_syntax.snap b/crates/lsp/src/snapshots/lsp__documents__tests__document_syntax.snap deleted file mode 100644 index 2717648c..00000000 --- a/crates/lsp/src/snapshots/lsp__documents__tests__document_syntax.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: crates/lsp/src/documents.rs -expression: original_syntax ---- -0: R_ROOT@0..8 - 0: (empty) - 1: R_EXPRESSION_LIST@0..8 - 0: R_CALL@0..8 - 0: R_IDENTIFIER@0..3 - 0: IDENT@0..3 "foo" [] [] - 1: R_CALL_ARGUMENTS@3..8 - 0: L_PAREN@3..4 "(" [] [] - 1: R_ARGUMENT_LIST@4..7 - 0: R_ARGUMENT@4..7 - 0: (empty) - 1: R_IDENTIFIER@4..7 - 0: IDENT@4..7 "bar" [] [] - 2: R_PAREN@7..8 ")" [] [] - 2: EOF@8..8 "" [] [] diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format.snap deleted file mode 100644 index 8bd66ada..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: formatted ---- -1 -2 + 2 -3 + 3 + 3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-2.snap deleted file mode 100644 index ec60bfb9..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-2.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output ---- -1+1 -# -2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-3.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-3.snap deleted file mode 100644 index f5bf150f..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-3.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output ---- -1+1 -{2 + 2} diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap deleted file mode 100644 index 478729cf..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-4.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output ---- -1+1 -{ - 2 + 2 -} diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-5.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-5.snap deleted file mode 100644 index df9e8931..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines-5.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output ---- -1+1 -{ - 2+2 - { - 3 + 3 - } -} diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines.snap deleted file mode 100644 index cf743f29..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_logical_lines.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output ---- -1+1 -2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_mismatched_indent.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_mismatched_indent.snap deleted file mode 100644 index 9681b5cd..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_mismatched_indent.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output ---- -1 - 2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines-2.snap deleted file mode 100644 index cc0b2f04..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines-2.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output2 ---- -1 + 1 -# -2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines.snap deleted file mode 100644 index f63c36ef..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_multiple_lines.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output1 ---- -1+1 -# -2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-2.snap deleted file mode 100644 index f3257fac..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-2.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output ---- - diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-3.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-3.snap deleted file mode 100644 index 0fb74fc7..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none-3.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output ---- -1 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none.snap deleted file mode 100644 index f3257fac..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_none.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output ---- - diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap deleted file mode 100644 index 8c4b6082..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-2.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output2 ---- -0+0 -1 + 1 -{ - 2 + 2 -} -3+3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap deleted file mode 100644 index eccb6f4e..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-3.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output3 ---- -0+0 -1 + 1 -{ - 2 + 2 -} -3 + 3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap deleted file mode 100644 index 699c29fb..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-4.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output4 ---- -0+0 -1+1 -{ - 2 + 2 -} -3 + 3 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-5.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-5.snap deleted file mode 100644 index 2055d4bb..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-5.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output5 ---- -1 + 1 -2+2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-6.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-6.snap deleted file mode 100644 index 2388991a..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists-6.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output6 ---- -1+1 -2 + 2 diff --git a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap b/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap deleted file mode 100644 index 6587090f..00000000 --- a/crates/lsp/src/snapshots/lsp__handlers_format__tests__format_range_unmatched_lists.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/lsp/src/handlers_format.rs -expression: output1 ---- -0+0 -1 + 1 -{ - 2 + 2 -} -3+3 diff --git a/crates/lsp/src/state.rs b/crates/lsp/src/state.rs deleted file mode 100644 index 219a5165..00000000 --- a/crates/lsp/src/state.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::collections::HashMap; - -use anyhow::anyhow; -use url::Url; - -use crate::config::LspConfig; -use crate::documents::Document; - -#[derive(Clone, Default, Debug)] -/// The world state, i.e. all the inputs necessary for analysing or refactoring -/// code. This is a pure value. There is no interior mutability in this data -/// structure. It can be cloned and safely sent to other threads. -pub(crate) struct WorldState { - /// Watched documents - pub(crate) documents: HashMap, - - /// The scopes for the console. This currently contains a list (outer `Vec`) - /// of names (inner `Vec`) within the environments on the search path, starting - /// from the global environment and ending with the base package. Eventually - /// this might also be populated with the scope for the current environment - /// in debug sessions (not implemented yet). - /// - /// This is currently one of the main sources of known symbols for - /// diagnostics. In the future we should better delineate interactive - /// contexts (e.g. the console, but scripts might also be treated as - /// interactive, which could be a user setting) and non-interactive ones - /// (e.g. a package). In non-interactive contexts, the lexical scopes - /// examined for diagnostics should be fully determined by variable bindings - /// and imports (code-first diagnostics). - /// - /// In the future this should probably become more complex with a list of - /// either symbol names (as is now the case) or named environments, such as - /// `pkg:ggplot2`. Storing named environments here will allow the LSP to - /// retrieve the symbols in a pull fashion (the whole console scopes are - /// currently pushed to the LSP), and cache the symbols with Salsa. The - /// performance is not currently an issue but this could change once we do - /// more analysis of symbols in the search path. - pub(crate) console_scopes: Vec>, - - /// Currently installed packages - pub(crate) installed_packages: Vec, - - pub(crate) config: LspConfig, -} - -impl WorldState { - pub(crate) fn get_document(&self, uri: &Url) -> anyhow::Result<&Document> { - if let Some(doc) = self.documents.get(uri) { - Ok(doc) - } else { - Err(anyhow!("Can't find document for URI {uri}")) - } - } - - pub(crate) fn get_document_mut(&mut self, uri: &Url) -> anyhow::Result<&mut Document> { - if let Some(doc) = self.documents.get_mut(uri) { - Ok(doc) - } else { - Err(anyhow!("Can't find document for URI {uri}")) - } - } -} - -pub(crate) fn workspace_uris(state: &WorldState) -> Vec { - let uris: Vec = state.documents.iter().map(|elt| elt.0.clone()).collect(); - uris -} diff --git a/crates/lsp/src/test_utils.rs b/crates/lsp/src/test_utils.rs deleted file mode 100644 index fe60c0c9..00000000 --- a/crates/lsp/src/test_utils.rs +++ /dev/null @@ -1,27 +0,0 @@ -use biome_text_size::{TextRange, TextSize}; - -pub(crate) fn extract_marked_range(input: &str) -> (String, TextRange) { - let mut output = String::new(); - let mut start = None; - let mut end = None; - let mut chars = input.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '<' && chars.peek() == Some(&'<') { - chars.next(); - start = Some(TextSize::from(output.len() as u32)); - } else if c == '>' && chars.peek() == Some(&'>') { - chars.next(); - end = Some(TextSize::from(output.len() as u32)); - } else { - output.push(c); - } - } - - let range = match (start, end) { - (Some(start), Some(end)) => TextRange::new(start, end), - _ => panic!("Missing range markers"), - }; - - (output, range) -} diff --git a/crates/lsp/src/to_proto.rs b/crates/lsp/src/to_proto.rs deleted file mode 100644 index 4b4c5f5e..00000000 --- a/crates/lsp/src/to_proto.rs +++ /dev/null @@ -1,51 +0,0 @@ -// -// to_proto.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -// Utilites for converting internal types to LSP types - -pub(crate) use rust_analyzer::to_proto::text_edit_vec; - -#[cfg(test)] -pub(crate) use biome_lsp_converters::to_proto::range; - -use crate::rust_analyzer::{self, line_index::LineIndex, text_edit::TextEdit}; -use biome_text_size::TextRange; -use tower_lsp::lsp_types; - -pub(crate) fn doc_edit_vec( - line_index: &LineIndex, - text_edit: TextEdit, -) -> anyhow::Result> { - let edits = text_edit_vec(line_index, text_edit)?; - - Ok(edits - .into_iter() - .map(|edit| lsp_types::TextDocumentContentChangeEvent { - range: Some(edit.range), - range_length: None, - text: edit.new_text, - }) - .collect()) -} - -pub(crate) fn replace_range_edit( - line_index: &LineIndex, - range: TextRange, - replace_with: String, -) -> anyhow::Result> { - let edit = TextEdit::replace(range, replace_with); - text_edit_vec(line_index, edit) -} - -pub(crate) fn replace_all_edit( - line_index: &LineIndex, - text: &str, - replace_with: &str, -) -> anyhow::Result> { - let edit = TextEdit::diff(text, replace_with); - text_edit_vec(line_index, edit) -} diff --git a/crates/lsp/src/tower_lsp.rs b/crates/lsp/src/tower_lsp.rs deleted file mode 100644 index 60de59d0..00000000 --- a/crates/lsp/src/tower_lsp.rs +++ /dev/null @@ -1,361 +0,0 @@ -// -// tower_lsp.rs -// -// Copyright (C) 2022-2024 Posit Software, PBC. All rights reserved. -// -// - -#![allow(deprecated)] - -use strum::IntoStaticStr; - -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio::sync::mpsc::unbounded_channel as tokio_unbounded_channel; -use tower_lsp::jsonrpc::Result; -use tower_lsp::lsp_types::*; -use tower_lsp::Client; -use tower_lsp::LanguageServer; -use tower_lsp::LspService; -use tower_lsp::{jsonrpc, ClientSocket}; - -use crate::handlers_ext::ViewFileParams; -use crate::main_loop::Event; -use crate::main_loop::GlobalState; -use crate::main_loop::TokioUnboundedSender; - -// Based on https://stackoverflow.com/a/69324393/1725177 -macro_rules! cast_response { - ($target:expr, $pat:path) => {{ - match $target { - Ok($pat(resp)) => Ok(resp), - Err(err) => Err(new_jsonrpc_error(format!("{err:?}"))), - _ => panic!("Unexpected variant while casting to {}", stringify!($pat)), - } - }}; -} - -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] -pub(crate) enum LspMessage { - Notification(LspNotification), - Request( - LspRequest, - TokioUnboundedSender>, - ), -} - -#[derive(Debug, IntoStaticStr)] -pub(crate) enum LspNotification { - Initialized(InitializedParams), - DidChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams), - DidChangeConfiguration(DidChangeConfigurationParams), - DidChangeWatchedFiles(DidChangeWatchedFilesParams), - DidOpenTextDocument(DidOpenTextDocumentParams), - DidChangeTextDocument(DidChangeTextDocumentParams), - DidSaveTextDocument(DidSaveTextDocumentParams), - DidCloseTextDocument(DidCloseTextDocumentParams), -} - -#[allow(clippy::large_enum_variant)] -#[derive(Debug, IntoStaticStr)] -pub(crate) enum LspRequest { - Initialize(InitializeParams), - DocumentFormatting(DocumentFormattingParams), - Shutdown, - DocumentRangeFormatting(DocumentRangeFormattingParams), - AirViewFile(ViewFileParams), -} - -#[allow(clippy::large_enum_variant)] -#[derive(Debug, IntoStaticStr)] -pub(crate) enum LspResponse { - Initialize(InitializeResult), - DocumentFormatting(Option>), - DocumentRangeFormatting(Option>), - Shutdown(()), - AirViewFile(String), -} - -impl std::fmt::Display for LspNotification { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.into()) - } -} -impl std::fmt::Display for LspRequest { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.into()) - } -} -impl std::fmt::Display for LspResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.into()) - } -} - -impl LspNotification { - fn trace(&self) -> TraceLspNotification { - TraceLspNotification { inner: self } - } -} -impl LspRequest { - fn trace(&self) -> TraceLspRequest { - TraceLspRequest { inner: self } - } -} -impl LspResponse { - fn trace(&self) -> TraceLspResponse { - TraceLspResponse { inner: self } - } -} - -struct TraceLspNotification<'a> { - inner: &'a LspNotification, -} -struct TraceLspRequest<'a> { - inner: &'a LspRequest, -} -struct TraceLspResponse<'a> { - inner: &'a LspResponse, -} - -impl std::fmt::Debug for TraceLspNotification<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.inner { - LspNotification::DidOpenTextDocument(params) => { - // Ignore the document itself in trace logs - f.debug_tuple(self.inner.into()) - .field(¶ms.text_document.uri) - .field(¶ms.text_document.version) - .field(¶ms.text_document.language_id) - .finish() - } - _ => std::fmt::Debug::fmt(self.inner, f), - } - } -} - -impl std::fmt::Debug for TraceLspRequest<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(self.inner, f) - } -} - -impl std::fmt::Debug for TraceLspResponse<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(self.inner, f) - } -} - -#[derive(Debug)] -struct Backend { - /// Channel for communication with the main loop. - events_tx: TokioUnboundedSender, - - /// Handle to main loop. Drop it to cancel the loop, all associated tasks, - /// and drop all owned state. - _main_loop: tokio::task::JoinSet<()>, -} - -impl Backend { - async fn request(&self, request: LspRequest) -> anyhow::Result { - tracing::info!("Incoming: {request}"); - tracing::trace!("Incoming (debug):\n{request:#?}", request = request.trace()); - - let (response_tx, mut response_rx) = - tokio_unbounded_channel::>(); - - // Relay request to main loop - self.events_tx - .send(Event::Lsp(LspMessage::Request(request, response_tx))) - .unwrap(); - - // Wait for response from main loop - let response = response_rx.recv().await.unwrap()?; - - tracing::info!("Outgoing: {response}"); - tracing::trace!( - "Outgoing (debug):\n{response:#?}", - response = response.trace() - ); - Ok(response) - } - - fn notify(&self, notif: LspNotification) { - tracing::info!("Incoming: {notif}"); - tracing::trace!("Incoming (debug):\n{notif:#?}", notif = notif.trace()); - - // Relay notification to main loop - self.events_tx - .send(Event::Lsp(LspMessage::Notification(notif))) - .unwrap(); - } - - async fn air_view_file(&self, params: ViewFileParams) -> tower_lsp::jsonrpc::Result { - cast_response!( - self.request(LspRequest::AirViewFile(params)).await, - LspResponse::AirViewFile - ) - } -} - -#[tower_lsp::async_trait] -impl LanguageServer for Backend { - async fn initialize(&self, params: InitializeParams) -> Result { - cast_response!( - self.request(LspRequest::Initialize(params)).await, - LspResponse::Initialize - ) - } - - async fn initialized(&self, params: InitializedParams) { - self.notify(LspNotification::Initialized(params)); - } - - async fn shutdown(&self) -> Result<()> { - cast_response!( - self.request(LspRequest::Shutdown).await, - LspResponse::Shutdown - ) - } - - async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { - self.notify(LspNotification::DidChangeWorkspaceFolders(params)); - } - - async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { - self.notify(LspNotification::DidChangeConfiguration(params)); - } - - async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) { - self.notify(LspNotification::DidChangeWatchedFiles(params)); - } - - async fn did_open(&self, params: DidOpenTextDocumentParams) { - self.notify(LspNotification::DidOpenTextDocument(params)); - } - - async fn did_change(&self, params: DidChangeTextDocumentParams) { - self.notify(LspNotification::DidChangeTextDocument(params)); - } - - async fn did_save(&self, params: DidSaveTextDocumentParams) { - self.notify(LspNotification::DidSaveTextDocument(params)); - } - - async fn did_close(&self, params: DidCloseTextDocumentParams) { - self.notify(LspNotification::DidCloseTextDocument(params)); - } - - async fn formatting(&self, params: DocumentFormattingParams) -> Result>> { - cast_response!( - self.request(LspRequest::DocumentFormatting(params)).await, - LspResponse::DocumentFormatting - ) - } - - async fn range_formatting( - &self, - params: DocumentRangeFormattingParams, - ) -> Result>> { - cast_response!( - self.request(LspRequest::DocumentRangeFormatting(params)) - .await, - LspResponse::DocumentRangeFormatting - ) - } -} - -pub async fn start_lsp(read: I, write: O) -where - I: AsyncRead + Unpin, - O: AsyncWrite, -{ - let (service, socket) = new_lsp(); - let server = tower_lsp::Server::new(read, write, socket); - server.serve(service).await; -} - -fn new_lsp() -> (LspService, ClientSocket) { - let init = |client: Client| { - let (state, events_tx) = GlobalState::new(client); - - // Start main loop and hold onto the handle that keeps it alive - let main_loop = state.start(); - - Backend { - events_tx, - _main_loop: main_loop, - } - }; - - LspService::build(init) - .custom_method("air/viewFile", Backend::air_view_file) - .finish() -} - -fn new_jsonrpc_error(message: String) -> jsonrpc::Error { - jsonrpc::Error { - code: jsonrpc::ErrorCode::ServerError(-1), - message: message.into(), - data: None, - } -} - -#[cfg(test)] -pub(crate) async fn start_test_client() -> lsp_test::lsp_client::TestClient { - lsp_test::lsp_client::TestClient::new(|server_rx, client_tx| async { - start_lsp(server_rx, client_tx).await - }) -} - -#[cfg(test)] -pub(crate) async fn init_test_client() -> lsp_test::lsp_client::TestClient { - let mut client = start_test_client().await; - - client.initialize().await; - client.recv_response().await; - - client -} - -#[cfg(test)] -mod tests { - use super::*; - use assert_matches::assert_matches; - use tower_lsp::lsp_types; - - #[tests_macros::lsp_test] - async fn test_init() { - let mut client = start_test_client().await; - - client.initialize().await; - - let value = client.recv_response().await; - let value: lsp_types::InitializeResult = - serde_json::from_value(value.result().unwrap().clone()).unwrap(); - - assert_matches!( - value, - lsp_types::InitializeResult { - capabilities, - server_info - } => { - assert_matches!(capabilities, ServerCapabilities { - position_encoding, - text_document_sync, - .. - } => { - assert_eq!(position_encoding, Some(PositionEncodingKind::UTF16)); - assert_eq!(text_document_sync, Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::INCREMENTAL))); - }); - - assert_matches!(server_info, Some(ServerInfo { name, version }) => { - assert!(name.contains("Air Language Server")); - assert!(version.is_some()); - }); - } - ); - - client - } -} diff --git a/crates/lsp/src/tower_lsp_test_client.rs b/crates/lsp/src/tower_lsp_test_client.rs deleted file mode 100644 index cae23ed9..00000000 --- a/crates/lsp/src/tower_lsp_test_client.rs +++ /dev/null @@ -1,122 +0,0 @@ -use biome_text_size::TextRange; -use lsp_test::lsp_client::TestClient; -use tower_lsp::lsp_types; - -use crate::{documents::Document, from_proto, to_proto}; - -pub(crate) trait TestClientExt { - async fn open_document(&mut self, doc: &Document) -> lsp_types::TextDocumentItem; - - async fn format_document(&mut self, doc: &Document) -> String; - async fn format_document_range(&mut self, doc: &Document, range: TextRange) -> String; - async fn format_document_edits(&mut self, doc: &Document) -> Option>; - async fn format_document_range_edits( - &mut self, - doc: &Document, - range: TextRange, - ) -> Option>; -} - -impl TestClientExt for TestClient { - async fn open_document(&mut self, doc: &Document) -> lsp_types::TextDocumentItem { - let path = format!("test://{}", uuid::Uuid::new_v4()); - let uri = url::Url::parse(&path).unwrap(); - - let text_document = lsp_types::TextDocumentItem { - uri, - language_id: String::from("r"), - version: 0, - text: doc.contents.clone(), - }; - - let params = lsp_types::DidOpenTextDocumentParams { - text_document: text_document.clone(), - }; - self.did_open_text_document(params).await; - - text_document - } - - async fn format_document(&mut self, doc: &Document) -> String { - let edits = self.format_document_edits(doc).await.unwrap(); - from_proto::apply_text_edits(doc, edits).unwrap() - } - - async fn format_document_range(&mut self, doc: &Document, range: TextRange) -> String { - let Some(edits) = self.format_document_range_edits(doc, range).await else { - return doc.contents.clone(); - }; - from_proto::apply_text_edits(doc, edits).unwrap() - } - - async fn format_document_edits(&mut self, doc: &Document) -> Option> { - let lsp_doc = self.open_document(doc).await; - - let options = lsp_types::FormattingOptions { - tab_size: 4, - insert_spaces: false, - ..Default::default() - }; - - self.formatting(lsp_types::DocumentFormattingParams { - text_document: lsp_types::TextDocumentIdentifier { - uri: lsp_doc.uri.clone(), - }, - options, - work_done_progress_params: Default::default(), - }) - .await; - - let response = self.recv_response().await; - - if let Some(err) = response.error() { - panic!("Unexpected error: {}", err.message); - }; - - let value: Option> = - serde_json::from_value(response.result().unwrap().clone()).unwrap(); - - self.close_document(lsp_doc.uri).await; - - value - } - - async fn format_document_range_edits( - &mut self, - doc: &Document, - range: TextRange, - ) -> Option> { - let lsp_doc = self.open_document(doc).await; - - let options = lsp_types::FormattingOptions { - tab_size: 4, - insert_spaces: false, - ..Default::default() - }; - - let range = to_proto::range(&doc.line_index.index, range, doc.line_index.encoding).unwrap(); - - self.range_formatting(lsp_types::DocumentRangeFormattingParams { - text_document: lsp_types::TextDocumentIdentifier { - uri: lsp_doc.uri.clone(), - }, - range, - options, - work_done_progress_params: Default::default(), - }) - .await; - - let response = self.recv_response().await; - - if let Some(err) = response.error() { - panic!("Unexpected error: {}", err.message); - }; - - let value: Option> = - serde_json::from_value(response.result().unwrap().clone()).unwrap(); - - self.close_document(lsp_doc.uri).await; - - value - } -} diff --git a/crates/lsp/src/workspaces.rs b/crates/lsp/src/workspaces.rs deleted file mode 100644 index 1d64eae2..00000000 --- a/crates/lsp/src/workspaces.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::path::Path; -use std::path::PathBuf; - -use tower_lsp::lsp_types::WorkspaceFolder; -use url::Url; -use workspace::resolve::PathResolver; -use workspace::resolve::SettingsResolver; -use workspace::settings::Settings; - -/// Resolver for retrieving [`Settings`] associated with a workspace specific [`Path`] -#[derive(Debug, Default)] -pub(crate) struct WorkspaceSettingsResolver { - /// Resolves a `path` to the closest workspace specific `SettingsResolver`. - /// That `SettingsResolver` can then return `Settings` for the `path`. - path_to_settings_resolver: PathResolver, -} - -impl WorkspaceSettingsResolver { - /// Construct a new workspace settings resolver from an initial set of workspace folders - pub(crate) fn from_workspace_folders( - workspace_folders: Vec, - fallback: Settings, - ) -> Self { - let settings_resolver_fallback = SettingsResolver::new(fallback.clone()); - let path_to_settings_resolver = PathResolver::new(settings_resolver_fallback); - - let mut resolver = Self { - path_to_settings_resolver, - }; - - // Add each workspace folder's settings into the resolver. - // If we fail for any reason (i.e. parse failure of an `air.toml`) then - // we log an error and try to resolve the remaining workspace folders. We don't want - // to propagate an error here because we don't want to prevent the server from - // starting up entirely. - // TODO: This is one place it would be nice to show a toast notification back - // to the user, but we probably need to add support to the Aux thread for that? - for workspace_folder in workspace_folders { - if let Err(error) = - resolver.open_workspace_folder(&workspace_folder.uri, fallback.clone()) - { - tracing::error!( - "Failed to load workspace settings for '{uri}':\n{error}", - uri = workspace_folder.uri, - error = error - ); - } - } - - resolver - } - - pub(crate) fn open_workspace_folder( - &mut self, - url: &Url, - fallback: Settings, - ) -> anyhow::Result<()> { - let path = match Self::url_to_path(url)? { - Some(path) => path, - None => { - tracing::warn!("Ignoring non-file workspace URL: {url}"); - return Ok(()); - } - }; - - let mut settings_resolver = SettingsResolver::new(fallback); - settings_resolver.load_from_paths(&[&path])?; - - tracing::trace!("Adding workspace settings: {}", path.display()); - self.path_to_settings_resolver.add(&path, settings_resolver); - - Ok(()) - } - - pub(crate) fn close_workspace_folder( - &mut self, - url: &Url, - ) -> anyhow::Result> { - match Self::url_to_path(url)? { - Some(path) => { - tracing::trace!("Removing workspace settings: {}", path.display()); - Ok(self.path_to_settings_resolver.remove(&path)) - } - None => { - tracing::warn!("Ignoring non-file workspace URL: {url}"); - Ok(None) - } - } - } - - /// Return the appropriate [`Settings`] for a given document [`Url`]. - pub(crate) fn settings_for_url(&self, url: &Url) -> &Settings { - if let Ok(Some(path)) = Self::url_to_path(url) { - return self.settings_for_path(&path); - } - - // For `untitled` schemes, we have special behavior. - // If there is exactly 1 workspace, we resolve using a path of - // `{workspace_path}/untitled` to provide relevant settings for this workspace. - if url.scheme() == "untitled" && self.path_to_settings_resolver.len() == 1 { - tracing::trace!("Using workspace settings for 'untitled' URL: {url}"); - let workspace_path = self.path_to_settings_resolver.keys().next().unwrap(); - let path = workspace_path.join("untitled"); - return self.settings_for_path(&path); - } - - tracing::trace!("Using default settings for non-file URL: {url}"); - self.path_to_settings_resolver.fallback().fallback() - } - - /// Reloads all workspaces matched by the [`Url`] - /// - /// This is utilized by the watched files handler to reload the settings - /// resolver whenever an `air.toml` is modified. - pub(crate) fn reload_workspaces_matched_by_url(&mut self, url: &Url) { - let path = match Self::url_to_path(url) { - Ok(Some(path)) => path, - Ok(None) => { - tracing::trace!("Ignoring non-`file` changed URL: {url}"); - return; - } - Err(error) => { - tracing::error!("Failed to reload workspaces associated with {url}:\n{error}"); - return; - } - }; - - if !path.ends_with("air.toml") { - // We could get called with a changed file that isn't an `air.toml` if we are - // watching more than `air.toml` files - tracing::trace!("Ignoring non-`air.toml` changed URL: {url}"); - return; - } - - for (workspace_path, settings_resolver) in self.path_to_settings_resolver.matches_mut(&path) - { - tracing::trace!("Reloading workspace settings: {}", workspace_path.display()); - - settings_resolver.clear(); - - if let Err(error) = settings_resolver.load_from_paths(&[workspace_path]) { - tracing::error!( - "Failed to reload workspace settings for {path}:\n{error}", - path = workspace_path.display(), - error = error - ); - } - } - } - - /// Return the appropriate [`Settings`] for a given [`Path`]. - /// - /// This actually performs a double resolution. It first resolves to the - /// workspace specific `SettingsResolver` that matches this path, and then uses that - /// resolver to actually resolve the `Settings` for this path. We do it this way - /// to ensure we can easily add and remove workspaces (including all of their - /// hierarchical paths). - fn settings_for_path(&self, path: &Path) -> &Settings { - let settings_resolver = self.path_to_settings_resolver.resolve_or_fallback(path); - settings_resolver.resolve_or_fallback(path) - } - - fn url_to_path(url: &Url) -> anyhow::Result> { - if url.scheme() != "file" { - return Ok(None); - } - - let path = url - .to_file_path() - .map_err(|()| anyhow::anyhow!("Failed to convert workspace URL to file path: {url}"))?; - - Ok(Some(path)) - } -} diff --git a/crates/lsp_test/Cargo.toml b/crates/lsp_test/Cargo.toml deleted file mode 100644 index c76e8be8..00000000 --- a/crates/lsp_test/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "lsp_test" -version = "0.0.0" -publish = false -authors.workspace = true -categories.workspace = true -edition.workspace = true -homepage.workspace = true -keywords.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true - -[dependencies] -bytes.workspace = true -futures.workspace = true -futures-util.workspace = true -httparse.workspace = true -memchr.workspace = true -serde.workspace = true -serde_json.workspace = true -tokio = { workspace = true, features = ["full"] } -tokio-util.workspace = true -tower-lsp.workspace = true -tracing.workspace = true -url.workspace = true - -[lints] -workspace = true diff --git a/crates/lsp_test/src/lib.rs b/crates/lsp_test/src/lib.rs deleted file mode 100644 index 91bd341f..00000000 --- a/crates/lsp_test/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod lsp_client; - -pub(crate) mod tower_lsp; diff --git a/crates/lsp_test/src/lsp_client.rs b/crates/lsp_test/src/lsp_client.rs deleted file mode 100644 index dc6cd1da..00000000 --- a/crates/lsp_test/src/lsp_client.rs +++ /dev/null @@ -1,163 +0,0 @@ -// -// lsp_client.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -use futures::StreamExt; -use futures_util::sink::SinkExt; -use std::future::Future; -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio::io::{ReadHalf, SimplexStream, WriteHalf}; -use tokio_util::codec::{FramedRead, FramedWrite}; -use tower_lsp::lsp_types::ClientInfo; -use tower_lsp::{jsonrpc, lsp_types}; - -use crate::tower_lsp::codec::LanguageServerCodec; -use crate::tower_lsp::request::Request; - -pub struct TestClient { - pub rx: FramedRead, LanguageServerCodec>, - pub tx: FramedWrite, LanguageServerCodec>, - - server_handle: Option>, - id_counter: i64, - - init_params: Option, -} - -impl TestClient { - pub fn new(start: F) -> Self - where - F: FnOnce(Box, Box) -> Fut, - Fut: Future + Send + 'static, - { - let (client_rx, client_tx) = tokio::io::simplex(1024); - let (server_rx, server_tx) = tokio::io::simplex(1024); - - let server_handle = tokio::spawn(start(Box::new(server_rx), Box::new(client_tx))); - - let rx = FramedRead::new(client_rx, LanguageServerCodec::default()); - let tx = FramedWrite::new(server_tx, LanguageServerCodec::default()); - - Self { - rx, - tx, - server_handle: Some(server_handle), - id_counter: 0, - init_params: None, - } - } - - // `jsonrpc::Id` requires i64 IDs - fn id(&mut self) -> i64 { - let id = self.id_counter; - self.id_counter = id + 1; - id - } - - pub async fn recv_response(&mut self) -> jsonrpc::Response { - // Unwrap: Option (None if stream closed), then Result (Err if codec fails). - self.rx.next().await.unwrap().unwrap() - } - - pub async fn notify(&mut self, params: N::Params) - where - N: lsp_types::notification::Notification, - { - let not = Request::from_notification::(params); - - // Unwrap: For this test client it's fine to panic if we can't send - self.tx.send(not).await.unwrap(); - } - - pub async fn request(&mut self, params: R::Params) -> i64 - where - R: lsp_types::request::Request, - { - let id = self.id(); - let req = Request::from_request::(jsonrpc::Id::Number(id), params); - - // Unwrap: For this test client it's fine to panic if we can't send - self.tx.send(req).await.unwrap(); - - id - } - - pub async fn initialize(&mut self) -> i64 { - let params: Option = std::mem::take(&mut self.init_params); - let params = params.unwrap_or_default(); - let params = Self::with_client_info(params); - self.request::(params).await - } - - // Regardless of how we got the params, ensure the client name is set to - // `AirTestClient` so we can recognize it when we set up global logging. - fn with_client_info( - mut init_params: lsp_types::InitializeParams, - ) -> lsp_types::InitializeParams { - init_params.client_info = Some(ClientInfo { - name: String::from("AirTestClient"), - version: None, - }); - init_params - } - - pub fn with_initialize_params(&mut self, init_params: lsp_types::InitializeParams) { - self.init_params = Some(init_params); - } - - pub async fn close_document(&mut self, uri: url::Url) { - let params = lsp_types::DidCloseTextDocumentParams { - text_document: lsp_types::TextDocumentIdentifier { uri }, - }; - self.did_close_text_document(params).await; - } - - pub async fn shutdown(&mut self) { - // TODO: Check that no messages are incoming - - // Don't use `Request::from_request()`. It has a bug with undefined - // params (when `R::Params = ()`) which causes tower-lsp to not - // recognise the Shutdown request. - let req = Request::build("shutdown").id(self.id()).finish(); - - // Unwrap: For this test client it's fine to panic if we can't send - self.tx.send(req).await.unwrap(); - self.recv_response().await; - } - - pub async fn exit(&mut self) { - // Unwrap: Can only exit once - let handle = std::mem::take(&mut self.server_handle).unwrap(); - - self.notify::(()).await; - - // Now wait for the server task to complete. - // Unwrap: Panics if task can't shut down as expected - handle.await.unwrap(); - } - - pub async fn did_open_text_document(&mut self, params: lsp_types::DidOpenTextDocumentParams) { - self.notify::(params) - .await - } - - pub async fn did_close_text_document(&mut self, params: lsp_types::DidCloseTextDocumentParams) { - self.notify::(params) - .await - } - - pub async fn formatting(&mut self, params: lsp_types::DocumentFormattingParams) -> i64 { - self.request::(params).await - } - - pub async fn range_formatting( - &mut self, - params: lsp_types::DocumentRangeFormattingParams, - ) -> i64 { - self.request::(params) - .await - } -} diff --git a/crates/lsp_test/src/tower_lsp/codec.rs b/crates/lsp_test/src/tower_lsp/codec.rs deleted file mode 100644 index 38f2af83..00000000 --- a/crates/lsp_test/src/tower_lsp/codec.rs +++ /dev/null @@ -1,398 +0,0 @@ -// --- source -// authors = ["Eyal Kalderon "] -// origin = "https://github.com/ebkalderon/tower-lsp/blob/master/src/codec.r" -// license = "MIT OR Apache-2.0" -// --- - -//! Encoder and decoder for Language Server Protocol messages. - -use std::error::Error; -use std::fmt::{self, Display, Formatter}; -use std::io::{Error as IoError, Write}; -use std::marker::PhantomData; -use std::num::ParseIntError; -use std::str::Utf8Error; - -use bytes::buf::BufMut; -use bytes::{Buf, BytesMut}; -use memchr::memmem; -use serde::{de::DeserializeOwned, Serialize}; -use tracing::warn; - -use tokio_util::codec::{Decoder, Encoder}; - -/// Errors that can occur when processing an LSP message. -#[derive(Debug)] -pub enum ParseError { - /// Failed to parse the JSON body. - Body(serde_json::Error), - /// Failed to encode the response. - Encode(IoError), - /// Failed to parse headers. - Headers(httparse::Error), - /// The media type in the `Content-Type` header is invalid. - InvalidContentType, - /// The length value in the `Content-Length` header is invalid. - InvalidContentLength(ParseIntError), - /// Request lacks the required `Content-Length` header. - MissingContentLength, - /// Request contains invalid UTF8. - Utf8(Utf8Error), -} - -impl Display for ParseError { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match *self { - ParseError::Body(ref e) => write!(f, "unable to parse JSON body: {e}"), - ParseError::Encode(ref e) => write!(f, "failed to encode response: {e}"), - ParseError::Headers(ref e) => write!(f, "failed to parse headers: {e}"), - ParseError::InvalidContentType => write!(f, "unable to parse content type"), - ParseError::InvalidContentLength(ref e) => { - write!(f, "unable to parse content length: {e}") - } - ParseError::MissingContentLength => { - write!(f, "missing required `Content-Length` header") - } - ParseError::Utf8(ref e) => write!(f, "request contains invalid UTF8: {e}"), - } - } -} - -impl Error for ParseError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match *self { - ParseError::Body(ref e) => Some(e), - ParseError::Encode(ref e) => Some(e), - ParseError::InvalidContentLength(ref e) => Some(e), - ParseError::Utf8(ref e) => Some(e), - _ => None, - } - } -} - -impl From for ParseError { - fn from(error: serde_json::Error) -> Self { - ParseError::Body(error) - } -} - -impl From for ParseError { - fn from(error: IoError) -> Self { - ParseError::Encode(error) - } -} - -impl From for ParseError { - fn from(error: httparse::Error) -> Self { - ParseError::Headers(error) - } -} - -impl From for ParseError { - fn from(error: ParseIntError) -> Self { - ParseError::InvalidContentLength(error) - } -} - -impl From for ParseError { - fn from(error: Utf8Error) -> Self { - ParseError::Utf8(error) - } -} - -/// Encodes and decodes Language Server Protocol messages. -pub struct LanguageServerCodec { - content_len: Option, - _marker: PhantomData, -} - -impl Default for LanguageServerCodec { - fn default() -> Self { - LanguageServerCodec { - content_len: None, - _marker: PhantomData, - } - } -} - -impl Encoder for LanguageServerCodec { - type Error = ParseError; - - fn encode(&mut self, item: T, dst: &mut BytesMut) -> Result<(), Self::Error> { - let msg = serde_json::to_string(&item)?; - // trace!("-> {}", msg); - - // Reserve just enough space to hold the `Content-Length: ` and `\r\n\r\n` constants, - // the length of the message, and the message body. - dst.reserve(msg.len() + number_of_digits(msg.len()) + 20); - let mut writer = dst.writer(); - write!(writer, "Content-Length: {}\r\n\r\n{}", msg.len(), msg)?; - writer.flush()?; - - Ok(()) - } -} - -fn number_of_digits(mut n: usize) -> usize { - let mut num_digits = 0; - - while n > 0 { - n /= 10; - num_digits += 1; - } - - num_digits -} - -impl Decoder for LanguageServerCodec { - type Item = T; - type Error = ParseError; - - fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - if let Some(content_len) = self.content_len { - if src.len() < content_len { - return Ok(None); - } - - let bytes = &src[..content_len]; - let message = std::str::from_utf8(bytes)?; - - let result = if message.is_empty() { - Ok(None) - } else { - // trace!("<- {}", message); - match serde_json::from_str(message) { - Ok(parsed) => Ok(Some(parsed)), - Err(err) => Err(err.into()), - } - }; - - src.advance(content_len); - self.content_len = None; // Reset state in preparation for parsing next message. - - result - } else { - let mut dst = [httparse::EMPTY_HEADER; 2]; - - let (headers_len, headers) = match httparse::parse_headers(src, &mut dst)? { - httparse::Status::Complete(output) => output, - httparse::Status::Partial => return Ok(None), - }; - - match decode_headers(headers) { - Ok(content_len) => { - src.advance(headers_len); - self.content_len = Some(content_len); - self.decode(src) // Recurse right back in, now that `Content-Length` is known. - } - Err(err) => { - match err { - ParseError::MissingContentLength => {} - _ => src.advance(headers_len), - } - - // Skip any garbage bytes by scanning ahead for another potential message. - src.advance(memmem::find(src, b"Content-Length").unwrap_or_default()); - Err(err) - } - } - } - } -} - -fn decode_headers(headers: &[httparse::Header<'_>]) -> Result { - let mut content_len = None; - - for header in headers { - match header.name { - "Content-Length" => { - let string = std::str::from_utf8(header.value)?; - let parsed_len = string.parse()?; - content_len = Some(parsed_len); - } - "Content-Type" => { - let string = std::str::from_utf8(header.value)?; - let charset = string - .split(';') - .skip(1) - .map(|param| param.trim()) - .find_map(|param| param.strip_prefix("charset=")); - - match charset { - Some("utf-8" | "utf8") => {} - _ => return Err(ParseError::InvalidContentType), - } - } - other => warn!("encountered unsupported header: {:?}", other), - } - } - - if let Some(content_len) = content_len { - Ok(content_len) - } else { - Err(ParseError::MissingContentLength) - } -} - -#[cfg(test)] -mod tests { - use bytes::BytesMut; - use serde_json::Value; - - use super::*; - - macro_rules! assert_err { - ($expression:expr, $($pattern:tt)+) => { - match $expression { - $($pattern)+ => (), - ref e => panic!("expected `{}` but got `{:?}`", stringify!($($pattern)+), e), - } - } - } - - fn encode_message(content_type: Option<&str>, message: &str) -> String { - let content_type = content_type - .map(|ty| format!("\r\nContent-Type: {ty}")) - .unwrap_or_default(); - - format!( - "Content-Length: {}{}\r\n\r\n{}", - message.len(), - content_type, - message - ) - } - - #[test] - fn encode_and_decode() { - let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#; - let encoded = encode_message(None, decoded); - - let mut codec = LanguageServerCodec::default(); - let mut buffer = BytesMut::new(); - let item: Value = serde_json::from_str(decoded).unwrap(); - codec.encode(item, &mut buffer).unwrap(); - assert_eq!(buffer, BytesMut::from(encoded.as_str())); - - let mut buffer = BytesMut::from(encoded.as_str()); - let message = codec.decode(&mut buffer).unwrap(); - let decoded = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(decoded)); - } - - #[test] - fn decodes_optional_content_type() { - let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#; - let content_type = "application/vscode-jsonrpc; charset=utf-8"; - let encoded = encode_message(Some(content_type), decoded); - - let mut codec = LanguageServerCodec::default(); - let mut buffer = BytesMut::from(encoded.as_str()); - let message = codec.decode(&mut buffer).unwrap(); - let decoded_: Value = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(decoded_)); - - let content_type = "application/vscode-jsonrpc; charset=utf8"; - let encoded = encode_message(Some(content_type), decoded); - - let mut buffer = BytesMut::from(encoded.as_str()); - let message = codec.decode(&mut buffer).unwrap(); - let decoded_: Value = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(decoded_)); - - let content_type = "application/vscode-jsonrpc; charset=invalid"; - let encoded = encode_message(Some(content_type), decoded); - - let mut buffer = BytesMut::from(encoded.as_str()); - assert_err!( - codec.decode(&mut buffer), - Err(ParseError::InvalidContentType) - ); - - let content_type = "application/vscode-jsonrpc"; - let encoded = encode_message(Some(content_type), decoded); - - let mut buffer = BytesMut::from(encoded.as_str()); - assert_err!( - codec.decode(&mut buffer), - Err(ParseError::InvalidContentType) - ); - - let content_type = "this-mime-should-be-ignored; charset=utf8"; - let encoded = encode_message(Some(content_type), decoded); - - let mut buffer = BytesMut::from(encoded.as_str()); - let message = codec.decode(&mut buffer).unwrap(); - let decoded_: Value = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(decoded_)); - } - - #[test] - fn decodes_zero_length_message() { - let content_type = "application/vscode-jsonrpc; charset=utf-8"; - let encoded = encode_message(Some(content_type), ""); - - let mut codec = LanguageServerCodec::default(); - let mut buffer = BytesMut::from(encoded.as_str()); - let message: Option = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - } - - #[test] - fn recovers_from_parse_error() { - let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#; - let encoded = encode_message(None, decoded); - let mixed = format!("foobar{encoded}Content-Length: foobar\r\n\r\n{encoded}"); - - let mut codec = LanguageServerCodec::default(); - let mut buffer = BytesMut::from(mixed.as_str()); - assert_err!( - codec.decode(&mut buffer), - Err(ParseError::MissingContentLength) - ); - - let message: Option = codec.decode(&mut buffer).unwrap(); - let first_valid = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(first_valid)); - assert_err!( - codec.decode(&mut buffer), - Err(ParseError::InvalidContentLength(_)) - ); - - let message = codec.decode(&mut buffer).unwrap(); - let second_valid = serde_json::from_str(decoded).unwrap(); - assert_eq!(message, Some(second_valid)); - - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - } - - #[test] - fn decodes_small_chunks() { - let decoded = r#"{"jsonrpc":"2.0","method":"exit"}"#; - let content_type = "application/vscode-jsonrpc; charset=utf-8"; - let encoded = encode_message(Some(content_type), decoded); - - let mut codec = LanguageServerCodec::default(); - let mut buffer = BytesMut::from(encoded.as_str()); - - let rest = buffer.split_off(40); - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - buffer.unsplit(rest); - - let rest = buffer.split_off(80); - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - buffer.unsplit(rest); - - let rest = buffer.split_off(16); - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, None); - buffer.unsplit(rest); - - let decoded: Value = serde_json::from_str(decoded).unwrap(); - let message = codec.decode(&mut buffer).unwrap(); - assert_eq!(message, Some(decoded)); - } -} diff --git a/crates/lsp_test/src/tower_lsp/mod.rs b/crates/lsp_test/src/tower_lsp/mod.rs deleted file mode 100644 index 4733be92..00000000 --- a/crates/lsp_test/src/tower_lsp/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub(crate) mod codec; - -#[allow(dead_code)] -pub(crate) mod request; diff --git a/crates/lsp_test/src/tower_lsp/request.rs b/crates/lsp_test/src/tower_lsp/request.rs deleted file mode 100644 index d5c68773..00000000 --- a/crates/lsp_test/src/tower_lsp/request.rs +++ /dev/null @@ -1,217 +0,0 @@ -// --- source -// authors = ["Eyal Kalderon "] -// origin = "https://github.com/ebkalderon/tower-lsp/blob/master/src/jsonrpc/request.rs" -// license = "MIT OR Apache-2.0" -// --- - -use std::borrow::Cow; -use std::fmt::{self, Display, Formatter}; -use std::str::FromStr; - -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::Value; - -// use super::{Id, Version}; -use tower_lsp::jsonrpc::Id; -use tower_lsp::lsp_types; - -#[derive(Clone, Debug, PartialEq)] -struct Version; - -impl<'de> Deserialize<'de> for Version { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct Inner<'a>(#[serde(borrow)] Cow<'a, str>); - - let Inner(ver) = Inner::deserialize(deserializer)?; - - match ver.as_ref() { - "2.0" => Ok(Version), - _ => Err(de::Error::custom("expected JSON-RPC version \"2.0\"")), - } - } -} - -impl Serialize for Version { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str("2.0") - } -} - -fn deserialize_some<'de, T, D>(deserializer: D) -> Result, D::Error> -where - T: Deserialize<'de>, - D: Deserializer<'de>, -{ - T::deserialize(deserializer).map(Some) -} - -/// A JSON-RPC request or notification. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct Request { - jsonrpc: Version, - #[serde(default)] - method: Cow<'static, str>, - #[serde(default, deserialize_with = "deserialize_some")] - #[serde(skip_serializing_if = "Option::is_none")] - params: Option, - #[serde(default, deserialize_with = "deserialize_some")] - #[serde(skip_serializing_if = "Option::is_none")] - id: Option, -} - -impl Request { - /// Starts building a JSON-RPC method call. - /// - /// Returns a `RequestBuilder`, which allows setting the `params` field or adding a request ID. - pub fn build(method: M) -> RequestBuilder - where - M: Into>, - { - RequestBuilder { - method: method.into(), - params: None, - id: None, - } - } - - /// Constructs a JSON-RPC request from its corresponding LSP type. - /// - /// # Panics - /// - /// Panics if `params` could not be serialized into a [`serde_json::Value`]. Since the - /// [`lsp_types::request::Request`] trait promises this invariant is upheld, this should never - /// happen in practice (unless the trait was implemented incorrectly). - pub(crate) fn from_request(id: Id, params: R::Params) -> Self - where - R: lsp_types::request::Request, - { - Request { - jsonrpc: Version, - method: R::METHOD.into(), - params: Some(serde_json::to_value(params).unwrap()), - id: Some(id), - } - } - - /// Constructs a JSON-RPC notification from its corresponding LSP type. - /// - /// # Panics - /// - /// Panics if `params` could not be serialized into a [`serde_json::Value`]. Since the - /// [`lsp_types::notification::Notification`] trait promises this invariant is upheld, this - /// should never happen in practice (unless the trait was implemented incorrectly). - pub(crate) fn from_notification(params: N::Params) -> Self - where - N: lsp_types::notification::Notification, - { - Request { - jsonrpc: Version, - method: N::METHOD.into(), - params: Some(serde_json::to_value(params).unwrap()), - id: None, - } - } - - /// Returns the name of the method to be invoked. - pub fn method(&self) -> &str { - self.method.as_ref() - } - - /// Returns the unique ID of this request, if present. - pub fn id(&self) -> Option<&Id> { - self.id.as_ref() - } - - /// Returns the `params` field, if present. - pub fn params(&self) -> Option<&Value> { - self.params.as_ref() - } - - /// Splits this request into the method name, request ID, and the `params` field, if present. - pub fn into_parts(self) -> (Cow<'static, str>, Option, Option) { - (self.method, self.id, self.params) - } -} - -impl Display for Request { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - use std::{io, str}; - - struct WriterFormatter<'a, 'b: 'a> { - inner: &'a mut Formatter<'b>, - } - - impl io::Write for WriterFormatter<'_, '_> { - fn write(&mut self, buf: &[u8]) -> io::Result { - fn io_error(_: E) -> io::Error { - // Error value does not matter because fmt::Display impl below just - // maps it to fmt::Error - io::Error::new(io::ErrorKind::Other, "fmt error") - } - let s = str::from_utf8(buf).map_err(io_error)?; - self.inner.write_str(s).map_err(io_error)?; - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } - } - - let mut w = WriterFormatter { inner: f }; - serde_json::to_writer(&mut w, self).map_err(|_| fmt::Error) - } -} - -impl FromStr for Request { - type Err = serde_json::Error; - - fn from_str(s: &str) -> Result { - serde_json::from_str(s) - } -} - -/// A builder to construct the properties of a `Request`. -/// -/// To construct a `RequestBuilder`, refer to [`Request::build`]. -#[derive(Debug)] -pub struct RequestBuilder { - method: Cow<'static, str>, - params: Option, - id: Option, -} - -impl RequestBuilder { - /// Sets the `id` member of the request to the given value. - /// - /// If this method is not called, the resulting `Request` will be assumed to be a notification. - pub fn id>(mut self, id: I) -> Self { - self.id = Some(id.into()); - self - } - - /// Sets the `params` member of the request to the given value. - /// - /// This member is omitted from the request by default. - pub fn params>(mut self, params: V) -> Self { - self.params = Some(params.into()); - self - } - - /// Constructs the JSON-RPC request and returns it. - pub fn finish(self) -> Request { - Request { - jsonrpc: Version, - method: self.method, - params: self.params, - id: self.id, - } - } -} diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index b845ead7..67d3ab8e 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -193,7 +193,6 @@ impl Server { Ok(()) } - // TODO: Add in other dynamic configuration fn try_register_capabilities( resolved_client_capabilities: &ResolvedClientCapabilities, scheduler: &mut Scheduler, diff --git a/crates/tests_macros/src/lib.rs b/crates/tests_macros/src/lib.rs index 3ff3db27..8b83af54 100644 --- a/crates/tests_macros/src/lib.rs +++ b/crates/tests_macros/src/lib.rs @@ -252,24 +252,3 @@ pub fn gen_tests(input: TokenStream) -> TokenStream { Err(e) => abort!(e, "{}", e), } } - -// We can't effectively interact with the server task and shut it down from `Drop` -// so we do it on teardown in this procedural macro. Users must return their client. -#[proc_macro_attribute] -pub fn lsp_test(_attr: TokenStream, item: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(item as syn::ItemFn); - - let name = &input.sig.ident; - let block = &input.block; - - let expanded = quote! { - #[tokio::test] - async fn #name() { - let mut client = #block; - client.shutdown().await; - client.exit().await; - } - }; - - TokenStream::from(expanded) -} From 80fb2763c4a008d1502a0c04e2ed01e3911a2e1b Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:40:57 -0500 Subject: [PATCH 16/44] Rename `ruff_source_file` to `source_file` --- Cargo.lock | 28 +++++++++---------- Cargo.toml | 2 +- crates/air/Cargo.toml | 2 +- crates/air/src/commands/format.rs | 2 +- crates/air_formatter_test/Cargo.toml | 2 +- crates/air_formatter_test/src/spec.rs | 2 +- crates/air_r_parser/Cargo.toml | 2 +- crates/air_r_parser/tests/spec_test.rs | 2 +- crates/ruff_server/Cargo.toml | 2 +- crates/ruff_server/src/edit/text_document.rs | 8 +++--- crates/ruff_server/src/proto/text_edit.rs | 4 +-- crates/ruff_server/src/proto/text_range.rs | 2 +- crates/ruff_server/src/proto/text_size.rs | 4 +-- .../Cargo.toml | 2 +- .../src/lib.rs | 0 .../src/line_index.rs | 8 +++--- .../src/newlines.rs | 0 .../src/one_indexed.rs | 0 .../src/source_location.rs | 0 crates/workspace/Cargo.toml | 2 +- crates/workspace/src/settings.rs | 8 +++--- 21 files changed, 41 insertions(+), 41 deletions(-) rename crates/{ruff_source_file => source_file}/Cargo.toml (94%) rename crates/{ruff_source_file => source_file}/src/lib.rs (100%) rename crates/{ruff_source_file => source_file}/src/line_index.rs (98%) rename crates/{ruff_source_file => source_file}/src/newlines.rs (100%) rename crates/{ruff_source_file => source_file}/src/one_indexed.rs (100%) rename crates/{ruff_source_file => source_file}/src/source_location.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 55efb6f4..f7f20d15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,7 +43,7 @@ dependencies = [ "itertools", "lsp-server", "ruff_server", - "ruff_source_file", + "source_file", "tempfile", "thiserror 2.0.5", "tracing", @@ -60,10 +60,10 @@ dependencies = [ "biome_parser", "biome_rowan", "insta", - "ruff_source_file", "serde", "similar", "similar-asserts", + "source_file", ] [[package]] @@ -101,8 +101,8 @@ dependencies = [ "biome_rowan", "biome_unicode_table", "insta", - "ruff_source_file", "serde", + "source_file", "tests_macros", "tracing", "tree-sitter", @@ -1438,11 +1438,11 @@ dependencies = [ "lsp-server", "lsp-types", "regex", - "ruff_source_file", "rustc-hash", "serde", "serde_json", "server_test", + "source_file", "thiserror 2.0.5", "tracing", "tracing-subscriber", @@ -1453,15 +1453,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "ruff_source_file" -version = "0.0.0" -dependencies = [ - "biome_text_size", - "memchr", - "serde", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1706,6 +1697,15 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "source_file" +version = "0.0.0" +dependencies = [ + "biome_text_size", + "memchr", + "serde", +] + [[package]] name = "spin" version = "0.9.8" @@ -2286,9 +2286,9 @@ dependencies = [ "fs", "ignore", "insta", - "ruff_source_file", "rustc-hash", "serde", + "source_file", "tempfile", "thiserror 2.0.5", "toml", diff --git a/Cargo.toml b/Cargo.toml index 2ae6bbcd..e2a61ad7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,8 +30,8 @@ biome_ungrammar = { path = "./crates/biome_ungrammar" } fs = { path = "./crates/fs" } tests_macros = { path = "./crates/tests_macros" } ruff_server = { path = "./crates/ruff_server" } -ruff_source_file = { path = "./crates/ruff_source_file" } server_test = { path = "./crates/server_test" } +source_file = { path = "./crates/source_file" } workspace = { path = "./crates/workspace" } anyhow = "1.0.89" diff --git a/crates/air/Cargo.toml b/crates/air/Cargo.toml index d2897f87..102edb6e 100644 --- a/crates/air/Cargo.toml +++ b/crates/air/Cargo.toml @@ -25,7 +25,7 @@ ignore = { workspace = true } itertools = { workspace = true } lsp-server = { workspace = true } ruff_server = { workspace = true } -ruff_source_file = { workspace = true } +source_file = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } workspace = { workspace = true } diff --git a/crates/air/src/commands/format.rs b/crates/air/src/commands/format.rs index 949f13df..ae944c6d 100644 --- a/crates/air/src/commands/format.rs +++ b/crates/air/src/commands/format.rs @@ -129,7 +129,7 @@ fn format_file( let options = settings.to_format_options(&source); - let (source, _) = ruff_source_file::normalize_newlines(source); + let (source, _) = source_file::normalize_newlines(source); let formatted = match format_source(source.as_str(), options) { Ok(formatted) => formatted, Err(err) => return Err(FormatCommandError::Format(path.clone(), err)), diff --git a/crates/air_formatter_test/Cargo.toml b/crates/air_formatter_test/Cargo.toml index f61f11b2..15150909 100644 --- a/crates/air_formatter_test/Cargo.toml +++ b/crates/air_formatter_test/Cargo.toml @@ -21,10 +21,10 @@ biome_formatter = { workspace = true } biome_parser = { workspace = true } biome_rowan = { workspace = true } insta = { workspace = true, features = ["glob"] } -ruff_source_file = { workspace = true } serde = { workspace = true, features = ["derive"] } similar = "2.6.0" similar-asserts = "1.6.0" +source_file = { workspace = true } [lints] workspace = true diff --git a/crates/air_formatter_test/src/spec.rs b/crates/air_formatter_test/src/spec.rs index 9dddd2ff..fc82fe5c 100644 --- a/crates/air_formatter_test/src/spec.rs +++ b/crates/air_formatter_test/src/spec.rs @@ -29,7 +29,7 @@ impl<'a> SpecTestFile<'a> { let input_code = std::fs::read_to_string(input_file).unwrap(); // Normalize to Unix line endings - let (input_code, _) = ruff_source_file::normalize_newlines(input_code); + let (input_code, _) = source_file::normalize_newlines(input_code); // For the whole file, not a specific range right now let range_start_index = None; diff --git a/crates/air_r_parser/Cargo.toml b/crates/air_r_parser/Cargo.toml index 491557f7..0e4ab2d7 100644 --- a/crates/air_r_parser/Cargo.toml +++ b/crates/air_r_parser/Cargo.toml @@ -28,7 +28,7 @@ tree-sitter-r = { workspace = true } biome_console = { workspace = true } biome_diagnostics = { workspace = true } insta = { workspace = true } -ruff_source_file = { workspace = true } +source_file = { workspace = true } tests_macros = { workspace = true } # cargo-workspaces metadata diff --git a/crates/air_r_parser/tests/spec_test.rs b/crates/air_r_parser/tests/spec_test.rs index 0a3d570e..c3fef862 100644 --- a/crates/air_r_parser/tests/spec_test.rs +++ b/crates/air_r_parser/tests/spec_test.rs @@ -53,7 +53,7 @@ pub fn run(test_case: &str, _snapshot_name: &str, test_directory: &str, outcome_ .expect("Expected test path to be a readable file in UTF8 encoding"); // Normalize to Unix line endings - let (content, _) = ruff_source_file::normalize_newlines(content); + let (content, _) = source_file::normalize_newlines(content); let options = RParserOptions::default(); let parsed = parse(&content, options); diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index c8770562..fa1ac011 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -16,7 +16,7 @@ air_r_factory = { workspace = true } air_r_formatter = { workspace = true } air_r_parser = { workspace = true } air_r_syntax = { workspace = true } -ruff_source_file = { workspace = true } +source_file = { workspace = true } workspace = { workspace = true } anyhow = { workspace = true } diff --git a/crates/ruff_server/src/edit/text_document.rs b/crates/ruff_server/src/edit/text_document.rs index 04150a48..1fd8ecf5 100644 --- a/crates/ruff_server/src/edit/text_document.rs +++ b/crates/ruff_server/src/edit/text_document.rs @@ -6,8 +6,8 @@ use biome_rowan::TextRange; use lsp_types::TextDocumentContentChangeEvent; -use ruff_source_file::LineEnding; -use ruff_source_file::LineIndex; +use source_file::LineEnding; +use source_file::LineIndex; use crate::edit::PositionEncoding; use crate::proto::TextRangeExt; @@ -51,7 +51,7 @@ impl From<&str> for LanguageId { impl TextDocument { pub fn new(contents: String, version: DocumentVersion) -> Self { // Normalize to Unix line endings - let (contents, ending) = ruff_source_file::normalize_newlines(contents); + let (contents, ending) = source_file::normalize_newlines(contents); let index = LineIndex::from_source_text(&contents); Self { contents, @@ -117,7 +117,7 @@ impl TextDocument { // col] coordinates. for change in &mut changes.iter_mut() { let text = std::mem::take(&mut change.text); - (change.text, _) = ruff_source_file::normalize_newlines(text); + (change.text, _) = source_file::normalize_newlines(text); } if let [lsp_types::TextDocumentContentChangeEvent { diff --git a/crates/ruff_server/src/proto/text_edit.rs b/crates/ruff_server/src/proto/text_edit.rs index 711e92a2..0fcf2eee 100644 --- a/crates/ruff_server/src/proto/text_edit.rs +++ b/crates/ruff_server/src/proto/text_edit.rs @@ -1,5 +1,5 @@ -use ruff_source_file::LineEnding; -use ruff_source_file::LineIndex; +use source_file::LineEnding; +use source_file::LineIndex; use crate::edit::Indel; use crate::edit::PositionEncoding; diff --git a/crates/ruff_server/src/proto/text_range.rs b/crates/ruff_server/src/proto/text_range.rs index 8a6a4fb7..3693f71b 100644 --- a/crates/ruff_server/src/proto/text_range.rs +++ b/crates/ruff_server/src/proto/text_range.rs @@ -8,7 +8,7 @@ use crate::edit::PositionEncoding; use crate::proto::TextSizeExt; use biome_text_size::{TextRange, TextSize}; use lsp_types as types; -use ruff_source_file::LineIndex; +use source_file::LineIndex; // We don't own this type so we need a helper trait pub(crate) trait TextRangeExt { diff --git a/crates/ruff_server/src/proto/text_size.rs b/crates/ruff_server/src/proto/text_size.rs index 218984bf..5ecf9365 100644 --- a/crates/ruff_server/src/proto/text_size.rs +++ b/crates/ruff_server/src/proto/text_size.rs @@ -2,8 +2,8 @@ use crate::edit::PositionEncoding; use biome_rowan::TextRange; use biome_text_size::TextSize; use lsp_types as types; -use ruff_source_file::OneIndexed; -use ruff_source_file::{LineIndex, SourceLocation}; +use source_file::OneIndexed; +use source_file::{LineIndex, SourceLocation}; // We don't own this type so we need a helper trait pub(crate) trait TextSizeExt { diff --git a/crates/ruff_source_file/Cargo.toml b/crates/source_file/Cargo.toml similarity index 94% rename from crates/ruff_source_file/Cargo.toml rename to crates/source_file/Cargo.toml index 95b19909..87529da5 100644 --- a/crates/ruff_source_file/Cargo.toml +++ b/crates/source_file/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ruff_source_file" +name = "source_file" version = "0.0.0" publish = false authors = { workspace = true } diff --git a/crates/ruff_source_file/src/lib.rs b/crates/source_file/src/lib.rs similarity index 100% rename from crates/ruff_source_file/src/lib.rs rename to crates/source_file/src/lib.rs diff --git a/crates/ruff_source_file/src/line_index.rs b/crates/source_file/src/line_index.rs similarity index 98% rename from crates/ruff_source_file/src/line_index.rs rename to crates/source_file/src/line_index.rs index d85568c6..2051137b 100644 --- a/crates/ruff_source_file/src/line_index.rs +++ b/crates/source_file/src/line_index.rs @@ -71,7 +71,7 @@ impl LineIndex { /// /// ``` /// # use biome_text_size::TextSize; - /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; + /// # use source_file::{LineIndex, OneIndexed, SourceLocation}; /// let source = "def a():\n pass"; /// let index = LineIndex::from_source_text(source); /// @@ -140,7 +140,7 @@ impl LineIndex { /// /// ``` /// # use biome_text_size::TextSize; - /// # use ruff_source_file::{LineIndex, OneIndexed, SourceLocation}; + /// # use source_file::{LineIndex, OneIndexed, SourceLocation}; /// let source = "def a():\n pass"; /// let index = LineIndex::from_source_text(source); /// @@ -227,7 +227,7 @@ impl LineIndex { /// ### ASCII /// /// ``` - /// use ruff_source_file::{LineIndex, OneIndexed}; + /// use source_file::{LineIndex, OneIndexed}; /// use biome_text_size::TextSize; /// let source = r#"a = 4 /// c = "some string" @@ -251,7 +251,7 @@ impl LineIndex { /// ### UTF8 /// /// ``` - /// use ruff_source_file::{LineIndex, OneIndexed}; + /// use source_file::{LineIndex, OneIndexed}; /// use biome_text_size::TextSize; /// let source = r#"a = 4 /// c = "❤️" diff --git a/crates/ruff_source_file/src/newlines.rs b/crates/source_file/src/newlines.rs similarity index 100% rename from crates/ruff_source_file/src/newlines.rs rename to crates/source_file/src/newlines.rs diff --git a/crates/ruff_source_file/src/one_indexed.rs b/crates/source_file/src/one_indexed.rs similarity index 100% rename from crates/ruff_source_file/src/one_indexed.rs rename to crates/source_file/src/one_indexed.rs diff --git a/crates/ruff_source_file/src/source_location.rs b/crates/source_file/src/source_location.rs similarity index 100% rename from crates/ruff_source_file/src/source_location.rs rename to crates/source_file/src/source_location.rs diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 54a06b45..37854b82 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -18,8 +18,8 @@ fs = { workspace = true } ignore = { workspace = true } rustc-hash = { workspace = true } thiserror = { workspace = true } -ruff_source_file = { workspace = true } serde = { workspace = true, features = ["derive"] } +source_file = { workspace = true } toml = { workspace = true } [dev-dependencies] diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs index 63e5d70f..dd474ae1 100644 --- a/crates/workspace/src/settings.rs +++ b/crates/workspace/src/settings.rs @@ -41,10 +41,10 @@ impl FormatSettings { LineEnding::Native => biome_formatter::LineEnding::Crlf, #[cfg(not(target_os = "windows"))] LineEnding::Native => biome_formatter::LineEnding::Lf, - LineEnding::Auto => match ruff_source_file::find_newline(source) { - Some((_, ruff_source_file::LineEnding::Lf)) => biome_formatter::LineEnding::Lf, - Some((_, ruff_source_file::LineEnding::Crlf)) => biome_formatter::LineEnding::Crlf, - Some((_, ruff_source_file::LineEnding::Cr)) => biome_formatter::LineEnding::Cr, + LineEnding::Auto => match source_file::find_newline(source) { + Some((_, source_file::LineEnding::Lf)) => biome_formatter::LineEnding::Lf, + Some((_, source_file::LineEnding::Crlf)) => biome_formatter::LineEnding::Crlf, + Some((_, source_file::LineEnding::Cr)) => biome_formatter::LineEnding::Cr, None => biome_formatter::LineEnding::Lf, }, }; From e9378067de4fe1a73c7ce095d14e4763a873afa3 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:43:43 -0500 Subject: [PATCH 17/44] Rename `ruff_server` to `server` --- Cargo.lock | 80 +++++++++---------- Cargo.toml | 2 +- crates/air/Cargo.toml | 2 +- crates/air/src/commands/language_server.rs | 2 +- ..._api__requests__format__tests__format.snap | 7 -- ...__tests__format_range_logical_lines-2.snap | 7 -- ...__tests__format_range_logical_lines-3.snap | 6 -- ...__tests__format_range_logical_lines-4.snap | 8 -- ...__tests__format_range_logical_lines-5.snap | 11 --- ...ge__tests__format_range_logical_lines.snap | 6 -- ...tests__format_range_mismatched_indent.snap | 6 -- ..._tests__format_range_multiple_lines-2.snap | 7 -- ...e__tests__format_range_multiple_lines.snap | 7 -- ...mat_range__tests__format_range_none-2.snap | 5 -- ...mat_range__tests__format_range_none-3.snap | 5 -- ...ormat_range__tests__format_range_none.snap | 5 -- ...tests__format_range_unmatched_lists-2.snap | 10 --- ...tests__format_range_unmatched_lists-3.snap | 10 --- ...tests__format_range_unmatched_lists-4.snap | 10 --- ...tests__format_range_unmatched_lists-5.snap | 6 -- ...tests__format_range_unmatched_lists-6.snap | 6 -- ...__tests__format_range_unmatched_lists.snap | 10 --- crates/{ruff_server => server}/Cargo.toml | 2 +- crates/{ruff_server => server}/build.rs | 0 crates/{ruff_server => server}/src/crates.rs | 0 crates/{ruff_server => server}/src/edit.rs | 0 .../src/edit/text_diff.rs | 0 .../src/edit/text_document.rs | 0 .../src/edit/text_edit.rs | 0 crates/{ruff_server => server}/src/error.rs | 0 crates/{ruff_server => server}/src/lib.rs | 0 crates/{ruff_server => server}/src/logging.rs | 0 crates/{ruff_server => server}/src/message.rs | 0 crates/{ruff_server => server}/src/proto.rs | 0 .../src/proto/text_edit.rs | 0 .../src/proto/text_range.rs | 0 .../src/proto/text_size.rs | 0 crates/{ruff_server => server}/src/server.rs | 0 .../{ruff_server => server}/src/server/api.rs | 0 .../src/server/api/notifications.rs | 0 .../src/server/api/notifications/cancel.rs | 0 .../server/api/notifications/did_change.rs | 0 .../notifications/did_change_configuration.rs | 0 .../notifications/did_change_watched_files.rs | 0 .../api/notifications/did_change_workspace.rs | 0 .../src/server/api/notifications/did_close.rs | 0 .../src/server/api/notifications/did_open.rs | 0 .../src/server/api/notifications/set_trace.rs | 0 .../src/server/api/requests.rs | 0 .../src/server/api/requests/format.rs | 0 .../src/server/api/requests/format_range.rs | 0 ..._api__requests__format__tests__format.snap | 7 ++ ...__tests__format_range_logical_lines-2.snap | 7 ++ ...__tests__format_range_logical_lines-3.snap | 6 ++ ...__tests__format_range_logical_lines-4.snap | 8 ++ ...__tests__format_range_logical_lines-5.snap | 11 +++ ...ge__tests__format_range_logical_lines.snap | 6 ++ ...tests__format_range_mismatched_indent.snap | 6 ++ ..._tests__format_range_multiple_lines-2.snap | 7 ++ ...e__tests__format_range_multiple_lines.snap | 7 ++ ...mat_range__tests__format_range_none-2.snap | 5 ++ ...mat_range__tests__format_range_none-3.snap | 5 ++ ...ormat_range__tests__format_range_none.snap | 5 ++ ...tests__format_range_unmatched_lists-2.snap | 10 +++ ...tests__format_range_unmatched_lists-3.snap | 10 +++ ...tests__format_range_unmatched_lists-4.snap | 10 +++ ...tests__format_range_unmatched_lists-5.snap | 6 ++ ...tests__format_range_unmatched_lists-6.snap | 6 ++ ...__tests__format_range_unmatched_lists.snap | 10 +++ .../src/server/api/requests/view_file.rs | 0 .../src/server/api/traits.rs | 0 .../src/server/client.rs | 0 .../src/server/connection.rs | 0 .../src/server/schedule.rs | 0 .../src/server/schedule/task.rs | 0 .../src/server/schedule/thread.rs | 0 .../src/server/schedule/thread/pool.rs | 0 .../src/server/schedule/thread/priority.rs | 0 crates/{ruff_server => server}/src/session.rs | 0 .../src/session/capabilities.rs | 0 .../src/session/index.rs | 0 .../src/session/workspaces.rs | 0 crates/{ruff_server => server}/src/test.rs | 0 .../src/test/client.rs | 0 .../src/test/client_ext.rs | 0 .../{ruff_server => server}/src/test/utils.rs | 0 86 files changed, 176 insertions(+), 176 deletions(-) delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format__tests__format.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-2.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-3.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap delete mode 100644 crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap rename crates/{ruff_server => server}/Cargo.toml (98%) rename crates/{ruff_server => server}/build.rs (100%) rename crates/{ruff_server => server}/src/crates.rs (100%) rename crates/{ruff_server => server}/src/edit.rs (100%) rename crates/{ruff_server => server}/src/edit/text_diff.rs (100%) rename crates/{ruff_server => server}/src/edit/text_document.rs (100%) rename crates/{ruff_server => server}/src/edit/text_edit.rs (100%) rename crates/{ruff_server => server}/src/error.rs (100%) rename crates/{ruff_server => server}/src/lib.rs (100%) rename crates/{ruff_server => server}/src/logging.rs (100%) rename crates/{ruff_server => server}/src/message.rs (100%) rename crates/{ruff_server => server}/src/proto.rs (100%) rename crates/{ruff_server => server}/src/proto/text_edit.rs (100%) rename crates/{ruff_server => server}/src/proto/text_range.rs (100%) rename crates/{ruff_server => server}/src/proto/text_size.rs (100%) rename crates/{ruff_server => server}/src/server.rs (100%) rename crates/{ruff_server => server}/src/server/api.rs (100%) rename crates/{ruff_server => server}/src/server/api/notifications.rs (100%) rename crates/{ruff_server => server}/src/server/api/notifications/cancel.rs (100%) rename crates/{ruff_server => server}/src/server/api/notifications/did_change.rs (100%) rename crates/{ruff_server => server}/src/server/api/notifications/did_change_configuration.rs (100%) rename crates/{ruff_server => server}/src/server/api/notifications/did_change_watched_files.rs (100%) rename crates/{ruff_server => server}/src/server/api/notifications/did_change_workspace.rs (100%) rename crates/{ruff_server => server}/src/server/api/notifications/did_close.rs (100%) rename crates/{ruff_server => server}/src/server/api/notifications/did_open.rs (100%) rename crates/{ruff_server => server}/src/server/api/notifications/set_trace.rs (100%) rename crates/{ruff_server => server}/src/server/api/requests.rs (100%) rename crates/{ruff_server => server}/src/server/api/requests/format.rs (100%) rename crates/{ruff_server => server}/src/server/api/requests/format_range.rs (100%) create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format__tests__format.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_multiple_lines.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none-2.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none-3.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap create mode 100644 crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap rename crates/{ruff_server => server}/src/server/api/requests/view_file.rs (100%) rename crates/{ruff_server => server}/src/server/api/traits.rs (100%) rename crates/{ruff_server => server}/src/server/client.rs (100%) rename crates/{ruff_server => server}/src/server/connection.rs (100%) rename crates/{ruff_server => server}/src/server/schedule.rs (100%) rename crates/{ruff_server => server}/src/server/schedule/task.rs (100%) rename crates/{ruff_server => server}/src/server/schedule/thread.rs (100%) rename crates/{ruff_server => server}/src/server/schedule/thread/pool.rs (100%) rename crates/{ruff_server => server}/src/server/schedule/thread/priority.rs (100%) rename crates/{ruff_server => server}/src/session.rs (100%) rename crates/{ruff_server => server}/src/session/capabilities.rs (100%) rename crates/{ruff_server => server}/src/session/index.rs (100%) rename crates/{ruff_server => server}/src/session/workspaces.rs (100%) rename crates/{ruff_server => server}/src/test.rs (100%) rename crates/{ruff_server => server}/src/test/client.rs (100%) rename crates/{ruff_server => server}/src/test/client_ext.rs (100%) rename crates/{ruff_server => server}/src/test/utils.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index f7f20d15..d5fc9d06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,7 +42,7 @@ dependencies = [ "ignore", "itertools", "lsp-server", - "ruff_server", + "server", "source_file", "tempfile", "thiserror 2.0.5", @@ -1414,45 +1414,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ruff_server" -version = "0.1.1" -dependencies = [ - "air_r_factory", - "air_r_formatter", - "air_r_parser", - "air_r_syntax", - "anyhow", - "assert_matches", - "biome_formatter", - "biome_rowan", - "biome_text_size", - "cargo_metadata", - "crossbeam", - "dissimilar", - "ignore", - "insta", - "itertools", - "jod-thread", - "libc", - "lsp-server", - "lsp-types", - "regex", - "rustc-hash", - "serde", - "serde_json", - "server_test", - "source_file", - "thiserror 2.0.5", - "tracing", - "tracing-subscriber", - "tree-sitter", - "tree-sitter-r", - "url", - "uuid", - "workspace", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1641,6 +1602,45 @@ dependencies = [ "serde", ] +[[package]] +name = "server" +version = "0.1.1" +dependencies = [ + "air_r_factory", + "air_r_formatter", + "air_r_parser", + "air_r_syntax", + "anyhow", + "assert_matches", + "biome_formatter", + "biome_rowan", + "biome_text_size", + "cargo_metadata", + "crossbeam", + "dissimilar", + "ignore", + "insta", + "itertools", + "jod-thread", + "libc", + "lsp-server", + "lsp-types", + "regex", + "rustc-hash", + "serde", + "serde_json", + "server_test", + "source_file", + "thiserror 2.0.5", + "tracing", + "tracing-subscriber", + "tree-sitter", + "tree-sitter-r", + "url", + "uuid", + "workspace", +] + [[package]] name = "server_test" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index e2a61ad7..d3b374a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ air_r_syntax = { path = "./crates/air_r_syntax" } biome_ungrammar = { path = "./crates/biome_ungrammar" } fs = { path = "./crates/fs" } tests_macros = { path = "./crates/tests_macros" } -ruff_server = { path = "./crates/ruff_server" } +server = { path = "./crates/server" } server_test = { path = "./crates/server_test" } source_file = { path = "./crates/source_file" } workspace = { path = "./crates/workspace" } diff --git a/crates/air/Cargo.toml b/crates/air/Cargo.toml index 102edb6e..9c2bb547 100644 --- a/crates/air/Cargo.toml +++ b/crates/air/Cargo.toml @@ -24,7 +24,7 @@ fs = { workspace = true } ignore = { workspace = true } itertools = { workspace = true } lsp-server = { workspace = true } -ruff_server = { workspace = true } +server = { workspace = true } source_file = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/air/src/commands/language_server.rs b/crates/air/src/commands/language_server.rs index c921c4aa..fd034f61 100644 --- a/crates/air/src/commands/language_server.rs +++ b/crates/air/src/commands/language_server.rs @@ -1,6 +1,6 @@ use std::num::NonZeroUsize; -use ruff_server::Server; +use server::Server; use crate::args::LanguageServerCommand; use crate::ExitStatus; diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format__tests__format.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format__tests__format.snap deleted file mode 100644 index f73985a1..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format__tests__format.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format.rs -expression: formatted ---- -1 -2 + 2 -3 + 3 + 3 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap deleted file mode 100644 index b59f3d58..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output ---- -1+1 -# -2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap deleted file mode 100644 index b32ccf9c..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output ---- -1+1 -{2 + 2} diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap deleted file mode 100644 index ee693476..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output ---- -1+1 -{ - 2 + 2 -} diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap deleted file mode 100644 index e2057f2a..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output ---- -1+1 -{ - 2+2 - { - 3 + 3 - } -} diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines.snap deleted file mode 100644 index c5a4d369..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_logical_lines.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output ---- -1+1 -2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap deleted file mode 100644 index aacf6963..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output ---- -1 - 2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap deleted file mode 100644 index 292171c8..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output2 ---- -1 + 1 -# -2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines.snap deleted file mode 100644 index 6f04197c..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_multiple_lines.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output1 ---- -1+1 -# -2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-2.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-2.snap deleted file mode 100644 index ae085c9a..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-2.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output ---- - diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-3.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-3.snap deleted file mode 100644 index f9b29214..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none-3.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output ---- -1 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none.snap deleted file mode 100644 index ae085c9a..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_none.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output ---- - diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap deleted file mode 100644 index 1f7eb677..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output2 ---- -0+0 -1 + 1 -{ - 2 + 2 -} -3+3 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap deleted file mode 100644 index 846ca65c..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output3 ---- -0+0 -1 + 1 -{ - 2 + 2 -} -3 + 3 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap deleted file mode 100644 index 7a3a5c46..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output4 ---- -0+0 -1+1 -{ - 2 + 2 -} -3 + 3 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap deleted file mode 100644 index c896564b..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output5 ---- -1 + 1 -2+2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap deleted file mode 100644 index cfca8f65..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output6 ---- -1+1 -2 + 2 diff --git a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap b/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap deleted file mode 100644 index 43ee58db..00000000 --- a/crates/ruff_server/src/server/api/requests/snapshots/ruff_server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/ruff_server/src/server/api/requests/format_range.rs -expression: output1 ---- -0+0 -1 + 1 -{ - 2 + 2 -} -3+3 diff --git a/crates/ruff_server/Cargo.toml b/crates/server/Cargo.toml similarity index 98% rename from crates/ruff_server/Cargo.toml rename to crates/server/Cargo.toml index fa1ac011..878ec290 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ruff_server" +name = "server" version = "0.1.1" publish = false authors = { workspace = true } diff --git a/crates/ruff_server/build.rs b/crates/server/build.rs similarity index 100% rename from crates/ruff_server/build.rs rename to crates/server/build.rs diff --git a/crates/ruff_server/src/crates.rs b/crates/server/src/crates.rs similarity index 100% rename from crates/ruff_server/src/crates.rs rename to crates/server/src/crates.rs diff --git a/crates/ruff_server/src/edit.rs b/crates/server/src/edit.rs similarity index 100% rename from crates/ruff_server/src/edit.rs rename to crates/server/src/edit.rs diff --git a/crates/ruff_server/src/edit/text_diff.rs b/crates/server/src/edit/text_diff.rs similarity index 100% rename from crates/ruff_server/src/edit/text_diff.rs rename to crates/server/src/edit/text_diff.rs diff --git a/crates/ruff_server/src/edit/text_document.rs b/crates/server/src/edit/text_document.rs similarity index 100% rename from crates/ruff_server/src/edit/text_document.rs rename to crates/server/src/edit/text_document.rs diff --git a/crates/ruff_server/src/edit/text_edit.rs b/crates/server/src/edit/text_edit.rs similarity index 100% rename from crates/ruff_server/src/edit/text_edit.rs rename to crates/server/src/edit/text_edit.rs diff --git a/crates/ruff_server/src/error.rs b/crates/server/src/error.rs similarity index 100% rename from crates/ruff_server/src/error.rs rename to crates/server/src/error.rs diff --git a/crates/ruff_server/src/lib.rs b/crates/server/src/lib.rs similarity index 100% rename from crates/ruff_server/src/lib.rs rename to crates/server/src/lib.rs diff --git a/crates/ruff_server/src/logging.rs b/crates/server/src/logging.rs similarity index 100% rename from crates/ruff_server/src/logging.rs rename to crates/server/src/logging.rs diff --git a/crates/ruff_server/src/message.rs b/crates/server/src/message.rs similarity index 100% rename from crates/ruff_server/src/message.rs rename to crates/server/src/message.rs diff --git a/crates/ruff_server/src/proto.rs b/crates/server/src/proto.rs similarity index 100% rename from crates/ruff_server/src/proto.rs rename to crates/server/src/proto.rs diff --git a/crates/ruff_server/src/proto/text_edit.rs b/crates/server/src/proto/text_edit.rs similarity index 100% rename from crates/ruff_server/src/proto/text_edit.rs rename to crates/server/src/proto/text_edit.rs diff --git a/crates/ruff_server/src/proto/text_range.rs b/crates/server/src/proto/text_range.rs similarity index 100% rename from crates/ruff_server/src/proto/text_range.rs rename to crates/server/src/proto/text_range.rs diff --git a/crates/ruff_server/src/proto/text_size.rs b/crates/server/src/proto/text_size.rs similarity index 100% rename from crates/ruff_server/src/proto/text_size.rs rename to crates/server/src/proto/text_size.rs diff --git a/crates/ruff_server/src/server.rs b/crates/server/src/server.rs similarity index 100% rename from crates/ruff_server/src/server.rs rename to crates/server/src/server.rs diff --git a/crates/ruff_server/src/server/api.rs b/crates/server/src/server/api.rs similarity index 100% rename from crates/ruff_server/src/server/api.rs rename to crates/server/src/server/api.rs diff --git a/crates/ruff_server/src/server/api/notifications.rs b/crates/server/src/server/api/notifications.rs similarity index 100% rename from crates/ruff_server/src/server/api/notifications.rs rename to crates/server/src/server/api/notifications.rs diff --git a/crates/ruff_server/src/server/api/notifications/cancel.rs b/crates/server/src/server/api/notifications/cancel.rs similarity index 100% rename from crates/ruff_server/src/server/api/notifications/cancel.rs rename to crates/server/src/server/api/notifications/cancel.rs diff --git a/crates/ruff_server/src/server/api/notifications/did_change.rs b/crates/server/src/server/api/notifications/did_change.rs similarity index 100% rename from crates/ruff_server/src/server/api/notifications/did_change.rs rename to crates/server/src/server/api/notifications/did_change.rs diff --git a/crates/ruff_server/src/server/api/notifications/did_change_configuration.rs b/crates/server/src/server/api/notifications/did_change_configuration.rs similarity index 100% rename from crates/ruff_server/src/server/api/notifications/did_change_configuration.rs rename to crates/server/src/server/api/notifications/did_change_configuration.rs diff --git a/crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs b/crates/server/src/server/api/notifications/did_change_watched_files.rs similarity index 100% rename from crates/ruff_server/src/server/api/notifications/did_change_watched_files.rs rename to crates/server/src/server/api/notifications/did_change_watched_files.rs diff --git a/crates/ruff_server/src/server/api/notifications/did_change_workspace.rs b/crates/server/src/server/api/notifications/did_change_workspace.rs similarity index 100% rename from crates/ruff_server/src/server/api/notifications/did_change_workspace.rs rename to crates/server/src/server/api/notifications/did_change_workspace.rs diff --git a/crates/ruff_server/src/server/api/notifications/did_close.rs b/crates/server/src/server/api/notifications/did_close.rs similarity index 100% rename from crates/ruff_server/src/server/api/notifications/did_close.rs rename to crates/server/src/server/api/notifications/did_close.rs diff --git a/crates/ruff_server/src/server/api/notifications/did_open.rs b/crates/server/src/server/api/notifications/did_open.rs similarity index 100% rename from crates/ruff_server/src/server/api/notifications/did_open.rs rename to crates/server/src/server/api/notifications/did_open.rs diff --git a/crates/ruff_server/src/server/api/notifications/set_trace.rs b/crates/server/src/server/api/notifications/set_trace.rs similarity index 100% rename from crates/ruff_server/src/server/api/notifications/set_trace.rs rename to crates/server/src/server/api/notifications/set_trace.rs diff --git a/crates/ruff_server/src/server/api/requests.rs b/crates/server/src/server/api/requests.rs similarity index 100% rename from crates/ruff_server/src/server/api/requests.rs rename to crates/server/src/server/api/requests.rs diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/server/src/server/api/requests/format.rs similarity index 100% rename from crates/ruff_server/src/server/api/requests/format.rs rename to crates/server/src/server/api/requests/format.rs diff --git a/crates/ruff_server/src/server/api/requests/format_range.rs b/crates/server/src/server/api/requests/format_range.rs similarity index 100% rename from crates/ruff_server/src/server/api/requests/format_range.rs rename to crates/server/src/server/api/requests/format_range.rs diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format__tests__format.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format__tests__format.snap new file mode 100644 index 00000000..aee3cb44 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format__tests__format.snap @@ -0,0 +1,7 @@ +--- +source: crates/server/src/server/api/requests/format.rs +expression: formatted +--- +1 +2 + 2 +3 + 3 + 3 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap new file mode 100644 index 00000000..5862946a --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-2.snap @@ -0,0 +1,7 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +# +2 + 2 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap new file mode 100644 index 00000000..faa5f5c4 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-3.snap @@ -0,0 +1,6 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +{2 + 2} diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap new file mode 100644 index 00000000..b27c75f2 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-4.snap @@ -0,0 +1,8 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +{ + 2 + 2 +} diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap new file mode 100644 index 00000000..6831d0d5 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines-5.snap @@ -0,0 +1,11 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +{ + 2+2 + { + 3 + 3 + } +} diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines.snap new file mode 100644 index 00000000..865744cb --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_logical_lines.snap @@ -0,0 +1,6 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output +--- +1+1 +2 + 2 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap new file mode 100644 index 00000000..9b7b4ea3 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_mismatched_indent.snap @@ -0,0 +1,6 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output +--- +1 + 2 + 2 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap new file mode 100644 index 00000000..3624c83e --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_multiple_lines-2.snap @@ -0,0 +1,7 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output2 +--- +1 + 1 +# +2 + 2 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_multiple_lines.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_multiple_lines.snap new file mode 100644 index 00000000..e807b492 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_multiple_lines.snap @@ -0,0 +1,7 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output1 +--- +1+1 +# +2 + 2 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none-2.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none-2.snap new file mode 100644 index 00000000..41717d50 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none-2.snap @@ -0,0 +1,5 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output +--- + diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none-3.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none-3.snap new file mode 100644 index 00000000..2fe0f1bc --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none-3.snap @@ -0,0 +1,5 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output +--- +1 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none.snap new file mode 100644 index 00000000..41717d50 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_none.snap @@ -0,0 +1,5 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output +--- + diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap new file mode 100644 index 00000000..0c907f96 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-2.snap @@ -0,0 +1,10 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output2 +--- +0+0 +1 + 1 +{ + 2 + 2 +} +3+3 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap new file mode 100644 index 00000000..1a161f44 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-3.snap @@ -0,0 +1,10 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output3 +--- +0+0 +1 + 1 +{ + 2 + 2 +} +3 + 3 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap new file mode 100644 index 00000000..ab9954f3 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-4.snap @@ -0,0 +1,10 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output4 +--- +0+0 +1+1 +{ + 2 + 2 +} +3 + 3 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap new file mode 100644 index 00000000..3b917aad --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-5.snap @@ -0,0 +1,6 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output5 +--- +1 + 1 +2+2 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap new file mode 100644 index 00000000..88dd2ac0 --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists-6.snap @@ -0,0 +1,6 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output6 +--- +1+1 +2 + 2 diff --git a/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap new file mode 100644 index 00000000..0257bd0b --- /dev/null +++ b/crates/server/src/server/api/requests/snapshots/server__server__api__requests__format_range__tests__format_range_unmatched_lists.snap @@ -0,0 +1,10 @@ +--- +source: crates/server/src/server/api/requests/format_range.rs +expression: output1 +--- +0+0 +1 + 1 +{ + 2 + 2 +} +3+3 diff --git a/crates/ruff_server/src/server/api/requests/view_file.rs b/crates/server/src/server/api/requests/view_file.rs similarity index 100% rename from crates/ruff_server/src/server/api/requests/view_file.rs rename to crates/server/src/server/api/requests/view_file.rs diff --git a/crates/ruff_server/src/server/api/traits.rs b/crates/server/src/server/api/traits.rs similarity index 100% rename from crates/ruff_server/src/server/api/traits.rs rename to crates/server/src/server/api/traits.rs diff --git a/crates/ruff_server/src/server/client.rs b/crates/server/src/server/client.rs similarity index 100% rename from crates/ruff_server/src/server/client.rs rename to crates/server/src/server/client.rs diff --git a/crates/ruff_server/src/server/connection.rs b/crates/server/src/server/connection.rs similarity index 100% rename from crates/ruff_server/src/server/connection.rs rename to crates/server/src/server/connection.rs diff --git a/crates/ruff_server/src/server/schedule.rs b/crates/server/src/server/schedule.rs similarity index 100% rename from crates/ruff_server/src/server/schedule.rs rename to crates/server/src/server/schedule.rs diff --git a/crates/ruff_server/src/server/schedule/task.rs b/crates/server/src/server/schedule/task.rs similarity index 100% rename from crates/ruff_server/src/server/schedule/task.rs rename to crates/server/src/server/schedule/task.rs diff --git a/crates/ruff_server/src/server/schedule/thread.rs b/crates/server/src/server/schedule/thread.rs similarity index 100% rename from crates/ruff_server/src/server/schedule/thread.rs rename to crates/server/src/server/schedule/thread.rs diff --git a/crates/ruff_server/src/server/schedule/thread/pool.rs b/crates/server/src/server/schedule/thread/pool.rs similarity index 100% rename from crates/ruff_server/src/server/schedule/thread/pool.rs rename to crates/server/src/server/schedule/thread/pool.rs diff --git a/crates/ruff_server/src/server/schedule/thread/priority.rs b/crates/server/src/server/schedule/thread/priority.rs similarity index 100% rename from crates/ruff_server/src/server/schedule/thread/priority.rs rename to crates/server/src/server/schedule/thread/priority.rs diff --git a/crates/ruff_server/src/session.rs b/crates/server/src/session.rs similarity index 100% rename from crates/ruff_server/src/session.rs rename to crates/server/src/session.rs diff --git a/crates/ruff_server/src/session/capabilities.rs b/crates/server/src/session/capabilities.rs similarity index 100% rename from crates/ruff_server/src/session/capabilities.rs rename to crates/server/src/session/capabilities.rs diff --git a/crates/ruff_server/src/session/index.rs b/crates/server/src/session/index.rs similarity index 100% rename from crates/ruff_server/src/session/index.rs rename to crates/server/src/session/index.rs diff --git a/crates/ruff_server/src/session/workspaces.rs b/crates/server/src/session/workspaces.rs similarity index 100% rename from crates/ruff_server/src/session/workspaces.rs rename to crates/server/src/session/workspaces.rs diff --git a/crates/ruff_server/src/test.rs b/crates/server/src/test.rs similarity index 100% rename from crates/ruff_server/src/test.rs rename to crates/server/src/test.rs diff --git a/crates/ruff_server/src/test/client.rs b/crates/server/src/test/client.rs similarity index 100% rename from crates/ruff_server/src/test/client.rs rename to crates/server/src/test/client.rs diff --git a/crates/ruff_server/src/test/client_ext.rs b/crates/server/src/test/client_ext.rs similarity index 100% rename from crates/ruff_server/src/test/client_ext.rs rename to crates/server/src/test/client_ext.rs diff --git a/crates/ruff_server/src/test/utils.rs b/crates/server/src/test/utils.rs similarity index 100% rename from crates/ruff_server/src/test/utils.rs rename to crates/server/src/test/utils.rs From 0c5b6125d4bb9bb8286abdb32820cce91ffd7e92 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:47:21 -0500 Subject: [PATCH 18/44] Remove ruff attribution in code blocks (will be added elsewhere) --- crates/server/src/edit.rs | 6 ------ crates/server/src/edit/text_document.rs | 6 ------ crates/server/src/logging.rs | 6 ------ crates/server/src/message.rs | 6 ------ crates/server/src/proto/text_range.rs | 6 ------ crates/server/src/server.rs | 6 ------ crates/server/src/server/api.rs | 6 ------ crates/server/src/server/api/notifications/cancel.rs | 6 ------ crates/server/src/server/api/notifications/did_change.rs | 6 ------ .../server/api/notifications/did_change_configuration.rs | 6 ------ .../server/api/notifications/did_change_watched_files.rs | 6 ------ .../src/server/api/notifications/did_change_workspace.rs | 6 ------ crates/server/src/server/api/notifications/did_close.rs | 6 ------ crates/server/src/server/api/notifications/did_open.rs | 6 ------ crates/server/src/server/api/requests/format.rs | 6 ------ crates/server/src/server/api/requests/format_range.rs | 6 ------ crates/server/src/server/api/traits.rs | 6 ------ crates/server/src/server/client.rs | 6 ------ crates/server/src/server/connection.rs | 6 ------ crates/server/src/server/schedule.rs | 6 ------ crates/server/src/server/schedule/task.rs | 6 ------ crates/server/src/session.rs | 6 ------ crates/server/src/session/index.rs | 6 ------ crates/server/src/session/workspaces.rs | 6 ------ 24 files changed, 144 deletions(-) diff --git a/crates/server/src/edit.rs b/crates/server/src/edit.rs index c9f5f2a5..dd916dfe 100644 --- a/crates/server/src/edit.rs +++ b/crates/server/src/edit.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - //! Types and utilities for working with text, modifying source files, and `Ruff <-> LSP` type conversion. mod text_diff; diff --git a/crates/server/src/edit/text_document.rs b/crates/server/src/edit/text_document.rs index 1fd8ecf5..cf04c10d 100644 --- a/crates/server/src/edit/text_document.rs +++ b/crates/server/src/edit/text_document.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use biome_rowan::TextRange; use lsp_types::TextDocumentContentChangeEvent; use source_file::LineEnding; diff --git a/crates/server/src/logging.rs b/crates/server/src/logging.rs index a9960676..0f476e20 100644 --- a/crates/server/src/logging.rs +++ b/crates/server/src/logging.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - //! The logging system for `air lsp`. //! //! ## Air crate logs diff --git a/crates/server/src/message.rs b/crates/server/src/message.rs index 49f7ac67..61ca7162 100644 --- a/crates/server/src/message.rs +++ b/crates/server/src/message.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use anyhow::Context; use lsp_types::notification::Notification; use std::sync::OnceLock; diff --git a/crates/server/src/proto/text_range.rs b/crates/server/src/proto/text_range.rs index 3693f71b..a0a1da70 100644 --- a/crates/server/src/proto/text_range.rs +++ b/crates/server/src/proto/text_range.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use crate::edit::PositionEncoding; use crate::proto::TextSizeExt; use biome_text_size::{TextRange, TextSize}; diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index 67d3ab8e..9fa5c90f 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - //! Scheduling, I/O, and API endpoints. use lsp_server as lsp; diff --git a/crates/server/src/server/api.rs b/crates/server/src/server/api.rs index 12a5a93f..43ad1c4b 100644 --- a/crates/server/src/server/api.rs +++ b/crates/server/src/server/api.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use crate::{server::schedule::Task, session::Session}; use lsp_server as server; diff --git a/crates/server/src/server/api/notifications/cancel.rs b/crates/server/src/server/api/notifications/cancel.rs index d174ade2..d9d011c3 100644 --- a/crates/server/src/server/api/notifications/cancel.rs +++ b/crates/server/src/server/api/notifications/cancel.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use crate::server::client::{Notifier, Requester}; use crate::server::Result; use crate::session::Session; diff --git a/crates/server/src/server/api/notifications/did_change.rs b/crates/server/src/server/api/notifications/did_change.rs index 329e830e..ba51c1a9 100644 --- a/crates/server/src/server/api/notifications/did_change.rs +++ b/crates/server/src/server/api/notifications/did_change.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use crate::server::api::LSPResult; use crate::server::client::{Notifier, Requester}; use crate::server::Result; diff --git a/crates/server/src/server/api/notifications/did_change_configuration.rs b/crates/server/src/server/api/notifications/did_change_configuration.rs index b57af172..9a2e5af9 100644 --- a/crates/server/src/server/api/notifications/did_change_configuration.rs +++ b/crates/server/src/server/api/notifications/did_change_configuration.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use crate::server::client::{Notifier, Requester}; use crate::server::Result; use crate::session::Session; diff --git a/crates/server/src/server/api/notifications/did_change_watched_files.rs b/crates/server/src/server/api/notifications/did_change_watched_files.rs index 85cf6ba4..e7fc83a6 100644 --- a/crates/server/src/server/api/notifications/did_change_watched_files.rs +++ b/crates/server/src/server/api/notifications/did_change_watched_files.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use crate::server::client::{Notifier, Requester}; use crate::server::Result; use crate::session::Session; diff --git a/crates/server/src/server/api/notifications/did_change_workspace.rs b/crates/server/src/server/api/notifications/did_change_workspace.rs index ba3506d3..29bd5217 100644 --- a/crates/server/src/server/api/notifications/did_change_workspace.rs +++ b/crates/server/src/server/api/notifications/did_change_workspace.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use crate::error::ErrorVec; use crate::server::api::LSPResult; use crate::server::client::{Notifier, Requester}; diff --git a/crates/server/src/server/api/notifications/did_close.rs b/crates/server/src/server/api/notifications/did_close.rs index 1dc148c4..309b6f0e 100644 --- a/crates/server/src/server/api/notifications/did_close.rs +++ b/crates/server/src/server/api/notifications/did_close.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use crate::server::api::LSPResult; use crate::server::client::{Notifier, Requester}; use crate::server::Result; diff --git a/crates/server/src/server/api/notifications/did_open.rs b/crates/server/src/server/api/notifications/did_open.rs index fd8863b8..daeb413d 100644 --- a/crates/server/src/server/api/notifications/did_open.rs +++ b/crates/server/src/server/api/notifications/did_open.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use crate::edit::TextDocument; use crate::server::client::{Notifier, Requester}; use crate::server::Result; diff --git a/crates/server/src/server/api/requests/format.rs b/crates/server/src/server/api/requests/format.rs index 38edcf40..d5547ef5 100644 --- a/crates/server/src/server/api/requests/format.rs +++ b/crates/server/src/server/api/requests/format.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use air_r_parser::RParserOptions; use biome_formatter::LineEnding; use lsp_types::{self as types, request as req}; diff --git a/crates/server/src/server/api/requests/format_range.rs b/crates/server/src/server/api/requests/format_range.rs index f347fd29..94792b84 100644 --- a/crates/server/src/server/api/requests/format_range.rs +++ b/crates/server/src/server/api/requests/format_range.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use air_r_parser::RParserOptions; use air_r_syntax::RExpressionList; use air_r_syntax::RSyntaxKind; diff --git a/crates/server/src/server/api/traits.rs b/crates/server/src/server/api/traits.rs index 1d4bd543..f6a53974 100644 --- a/crates/server/src/server/api/traits.rs +++ b/crates/server/src/server/api/traits.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - //! A stateful LSP implementation that calls into the Ruff API. use crate::server::client::{Notifier, Requester}; diff --git a/crates/server/src/server/client.rs b/crates/server/src/server/client.rs index 6cb673f7..c5a502e2 100644 --- a/crates/server/src/server/client.rs +++ b/crates/server/src/server/client.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use std::any::TypeId; use lsp_server::{Notification, RequestId}; diff --git a/crates/server/src/server/connection.rs b/crates/server/src/server/connection.rs index 29fd737a..09388905 100644 --- a/crates/server/src/server/connection.rs +++ b/crates/server/src/server/connection.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use lsp_server as lsp; use lsp_types::{notification::Notification, request::Request}; use std::sync::{Arc, Weak}; diff --git a/crates/server/src/server/schedule.rs b/crates/server/src/server/schedule.rs index e79965c9..f0357068 100644 --- a/crates/server/src/server/schedule.rs +++ b/crates/server/src/server/schedule.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use std::num::NonZeroUsize; use crate::session::Session; diff --git a/crates/server/src/server/schedule/task.rs b/crates/server/src/server/schedule/task.rs index f9e1905a..beb8f4e1 100644 --- a/crates/server/src/server/schedule/task.rs +++ b/crates/server/src/server/schedule/task.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use lsp_server::RequestId; use serde::Serialize; diff --git a/crates/server/src/session.rs b/crates/server/src/session.rs index 9b9cc837..d1b5f9f5 100644 --- a/crates/server/src/session.rs +++ b/crates/server/src/session.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - //! Data model, state management, and configuration resolution. use std::sync::Arc; diff --git a/crates/server/src/session/index.rs b/crates/server/src/session/index.rs index 8366afaa..d083cb70 100644 --- a/crates/server/src/session/index.rs +++ b/crates/server/src/session/index.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use std::borrow::Cow; use std::path::PathBuf; use std::{path::Path, sync::Arc}; diff --git a/crates/server/src/session/workspaces.rs b/crates/server/src/session/workspaces.rs index f95736c2..94288e12 100644 --- a/crates/server/src/session/workspaces.rs +++ b/crates/server/src/session/workspaces.rs @@ -1,9 +1,3 @@ -// +------------------------------------------------------------+ -// | Code adopted from: | -// | Repository: https://github.com/astral-sh/ruff.git | -// | Commit: 5bc9d6d3aa694ab13f38dd5cf91b713fd3844380 | -// +------------------------------------------------------------+ - use std::path::Path; use std::path::PathBuf; From cde63e987fe97f7dd8c86379fb05b4d0cf53dcd1 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:53:01 -0500 Subject: [PATCH 19/44] Remove internal references to ruff --- crates/server/src/edit.rs | 4 ++-- crates/server/src/lib.rs | 2 +- crates/server/src/server.rs | 4 ++-- crates/server/src/server/api.rs | 12 +++++++----- crates/server/src/server/api/traits.rs | 2 +- crates/server/src/server/schedule.rs | 2 +- crates/server/src/server/schedule/thread/pool.rs | 2 +- crates/workspace/src/resolve.rs | 6 ------ 8 files changed, 15 insertions(+), 19 deletions(-) diff --git a/crates/server/src/edit.rs b/crates/server/src/edit.rs index dd916dfe..cce4e902 100644 --- a/crates/server/src/edit.rs +++ b/crates/server/src/edit.rs @@ -1,4 +1,4 @@ -//! Types and utilities for working with text, modifying source files, and `Ruff <-> LSP` type conversion. +//! Types and utilities for working with text and modifying source files mod text_diff; mod text_document; @@ -20,7 +20,7 @@ pub enum PositionEncoding { /// Second choice because UTF32 uses a fixed 4 byte encoding for each character (makes conversion relatively easy) UTF32, - /// Ruff's preferred encoding + /// Air's preferred encoding UTF8, } diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 56867ac5..ef61cf71 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -1,4 +1,4 @@ -//! ## The Ruff Language Server +//! ## The Air Language Server pub use server::Server; diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index 9fa5c90f..c3d2ae1f 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -134,14 +134,14 @@ impl Server { let backtrace = std::backtrace::Backtrace::force_capture(); tracing::error!("{panic_info}\n{backtrace}"); - // we also need to print to stderr directly for when using `$logTrace` because + // we also need to print to stderr directly for when using `window/logMessage` because // the message won't be sent to the client. // But don't use `eprintln` because `eprintln` itself may panic if the pipe is broken. let mut stderr = std::io::stderr().lock(); writeln!(stderr, "{panic_info}\n{backtrace}").ok(); try_show_message( - "The Ruff language server exited with a panic. See the logs for more details." + "The Air language server exited with a panic. See the logs for more details." .to_string(), lsp_types::MessageType::ERROR, ) diff --git a/crates/server/src/server/api.rs b/crates/server/src/server/api.rs index 43ad1c4b..32d3ef68 100644 --- a/crates/server/src/server/api.rs +++ b/crates/server/src/server/api.rs @@ -33,7 +33,7 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> { .unwrap_or_else(|err| { tracing::error!("Encountered error when routing request with ID {id}: {err}"); show_err_msg!( - "Ruff failed to handle a request from the editor. Check the logs for more details." + "Air failed to handle a request from the editor. Check the logs for more details." ); let result: Result<()> = Err(err); Task::immediate(id, result) @@ -65,7 +65,9 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { } .unwrap_or_else(|err| { tracing::error!("Encountered error when routing notification: {err}"); - show_err_msg!("Ruff failed to handle a notification from the editor. Check the logs for more details."); + show_err_msg!( + "Air failed to handle a notification from the editor. Check the logs for more details." + ); Task::nothing() }) } @@ -105,7 +107,7 @@ fn local_notification_task<'a, N: traits::SyncNotificationHandler>( Ok(Task::local(move |session, notifier, requester, _| { if let Err(err) = N::run(session, notifier, requester, params) { tracing::error!("An error occurred while running {id}: {err}"); - show_err_msg!("Ruff encountered a problem. Check the logs for more details."); + show_err_msg!("Air encountered a problem. Check the logs for more details."); } })) } @@ -124,7 +126,7 @@ fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationH Box::new(move |notifier, _| { if let Err(err) = N::run_with_snapshot(snapshot, notifier, params) { tracing::error!("An error occurred while running {id}: {err}"); - show_err_msg!("Ruff encountered a problem. Check the logs for more details."); + show_err_msg!("Air encountered a problem. Check the logs for more details."); } }) })) @@ -169,7 +171,7 @@ fn respond( { if let Err(err) = &result { tracing::error!("An error occurred with result ID {id}: {err}"); - show_err_msg!("Ruff encountered a problem. Check the logs for more details."); + show_err_msg!("Air encountered a problem. Check the logs for more details."); } if let Err(err) = responder.respond(id, result) { tracing::error!("Failed to send response: {err}"); diff --git a/crates/server/src/server/api/traits.rs b/crates/server/src/server/api/traits.rs index f6a53974..2d6198c0 100644 --- a/crates/server/src/server/api/traits.rs +++ b/crates/server/src/server/api/traits.rs @@ -1,4 +1,4 @@ -//! A stateful LSP implementation that calls into the Ruff API. +//! A stateful LSP implementation that calls into the Air API. use crate::server::client::{Notifier, Requester}; use crate::session::{DocumentSnapshot, Session}; diff --git a/crates/server/src/server/schedule.rs b/crates/server/src/server/schedule.rs index f0357068..270986c8 100644 --- a/crates/server/src/server/schedule.rs +++ b/crates/server/src/server/schedule.rs @@ -23,7 +23,7 @@ pub(crate) fn event_loop_thread( ) -> crate::Result>> { // Override OS defaults to avoid stack overflows on platforms with low stack size defaults. const MAIN_THREAD_STACK_SIZE: usize = 2 * 1024 * 1024; - const MAIN_THREAD_NAME: &str = "ruff:main"; + const MAIN_THREAD_NAME: &str = "air:main"; Ok( thread::Builder::new(thread::ThreadPriority::LatencySensitive) .name(MAIN_THREAD_NAME.into()) diff --git a/crates/server/src/server/schedule/thread/pool.rs b/crates/server/src/server/schedule/thread/pool.rs index ea654a11..81443c5d 100644 --- a/crates/server/src/server/schedule/thread/pool.rs +++ b/crates/server/src/server/schedule/thread/pool.rs @@ -59,7 +59,7 @@ impl Pool { for i in 0..threads { let handle = Builder::new(INITIAL_PRIORITY) .stack_size(STACK_SIZE) - .name(format!("ruff:worker:{i}")) + .name(format!("air:worker:{i}")) .spawn({ let extant_tasks = Arc::clone(&extant_tasks); let job_receiver: Receiver = job_receiver.clone(); diff --git a/crates/workspace/src/resolve.rs b/crates/workspace/src/resolve.rs index 622e59b4..3e275549 100644 --- a/crates/workspace/src/resolve.rs +++ b/crates/workspace/src/resolve.rs @@ -1,9 +1,3 @@ -// --- source -// authors = ["Charlie Marsh"] -// license = "MIT" -// origin = "https://github.com/astral-sh/ruff/tree/main/crates/ruff_workspace" -// --- - use std::collections::btree_map::Keys; use std::collections::btree_map::Range; use std::collections::btree_map::RangeMut; From 455c767efaa4ed8a9d5e59bafcc12d081732bfb4 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:55:47 -0500 Subject: [PATCH 20/44] `cargo machete` clean up --- Cargo.lock | 7 ------- crates/air/Cargo.toml | 3 --- crates/air_formatter_test/Cargo.toml | 1 - crates/server/Cargo.toml | 3 --- 4 files changed, 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5fc9d06..374a2867 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,10 +33,7 @@ dependencies = [ "air_r_formatter", "air_r_parser", "anyhow", - "biome_console", - "biome_diagnostics", "biome_formatter", - "biome_parser", "clap", "fs", "ignore", @@ -61,7 +58,6 @@ dependencies = [ "biome_rowan", "insta", "serde", - "similar", "similar-asserts", "source_file", ] @@ -1618,20 +1614,17 @@ dependencies = [ "cargo_metadata", "crossbeam", "dissimilar", - "ignore", "insta", "itertools", "jod-thread", "libc", "lsp-server", "lsp-types", - "regex", "rustc-hash", "serde", "serde_json", "server_test", "source_file", - "thiserror 2.0.5", "tracing", "tracing-subscriber", "tree-sitter", diff --git a/crates/air/Cargo.toml b/crates/air/Cargo.toml index 9c2bb547..83039d8b 100644 --- a/crates/air/Cargo.toml +++ b/crates/air/Cargo.toml @@ -15,10 +15,7 @@ rust-version.workspace = true air_r_formatter = { workspace = true } air_r_parser = { workspace = true } anyhow = { workspace = true } -biome_console = { workspace = true } -biome_diagnostics = { workspace = true } biome_formatter = { workspace = true } -biome_parser = { workspace = true } clap = { workspace = true, features = ["wrap_help"] } fs = { workspace = true } ignore = { workspace = true } diff --git a/crates/air_formatter_test/Cargo.toml b/crates/air_formatter_test/Cargo.toml index 15150909..895b4cc6 100644 --- a/crates/air_formatter_test/Cargo.toml +++ b/crates/air_formatter_test/Cargo.toml @@ -22,7 +22,6 @@ biome_parser = { workspace = true } biome_rowan = { workspace = true } insta = { workspace = true, features = ["glob"] } serde = { workspace = true, features = ["derive"] } -similar = "2.6.0" similar-asserts = "1.6.0" source_file = { workspace = true } diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 878ec290..609cf26c 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -25,16 +25,13 @@ biome_rowan = { workspace = true } biome_text_size = { workspace = true } crossbeam = { workspace = true } dissimilar = { workspace = true } -ignore = { workspace = true } itertools = { workspace = true } jod-thread = { workspace = true } lsp-server = { workspace = true } lsp-types = { workspace = true } -regex = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["ansi", "local-time"] } tree-sitter = { workspace = true } From c9142a502cd8820f60fc1a09c7550c01e4675d74 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 10:59:27 -0500 Subject: [PATCH 21/44] Remove ruff specific TODOs --- crates/server/src/server/api.rs | 4 ++-- crates/server/src/server/api/notifications/cancel.rs | 2 +- .../src/server/api/notifications/did_change_configuration.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/server/src/server/api.rs b/crates/server/src/server/api.rs index 32d3ef68..b922cf46 100644 --- a/crates/server/src/server/api.rs +++ b/crates/server/src/server/api.rs @@ -89,7 +89,7 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>( ) -> super::Result> { let (id, params) = cast_request::(req)?; Ok(Task::background(schedule, move |session: &Session| { - // TODO(jane): we should log an error if we can't take a snapshot. + // TODO: We should log an error if we can't take a snapshot. let Some(snapshot) = session.take_snapshot(R::document_url(¶ms).into_owned()) else { return Box::new(|_, _| {}); }; @@ -119,7 +119,7 @@ fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationH ) -> super::Result> { let (id, params) = cast_notification::(req)?; Ok(Task::background(schedule, move |session: &Session| { - // TODO(jane): we should log an error if we can't take a snapshot. + // TODO: We should log an error if we can't take a snapshot. let Some(snapshot) = session.take_snapshot(N::document_url(¶ms).into_owned()) else { return Box::new(|_, _| {}); }; diff --git a/crates/server/src/server/api/notifications/cancel.rs b/crates/server/src/server/api/notifications/cancel.rs index d9d011c3..e0999c4a 100644 --- a/crates/server/src/server/api/notifications/cancel.rs +++ b/crates/server/src/server/api/notifications/cancel.rs @@ -17,7 +17,7 @@ impl super::SyncNotificationHandler for Cancel { _requester: &mut Requester, _params: types::CancelParams, ) -> Result<()> { - // TODO(jane): Handle this once we have task cancellation in the scheduler. + // TODO: Handle this once we have task cancellation in the scheduler. Ok(()) } } diff --git a/crates/server/src/server/api/notifications/did_change_configuration.rs b/crates/server/src/server/api/notifications/did_change_configuration.rs index 9a2e5af9..a67155b5 100644 --- a/crates/server/src/server/api/notifications/did_change_configuration.rs +++ b/crates/server/src/server/api/notifications/did_change_configuration.rs @@ -17,7 +17,7 @@ impl super::SyncNotificationHandler for DidChangeConfiguration { _requester: &mut Requester, _params: types::DidChangeConfigurationParams, ) -> Result<()> { - // TODO(jane): get this wired up after the pre-release + // TODO: get this wired up as a "signal" to pull new configuration Ok(()) } } From 1739f0c189f20d692c52d518bcce4df3fbb6aea9 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 11:02:15 -0500 Subject: [PATCH 22/44] Don't need resolved capabilities in snapshots right now --- crates/server/src/session.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/crates/server/src/session.rs b/crates/server/src/session.rs index d1b5f9f5..72e1a230 100644 --- a/crates/server/src/session.rs +++ b/crates/server/src/session.rs @@ -1,7 +1,5 @@ //! Data model, state management, and configuration resolution. -use std::sync::Arc; - use lsp_types::Url; use lsp_types::WorkspaceFolder; @@ -21,14 +19,12 @@ pub(crate) struct Session { /// The global position encoding, negotiated during LSP initialization. position_encoding: PositionEncoding, /// Tracks what LSP features the client supports and doesn't support. - resolved_client_capabilities: Arc, + resolved_client_capabilities: ResolvedClientCapabilities, } /// An immutable snapshot of `Session` that references /// a specific document. pub(crate) struct DocumentSnapshot { - #[allow(dead_code)] - resolved_client_capabilities: Arc, document_ref: index::DocumentQuery, position_encoding: PositionEncoding, } @@ -42,7 +38,7 @@ impl Session { Ok(Self { position_encoding, index: index::Index::new(workspace_folders)?, - resolved_client_capabilities: Arc::new(resolved_client_capabilities), + resolved_client_capabilities, }) } @@ -54,7 +50,6 @@ impl Session { pub(crate) fn take_snapshot(&self, url: Url) -> Option { let key = self.key_from_url(url); Some(DocumentSnapshot { - resolved_client_capabilities: self.resolved_client_capabilities.clone(), document_ref: self.index.make_document_ref(key)?, position_encoding: self.position_encoding, }) @@ -131,11 +126,6 @@ impl Session { } impl DocumentSnapshot { - #[allow(dead_code)] - pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities { - &self.resolved_client_capabilities - } - pub fn query(&self) -> &index::DocumentQuery { &self.document_ref } From 3e40f5a04cff4f260a4db27621233f05349be57e Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 11:17:00 -0500 Subject: [PATCH 23/44] Remove some `dead_code`s --- crates/server/src/edit.rs | 12 +------ crates/server/src/edit/text_document.rs | 33 ------------------- .../src/server/api/notifications/did_open.rs | 7 ++-- crates/server/src/server/client.rs | 2 +- crates/server/src/session/index.rs | 21 ++---------- 5 files changed, 6 insertions(+), 69 deletions(-) diff --git a/crates/server/src/edit.rs b/crates/server/src/edit.rs index cce4e902..c998552d 100644 --- a/crates/server/src/edit.rs +++ b/crates/server/src/edit.rs @@ -5,8 +5,8 @@ mod text_document; mod text_edit; use lsp_types::{PositionEncodingKind, Url}; +pub(crate) use text_document::DocumentVersion; pub(crate) use text_document::TextDocument; -pub(crate) use text_document::{DocumentVersion, LanguageId}; pub(crate) use text_edit::{Indel, TextEdit}; /// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`]. @@ -31,16 +31,6 @@ pub enum DocumentKey { Text(Url), } -impl DocumentKey { - /// Converts the key back into its original URL. - #[allow(dead_code)] - pub(crate) fn into_url(self) -> Url { - match self { - DocumentKey::Text(url) => url, - } - } -} - impl std::fmt::Display for DocumentKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/server/src/edit/text_document.rs b/crates/server/src/edit/text_document.rs index cf04c10d..0bebf7bf 100644 --- a/crates/server/src/edit/text_document.rs +++ b/crates/server/src/edit/text_document.rs @@ -23,23 +23,6 @@ pub struct TextDocument { /// The latest version of the document, set by the LSP client. The server will panic in /// debug mode if we attempt to update the document with an 'older' version. version: DocumentVersion, - /// The language ID of the document as provided by the client. - language_id: Option, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum LanguageId { - R, - Other, -} - -impl From<&str> for LanguageId { - fn from(language_id: &str) -> Self { - match language_id { - "r" => Self::R, - _ => Self::Other, - } - } } impl TextDocument { @@ -52,7 +35,6 @@ impl TextDocument { ending, index, version, - language_id: None, } } @@ -68,17 +50,6 @@ impl TextDocument { (doc, range) } - #[must_use] - pub fn with_language_id(mut self, language_id: &str) -> Self { - self.language_id = Some(LanguageId::from(language_id)); - self - } - - #[allow(dead_code)] - pub fn into_contents(self) -> String { - self.contents - } - pub fn contents(&self) -> &str { &self.contents } @@ -95,10 +66,6 @@ impl TextDocument { self.version } - pub fn language_id(&self) -> Option { - self.language_id - } - pub fn apply_changes( &mut self, mut changes: Vec, diff --git a/crates/server/src/server/api/notifications/did_open.rs b/crates/server/src/server/api/notifications/did_open.rs index daeb413d..c1528218 100644 --- a/crates/server/src/server/api/notifications/did_open.rs +++ b/crates/server/src/server/api/notifications/did_open.rs @@ -19,14 +19,11 @@ impl super::SyncNotificationHandler for DidOpen { types::DidOpenTextDocumentParams { text_document: types::TextDocumentItem { - uri, - text, - version, - language_id, + uri, text, version, .. }, }: types::DidOpenTextDocumentParams, ) -> Result<()> { - let document = TextDocument::new(text, version).with_language_id(&language_id); + let document = TextDocument::new(text, version); session.open_text_document(uri.clone(), document); diff --git a/crates/server/src/server/client.rs b/crates/server/src/server/client.rs index c5a502e2..f320dcb1 100644 --- a/crates/server/src/server/client.rs +++ b/crates/server/src/server/client.rs @@ -48,7 +48,7 @@ impl Client<'_> { } } -#[allow(dead_code)] // we'll need to use `Notifier` in the future +#[allow(dead_code)] // we'll need to use `Notifier` in the future to send notifs to the client impl Notifier { pub(crate) fn notify(&self, params: N::Params) -> crate::Result<()> where diff --git a/crates/server/src/session/index.rs b/crates/server/src/session/index.rs index d083cb70..a5d8000c 100644 --- a/crates/server/src/session/index.rs +++ b/crates/server/src/session/index.rs @@ -8,7 +8,6 @@ use rustc_hash::FxHashMap; use workspace::settings::Settings; -use crate::edit::LanguageId; use crate::edit::{DocumentKey, DocumentVersion, PositionEncoding, TextDocument}; use crate::session::workspaces::WorkspaceSettingsResolver; @@ -29,7 +28,8 @@ enum DocumentController { } /// A read-only query to an open document. -/// This query can 'select' a text document, full notebook, or a specific notebook cell. +/// This query can 'select' a text document, but eventually could gain support for +/// selecting notebooks or individual notebook cells. /// It also includes document settings. #[derive(Clone)] pub enum DocumentQuery { @@ -85,12 +85,10 @@ impl Index { self.settings.open_workspace_folder(url) } - #[allow(dead_code)] pub(super) fn num_documents(&self) -> usize { self.documents.len() } - #[allow(dead_code)] pub(super) fn num_workspaces(&self) -> usize { self.settings.len() } @@ -201,14 +199,6 @@ impl DocumentController { } impl DocumentQuery { - /// Retrieve the original key that describes this document query. - #[allow(dead_code)] - pub(crate) fn make_key(&self) -> DocumentKey { - match self { - Self::Text { file_url, .. } => DocumentKey::Text(file_url.clone()), - } - } - /// Get the document settings associated with this query. pub(crate) fn settings(&self) -> &Settings { match self { @@ -259,11 +249,4 @@ impl DocumentQuery { Self::Text { document, .. } => document, } } - - #[allow(dead_code)] - pub(crate) fn text_document_language_id(&self) -> Option { - // Optional because notebooks don't have a document language id - let DocumentQuery::Text { document, .. } = self; - document.language_id() - } } From dbc7ae6507b1df05398d48cccb5c93d9a4a0a2e8 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 11:23:38 -0500 Subject: [PATCH 24/44] Spacing --- crates/server/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index ef61cf71..be55962d 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -11,8 +11,8 @@ mod error; mod logging; mod proto; mod server; - mod session; + #[cfg(test)] mod test; From 5daa24a94966323341af00a65999b4315104d570 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 11:28:36 -0500 Subject: [PATCH 25/44] Prefer `anyhow::Result` not `crate::Result` --- crates/server/src/lib.rs | 4 ---- crates/server/src/message.rs | 2 +- crates/server/src/server.rs | 6 +++--- crates/server/src/server/api/requests/format.rs | 2 +- .../server/src/server/api/requests/format_range.rs | 2 +- crates/server/src/server/client.rs | 8 ++++---- crates/server/src/server/connection.rs | 10 +++++----- crates/server/src/server/schedule.rs | 6 +++--- crates/server/src/session.rs | 10 +++++----- crates/server/src/session/index.rs | 12 ++++++------ 10 files changed, 29 insertions(+), 33 deletions(-) diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index be55962d..d27c75f9 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -18,7 +18,3 @@ mod test; pub(crate) const SERVER_NAME: &str = "Air Language Server"; pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// A common result type used in most cases where a -/// result type is needed. -pub(crate) type Result = anyhow::Result; diff --git a/crates/server/src/message.rs b/crates/server/src/message.rs index 61ca7162..52bcfecd 100644 --- a/crates/server/src/message.rs +++ b/crates/server/src/message.rs @@ -25,7 +25,7 @@ pub(crate) fn show_message(message: String, message_type: lsp_types::MessageType pub(super) fn try_show_message( message: String, message_type: lsp_types::MessageType, -) -> crate::Result<()> { +) -> anyhow::Result<()> { MESSENGER .get() .ok_or_else(|| anyhow::anyhow!("Messenger not initialized"))? diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index c3d2ae1f..8e6bfc70 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -46,7 +46,7 @@ impl Server { worker_threads: NonZeroUsize, connection: lsp::Connection, connection_threads: Option, - ) -> crate::Result { + ) -> anyhow::Result { let initializer = ConnectionInitializer::new(connection, connection_threads); let (id, initialize_params) = initializer.initialize_start()?; @@ -105,7 +105,7 @@ impl Server { }) } - pub fn run(self) -> crate::Result<()> { + pub fn run(self) -> anyhow::Result<()> { // The new PanicInfoHook name requires MSRV >= 1.82 #[allow(deprecated)] type PanicHook = Box) + 'static + Sync + Send>; @@ -167,7 +167,7 @@ impl Server { resolved_client_capabilities: &ResolvedClientCapabilities, mut session: Session, worker_threads: NonZeroUsize, - ) -> crate::Result<()> { + ) -> anyhow::Result<()> { let mut scheduler = schedule::Scheduler::new(&mut session, worker_threads, connection.make_sender()); diff --git a/crates/server/src/server/api/requests/format.rs b/crates/server/src/server/api/requests/format.rs index d5547ef5..4adbfc84 100644 --- a/crates/server/src/server/api/requests/format.rs +++ b/crates/server/src/server/api/requests/format.rs @@ -70,7 +70,7 @@ fn format_text_document( fn format_source( source: &str, formatter_settings: &FormatSettings, -) -> crate::Result> { +) -> anyhow::Result> { let parse = air_r_parser::parse(source, RParserOptions::default()); if parse.has_errors() { diff --git a/crates/server/src/server/api/requests/format_range.rs b/crates/server/src/server/api/requests/format_range.rs index 94792b84..8da5112f 100644 --- a/crates/server/src/server/api/requests/format_range.rs +++ b/crates/server/src/server/api/requests/format_range.rs @@ -84,7 +84,7 @@ fn format_source_range( source: &str, formatter_settings: &FormatSettings, range: TextRange, -) -> crate::Result> { +) -> anyhow::Result> { let parse = air_r_parser::parse(source, RParserOptions::default()); if parse.has_errors() { diff --git a/crates/server/src/server/client.rs b/crates/server/src/server/client.rs index f320dcb1..3923eb3a 100644 --- a/crates/server/src/server/client.rs +++ b/crates/server/src/server/client.rs @@ -50,7 +50,7 @@ impl Client<'_> { #[allow(dead_code)] // we'll need to use `Notifier` in the future to send notifs to the client impl Notifier { - pub(crate) fn notify(&self, params: N::Params) -> crate::Result<()> + pub(crate) fn notify(&self, params: N::Params) -> anyhow::Result<()> where N: lsp_types::notification::Notification, { @@ -61,7 +61,7 @@ impl Notifier { self.0.send(message) } - pub(crate) fn notify_method(&self, method: String) -> crate::Result<()> { + pub(crate) fn notify_method(&self, method: String) -> anyhow::Result<()> { self.0 .send(lsp_server::Message::Notification(Notification::new( method, @@ -75,7 +75,7 @@ impl Responder { &self, id: RequestId, result: crate::server::Result, - ) -> crate::Result<()> + ) -> anyhow::Result<()> where R: serde::Serialize, { @@ -99,7 +99,7 @@ impl<'s> Requester<'s> { &mut self, params: R::Params, response_handler: impl Fn(R::Result) -> Task<'s> + 'static, - ) -> crate::Result<()> + ) -> anyhow::Result<()> where R: lsp_types::request::Request, { diff --git a/crates/server/src/server/connection.rs b/crates/server/src/server/connection.rs index 09388905..17963816 100644 --- a/crates/server/src/server/connection.rs +++ b/crates/server/src/server/connection.rs @@ -31,7 +31,7 @@ impl ConnectionInitializer { /// along with the initialization parameters that were provided. pub(super) fn initialize_start( &self, - ) -> crate::Result<(lsp::RequestId, lsp_types::InitializeParams)> { + ) -> anyhow::Result<(lsp::RequestId, lsp_types::InitializeParams)> { let (id, params) = self.connection.initialize_start()?; Ok((id, serde_json::from_value(params)?)) } @@ -44,7 +44,7 @@ impl ConnectionInitializer { server_capabilities: &lsp_types::ServerCapabilities, name: &str, version: &str, - ) -> crate::Result { + ) -> anyhow::Result { self.connection.initialize_finish( id, serde_json::json!({ @@ -81,7 +81,7 @@ impl Connection { } /// Check and respond to any incoming shutdown requests; returns`true` if the server should be shutdown. - pub(super) fn handle_shutdown(&self, message: &lsp::Message) -> crate::Result { + pub(super) fn handle_shutdown(&self, message: &lsp::Message) -> anyhow::Result { match message { lsp::Message::Request(lsp::Request { id, method, .. }) if method == lsp_types::request::Shutdown::METHOD => @@ -111,7 +111,7 @@ impl Connection { /// This is guaranteed to be nearly immediate since /// we close the only active channels to these threads prior /// to joining them. - pub(super) fn close(self) -> crate::Result<()> { + pub(super) fn close(self) -> anyhow::Result<()> { std::mem::drop( Arc::into_inner(self.sender) .expect("the client sender shouldn't have more than one strong reference"), @@ -134,7 +134,7 @@ pub(crate) struct ClientSender { // note: additional wrapper functions for senders may be implemented as needed. impl ClientSender { - pub(crate) fn send(&self, msg: lsp::Message) -> crate::Result<()> { + pub(crate) fn send(&self, msg: lsp::Message) -> anyhow::Result<()> { let Some(sender) = self.weak_sender.upgrade() else { anyhow::bail!("The connection with the client has been closed"); }; diff --git a/crates/server/src/server/schedule.rs b/crates/server/src/server/schedule.rs index 270986c8..dc1a8bc5 100644 --- a/crates/server/src/server/schedule.rs +++ b/crates/server/src/server/schedule.rs @@ -19,8 +19,8 @@ use super::{client::Client, ClientSender}; /// than some OS defaults (Windows, for example) and is also designated as /// high-priority. pub(crate) fn event_loop_thread( - func: impl FnOnce() -> crate::Result<()> + Send + 'static, -) -> crate::Result>> { + func: impl FnOnce() -> anyhow::Result<()> + Send + 'static, +) -> anyhow::Result>> { // Override OS defaults to avoid stack overflows on platforms with low stack size defaults. const MAIN_THREAD_STACK_SIZE: usize = 2 * 1024 * 1024; const MAIN_THREAD_NAME: &str = "air:main"; @@ -61,7 +61,7 @@ impl<'s> Scheduler<'s> { &mut self, params: R::Params, response_handler: impl Fn(R::Result) -> Task<'s> + 'static, - ) -> crate::Result<()> + ) -> anyhow::Result<()> where R: lsp_types::request::Request, { diff --git a/crates/server/src/session.rs b/crates/server/src/session.rs index 72e1a230..4836a40b 100644 --- a/crates/server/src/session.rs +++ b/crates/server/src/session.rs @@ -34,7 +34,7 @@ impl Session { resolved_client_capabilities: ResolvedClientCapabilities, position_encoding: PositionEncoding, workspace_folders: Vec, - ) -> crate::Result { + ) -> anyhow::Result { Ok(Self { position_encoding, index: index::Index::new(workspace_folders)?, @@ -69,7 +69,7 @@ impl Session { key: &DocumentKey, content_changes: Vec, new_version: DocumentVersion, - ) -> crate::Result<()> { + ) -> anyhow::Result<()> { let encoding = self.encoding(); self.index @@ -84,7 +84,7 @@ impl Session { /// De-registers a document, specified by its key. /// Calling this multiple times for the same document is a logic error. - pub(crate) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { + pub(crate) fn close_document(&mut self, key: &DocumentKey) -> anyhow::Result<()> { self.index.close_document(key)?; Ok(()) } @@ -95,12 +95,12 @@ impl Session { } /// Open a workspace folder at the given `url`. - pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { + pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { self.index.open_workspace_folder(url) } /// Close a workspace folder at the given `url`. - pub(crate) fn close_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { + pub(crate) fn close_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { self.index.close_workspace_folder(url)?; Ok(()) } diff --git a/crates/server/src/session/index.rs b/crates/server/src/session/index.rs index a5d8000c..129d4281 100644 --- a/crates/server/src/session/index.rs +++ b/crates/server/src/session/index.rs @@ -42,7 +42,7 @@ pub enum DocumentQuery { } impl Index { - pub(super) fn new(workspace_folders: Vec) -> crate::Result { + pub(super) fn new(workspace_folders: Vec) -> anyhow::Result { Ok(Self { documents: FxHashMap::default(), settings: WorkspaceSettingsResolver::from_workspace_folders(workspace_folders), @@ -62,7 +62,7 @@ impl Index { content_changes: Vec, new_version: DocumentVersion, encoding: PositionEncoding, - ) -> crate::Result<()> { + ) -> anyhow::Result<()> { let controller = self.document_controller_for_key(key)?; let Some(document) = controller.as_text_mut() else { anyhow::bail!("Text document URI does not point to a text document"); @@ -81,7 +81,7 @@ impl Index { DocumentKey::Text(url) } - pub(super) fn open_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { + pub(super) fn open_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { self.settings.open_workspace_folder(url) } @@ -93,7 +93,7 @@ impl Index { self.settings.len() } - pub(super) fn close_workspace_folder(&mut self, url: &Url) -> crate::Result<()> { + pub(super) fn close_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { self.settings.close_workspace_folder(url)?; Ok(()) } @@ -117,7 +117,7 @@ impl Index { .insert(url, DocumentController::new_text(document)); } - pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { + pub(super) fn close_document(&mut self, key: &DocumentKey) -> anyhow::Result<()> { let Some(url) = self.url_for_key(key).cloned() else { anyhow::bail!("Tried to close unavailable document `{key}`"); }; @@ -149,7 +149,7 @@ impl Index { fn document_controller_for_key( &mut self, key: &DocumentKey, - ) -> crate::Result<&mut DocumentController> { + ) -> anyhow::Result<&mut DocumentController> { let Some(url) = self.url_for_key(key).cloned() else { anyhow::bail!("Tried to open unavailable document `{key}`"); }; From 508467361072214dc3b0fab78e7e130ad0dfa916 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 12:57:01 -0500 Subject: [PATCH 26/44] Add a `SourceFile` for ergonomics Because it is really nice to bind the `contents` to its `index` so you dont have to pass them around separately --- crates/server/src/edit/text_document.rs | 83 +++----- crates/server/src/proto/text_edit.rs | 12 +- crates/server/src/proto/text_range.rs | 28 +-- crates/server/src/proto/text_size.rs | 55 ++--- .../server/src/server/api/requests/format.rs | 6 +- .../src/server/api/requests/format_range.rs | 8 +- crates/server/src/test/client_ext.rs | 9 +- crates/source_file/src/lib.rs | 3 +- crates/source_file/src/line_index.rs | 113 +--------- crates/source_file/src/source_file.rs | 193 ++++++++++++++++++ 10 files changed, 277 insertions(+), 233 deletions(-) create mode 100644 crates/source_file/src/source_file.rs diff --git a/crates/server/src/edit/text_document.rs b/crates/server/src/edit/text_document.rs index 0bebf7bf..76d05cfa 100644 --- a/crates/server/src/edit/text_document.rs +++ b/crates/server/src/edit/text_document.rs @@ -1,7 +1,7 @@ use biome_rowan::TextRange; use lsp_types::TextDocumentContentChangeEvent; use source_file::LineEnding; -use source_file::LineIndex; +use source_file::SourceFile; use crate::edit::PositionEncoding; use crate::proto::TextRangeExt; @@ -12,14 +12,12 @@ pub(crate) type DocumentVersion = i32; /// with changes made by the user, including unsaved changes. #[derive(Debug, Clone)] pub struct TextDocument { - /// The string contents of the document, normalized to unix line endings. - contents: String, - /// The original line endings of the document. + /// The source file containing the contents and line index for the document. + /// Line endings have been normalized to unix line endings here. + source: SourceFile, + /// The original line endings of the document. Used when sending changes back to the + /// LSP client. ending: LineEnding, - /// A computed line index for the document. This should always reflect - /// the current version of `contents`. Using a function like [`Self::modify`] - /// will re-calculate the line index automatically when the `contents` value is updated. - index: LineIndex, /// The latest version of the document, set by the LSP client. The server will panic in /// debug mode if we attempt to update the document with an 'older' version. version: DocumentVersion, @@ -27,13 +25,12 @@ pub struct TextDocument { impl TextDocument { pub fn new(contents: String, version: DocumentVersion) -> Self { - // Normalize to Unix line endings + // Normalize to Unix line endings on the way in let (contents, ending) = source_file::normalize_newlines(contents); - let index = LineIndex::from_source_text(&contents); + let source = SourceFile::new(contents); Self { - contents, + source, ending, - index, version, } } @@ -51,15 +48,15 @@ impl TextDocument { } pub fn contents(&self) -> &str { - &self.contents + self.source.contents() } pub fn ending(&self) -> LineEnding { self.ending } - pub fn index(&self) -> &LineIndex { - &self.index + pub fn source_file(&self) -> &SourceFile { + &self.source } pub fn version(&self) -> DocumentVersion { @@ -81,21 +78,16 @@ impl TextDocument { (change.text, _) = source_file::normalize_newlines(text); } - if let [lsp_types::TextDocumentContentChangeEvent { - range: None, text, .. - }] = changes.as_slice() + if let [lsp_types::TextDocumentContentChangeEvent { range: None, .. }] = changes.as_slice() { tracing::trace!("Fast path - replacing entire document"); - self.modify(|contents, version| { - contents.clone_from(text); - *version = new_version; - }); + // Unwrap: If-let ensures there is exactly 1 change event + let change = changes.pop().unwrap(); + self.source = SourceFile::new(change.text); + self.update_version(new_version); return; } - let mut new_contents = self.contents().to_string(); - let mut active_index = self.index().clone(); - for TextDocumentContentChangeEvent { range, text: change, @@ -103,47 +95,24 @@ impl TextDocument { } in changes { if let Some(range) = range { - let range = TextRange::from_proto(range, &new_contents, &active_index, encoding); - - new_contents.replace_range( + // Replace a range and rebuild the line index + let range = TextRange::from_proto(range, &self.source, encoding); + self.source.replace_range( usize::from(range.start())..usize::from(range.end()), &change, ); } else { - new_contents = change; + // Replace the whole file + self.source = SourceFile::new(change); } - - active_index = LineIndex::from_source_text(&new_contents); } - self.modify_with_manual_index(|contents, version, index| { - *index = active_index; - *contents = new_contents; - *version = new_version; - }); + self.update_version(new_version); } pub fn update_version(&mut self, new_version: DocumentVersion) { - self.modify_with_manual_index(|_, version, _| { - *version = new_version; - }); - } - - // A private function for modifying the document's internal state - fn modify(&mut self, func: impl FnOnce(&mut String, &mut DocumentVersion)) { - self.modify_with_manual_index(|c, v, i| { - func(c, v); - *i = LineIndex::from_source_text(c); - }); - } - - // A private function for overriding how we update the line index by default. - fn modify_with_manual_index( - &mut self, - func: impl FnOnce(&mut String, &mut DocumentVersion, &mut LineIndex), - ) { let old_version = self.version; - func(&mut self.contents, &mut self.version, &mut self.index); + self.version = new_version; debug_assert!(self.version >= old_version); } } @@ -204,7 +173,7 @@ def interface(): ); assert_eq!( - &document.contents, + document.contents(), r#"""" 测试comment 一些测试内容 @@ -270,6 +239,6 @@ def interface(): document.version + 1, PositionEncoding::UTF16, ); - assert_eq!(document.contents, "a𐐀bar"); + assert_eq!(document.contents(), "a𐐀bar"); } } diff --git a/crates/server/src/proto/text_edit.rs b/crates/server/src/proto/text_edit.rs index 0fcf2eee..916914b1 100644 --- a/crates/server/src/proto/text_edit.rs +++ b/crates/server/src/proto/text_edit.rs @@ -1,5 +1,5 @@ use source_file::LineEnding; -use source_file::LineIndex; +use source_file::SourceFile; use crate::edit::Indel; use crate::edit::PositionEncoding; @@ -9,13 +9,12 @@ use crate::proto::TextRangeExt; impl TextEdit { pub(crate) fn into_proto( self, - text: &str, - index: &LineIndex, + source: &SourceFile, encoding: PositionEncoding, ending: LineEnding, ) -> anyhow::Result> { self.into_iter() - .map(|indel| indel.into_proto(text, index, encoding, ending)) + .map(|indel| indel.into_proto(source, encoding, ending)) .collect() } } @@ -23,12 +22,11 @@ impl TextEdit { impl Indel { fn into_proto( self, - text: &str, - index: &LineIndex, + source: &SourceFile, encoding: PositionEncoding, ending: LineEnding, ) -> anyhow::Result { - let range = self.delete.into_proto(text, index, encoding); + let range = self.delete.into_proto(source, encoding); let new_text = match ending { LineEnding::Lf => self.insert, LineEnding::Crlf => self.insert.replace('\n', "\r\n"), diff --git a/crates/server/src/proto/text_range.rs b/crates/server/src/proto/text_range.rs index a0a1da70..900364e5 100644 --- a/crates/server/src/proto/text_range.rs +++ b/crates/server/src/proto/text_range.rs @@ -2,37 +2,27 @@ use crate::edit::PositionEncoding; use crate::proto::TextSizeExt; use biome_text_size::{TextRange, TextSize}; use lsp_types as types; -use source_file::LineIndex; +use source_file::SourceFile; // We don't own this type so we need a helper trait pub(crate) trait TextRangeExt { - fn into_proto(self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range; + fn into_proto(self, source: &SourceFile, encoding: PositionEncoding) -> types::Range; - fn from_proto( - range: types::Range, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, - ) -> Self; + fn from_proto(range: types::Range, source: &SourceFile, encoding: PositionEncoding) -> Self; } impl TextRangeExt for TextRange { - fn into_proto(self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range { + fn into_proto(self, source: &SourceFile, encoding: PositionEncoding) -> types::Range { types::Range { - start: self.start().into_proto(text, index, encoding), - end: self.end().into_proto(text, index, encoding), + start: self.start().into_proto(source, encoding), + end: self.end().into_proto(source, encoding), } } - fn from_proto( - range: types::Range, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, - ) -> Self { + fn from_proto(range: types::Range, source: &SourceFile, encoding: PositionEncoding) -> Self { TextRange::new( - TextSize::from_proto(range.start, text, index, encoding), - TextSize::from_proto(range.end, text, index, encoding), + TextSize::from_proto(range.start, source, encoding), + TextSize::from_proto(range.end, source, encoding), ) } } diff --git a/crates/server/src/proto/text_size.rs b/crates/server/src/proto/text_size.rs index 5ecf9365..759a66ad 100644 --- a/crates/server/src/proto/text_size.rs +++ b/crates/server/src/proto/text_size.rs @@ -3,66 +3,52 @@ use biome_rowan::TextRange; use biome_text_size::TextSize; use lsp_types as types; use source_file::OneIndexed; -use source_file::{LineIndex, SourceLocation}; +use source_file::{SourceFile, SourceLocation}; // We don't own this type so we need a helper trait pub(crate) trait TextSizeExt { - fn into_proto( - self, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, - ) -> types::Position; + fn into_proto(self, source: &SourceFile, encoding: PositionEncoding) -> types::Position; fn from_proto( position: types::Position, - text: &str, - index: &LineIndex, + source: &SourceFile, encoding: PositionEncoding, ) -> Self; } impl TextSizeExt for TextSize { - fn into_proto( - self, - text: &str, - index: &LineIndex, - encoding: PositionEncoding, - ) -> types::Position { - source_location_to_position(&offset_to_source_location(self, text, index, encoding)) + fn into_proto(self, source: &SourceFile, encoding: PositionEncoding) -> types::Position { + source_location_to_position(&offset_to_source_location(self, source, encoding)) } fn from_proto( position: types::Position, - text: &str, - index: &LineIndex, + source: &SourceFile, encoding: PositionEncoding, ) -> Self { - let line = index.line_range( - OneIndexed::from_zero_indexed(u32_index_to_usize(position.line)), - text, - ); + let line = source.line_range(OneIndexed::from_zero_indexed(u32_index_to_usize( + position.line, + ))); let column_offset = match encoding { PositionEncoding::UTF8 => TextSize::from(position.character), PositionEncoding::UTF16 => { // Fast path for ASCII only documents - if index.is_ascii() { + if source.is_ascii() { TextSize::from(position.character) } else { // UTF-16 encodes characters either as one or two 16 bit words. // The `position` is the 16-bit word offset from the start of the line (and not the character offset) - utf8_column_offset(position.character, &text[line]) + utf8_column_offset(position.character, &source.contents()[line]) } } PositionEncoding::UTF32 => { // UTF-32 uses 4 bytes for each character. Meaning, the position is a character offset. - return index.offset( + return source.offset( OneIndexed::from_zero_indexed(u32_index_to_usize(position.line)), OneIndexed::from_zero_indexed(u32_index_to_usize(position.character)), - text, ); } }; @@ -99,14 +85,13 @@ fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { fn offset_to_source_location( offset: TextSize, - text: &str, - index: &LineIndex, + source: &SourceFile, encoding: PositionEncoding, ) -> SourceLocation { match encoding { PositionEncoding::UTF8 => { - let row = index.line_index(offset); - let column = offset - index.line_start(row, text); + let row = source.line_index(offset); + let column = offset - source.line_start(row); SourceLocation { column: OneIndexed::from_zero_indexed(column.into()), @@ -114,12 +99,12 @@ fn offset_to_source_location( } } PositionEncoding::UTF16 => { - let row = index.line_index(offset); + let row = source.line_index(offset); - let column = if index.is_ascii() { - (offset - index.line_start(row, text)).into() + let column = if source.is_ascii() { + (offset - source.line_start(row)).into() } else { - let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)]; + let up_to_line = &source.contents()[TextRange::new(source.line_start(row), offset)]; up_to_line.encode_utf16().count() }; @@ -128,7 +113,7 @@ fn offset_to_source_location( row, } } - PositionEncoding::UTF32 => index.source_location(offset, text), + PositionEncoding::UTF32 => source.source_location(offset), } } diff --git a/crates/server/src/server/api/requests/format.rs b/crates/server/src/server/api/requests/format.rs index 4adbfc84..2acaa5be 100644 --- a/crates/server/src/server/api/requests/format.rs +++ b/crates/server/src/server/api/requests/format.rs @@ -47,8 +47,8 @@ fn format_text_document( let document_settings = query.settings(); let formatter_settings = &document_settings.format; - let text = text_document.contents(); - let index = text_document.index(); + let source = text_document.source_file(); + let text = source.contents(); let ending = text_document.ending(); let new_text = format_source(text, formatter_settings) @@ -61,7 +61,7 @@ fn format_text_document( let text_edit = TextEdit::diff(text, &new_text); let edits = text_edit - .into_proto(text, index, encoding, ending) + .into_proto(source, encoding, ending) .with_failure_code(lsp_server::ErrorCode::InternalError)?; Ok(Some(edits)) diff --git a/crates/server/src/server/api/requests/format_range.rs b/crates/server/src/server/api/requests/format_range.rs index 8da5112f..a159b39d 100644 --- a/crates/server/src/server/api/requests/format_range.rs +++ b/crates/server/src/server/api/requests/format_range.rs @@ -60,10 +60,10 @@ fn format_text_document_range( let document_settings = query.settings(); let formatter_settings = &document_settings.format; - let text = text_document.contents(); let ending = text_document.ending(); - let index = text_document.index(); - let range = TextRange::from_proto(range, text, index, encoding); + let source = text_document.source_file(); + let text = source.contents(); + let range = TextRange::from_proto(range, source, encoding); let Some((new_text, new_range)) = format_source_range(text, formatter_settings, range) .with_failure_code(lsp_server::ErrorCode::InternalError)? @@ -74,7 +74,7 @@ fn format_text_document_range( let text_edit = TextEdit::replace(new_range, new_text); let edits = text_edit - .into_proto(text, index, encoding, ending) + .into_proto(source, encoding, ending) .with_failure_code(lsp_server::ErrorCode::InternalError)?; Ok(Some(edits)) diff --git a/crates/server/src/test/client_ext.rs b/crates/server/src/test/client_ext.rs index 68047115..5596a478 100644 --- a/crates/server/src/test/client_ext.rs +++ b/crates/server/src/test/client_ext.rs @@ -96,7 +96,7 @@ impl TestClientExt for TestClient { ..Default::default() }; - let range = range.into_proto(doc.contents(), doc.index(), self.position_encoding()); + let range = range.into_proto(doc.source_file(), self.position_encoding()); self.range_formatting(lsp_types::DocumentRangeFormattingParams { text_document: lsp_types::TextDocumentIdentifier { @@ -133,10 +133,9 @@ fn apply_text_edits( ) -> anyhow::Result { use std::ops::Range; - let text = doc.contents(); - let mut new_text = text.to_string(); + let mut new_text = doc.contents().to_string(); - let index = doc.index(); + let source = doc.source_file(); // Apply edits from bottom to top to avoid inserted newlines to invalidate // positions in earlier parts of the doc (they are sent in reading order @@ -144,7 +143,7 @@ fn apply_text_edits( edits.reverse(); for edit in edits { - let range: Range = TextRange::from_proto(edit.range, text, index, encoding).into(); + let range: Range = TextRange::from_proto(edit.range, source, encoding).into(); new_text.replace_range(range, &edit.new_text); } diff --git a/crates/source_file/src/lib.rs b/crates/source_file/src/lib.rs index 92ef04e1..897a77e0 100644 --- a/crates/source_file/src/lib.rs +++ b/crates/source_file/src/lib.rs @@ -1,12 +1,13 @@ #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -pub use crate::line_index::LineIndex; pub use crate::newlines::{find_newline, infer_line_ending, normalize_newlines, LineEnding}; pub use crate::one_indexed::OneIndexed; +pub use crate::source_file::SourceFile; pub use crate::source_location::SourceLocation; mod line_index; mod newlines; mod one_indexed; +mod source_file; mod source_location; diff --git a/crates/source_file/src/line_index.rs b/crates/source_file/src/line_index.rs index 2051137b..78f1969a 100644 --- a/crates/source_file/src/line_index.rs +++ b/crates/source_file/src/line_index.rs @@ -14,7 +14,7 @@ use crate::SourceLocation; /// /// Cloning a [`LineIndex`] is cheap because it only requires bumping a reference count. #[derive(Clone, Eq, PartialEq)] -pub struct LineIndex { +pub(crate) struct LineIndex { inner: Arc, } @@ -26,7 +26,7 @@ struct LineIndexInner { impl LineIndex { /// Builds the [`LineIndex`] from the source text of a file. - pub fn from_source_text(text: &str) -> Self { + pub(crate) fn from_source_text(text: &str) -> Self { let mut line_starts: Vec = Vec::with_capacity(text.len() / 88); line_starts.push(TextSize::default()); @@ -66,34 +66,7 @@ impl LineIndex { } /// Returns the row and column index for an offset. - /// - /// ## Examples - /// - /// ``` - /// # use biome_text_size::TextSize; - /// # use source_file::{LineIndex, OneIndexed, SourceLocation}; - /// let source = "def a():\n pass"; - /// let index = LineIndex::from_source_text(source); - /// - /// assert_eq!( - /// index.source_location(TextSize::from(0), source), - /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } - /// ); - /// - /// assert_eq!( - /// index.source_location(TextSize::from(4), source), - /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } - /// ); - /// assert_eq!( - /// index.source_location(TextSize::from(13), source), - /// SourceLocation { row: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) } - /// ); - /// ``` - /// - /// ## Panics - /// - /// If the offset is out of bounds. - pub fn source_location(&self, offset: TextSize, content: &str) -> SourceLocation { + pub(crate) fn source_location(&self, offset: TextSize, content: &str) -> SourceLocation { match self.line_starts().binary_search(&offset) { // Offset is at the start of a line Ok(row) => SourceLocation { @@ -125,34 +98,21 @@ impl LineIndex { } /// Return the number of lines in the source code. - pub fn line_count(&self) -> usize { + pub(crate) fn line_count(&self) -> usize { self.line_starts().len() } /// Returns `true` if the text only consists of ASCII characters - pub fn is_ascii(&self) -> bool { + pub(crate) fn is_ascii(&self) -> bool { self.kind().is_ascii() } /// Returns the row number for a given offset. /// - /// ## Examples - /// - /// ``` - /// # use biome_text_size::TextSize; - /// # use source_file::{LineIndex, OneIndexed, SourceLocation}; - /// let source = "def a():\n pass"; - /// let index = LineIndex::from_source_text(source); - /// - /// assert_eq!(index.line_index(TextSize::from(0)), OneIndexed::from_zero_indexed(0)); - /// assert_eq!(index.line_index(TextSize::from(4)), OneIndexed::from_zero_indexed(0)); - /// assert_eq!(index.line_index(TextSize::from(13)), OneIndexed::from_zero_indexed(1)); - /// ``` - /// /// ## Panics /// /// If the offset is out of bounds. - pub fn line_index(&self, offset: TextSize) -> OneIndexed { + pub(crate) fn line_index(&self, offset: TextSize) -> OneIndexed { match self.line_starts().binary_search(&offset) { // Offset is at the start of a line Ok(row) => OneIndexed::from_zero_indexed(row), @@ -164,7 +124,7 @@ impl LineIndex { } /// Returns the [byte offset](TextSize) for the `line` with the given index. - pub fn line_start(&self, line: OneIndexed, contents: &str) -> TextSize { + pub(crate) fn line_start(&self, line: OneIndexed, contents: &str) -> TextSize { let row_index = line.to_zero_indexed(); let starts = self.line_starts(); @@ -178,7 +138,7 @@ impl LineIndex { /// Returns the [byte offset](TextSize) of the `line`'s end. /// The offset is the end of the line, up to and including the newline character ending the line (if any). - pub fn line_end(&self, line: OneIndexed, contents: &str) -> TextSize { + pub(crate) fn line_end(&self, line: OneIndexed, contents: &str) -> TextSize { let row_index = line.to_zero_indexed(); let starts = self.line_starts(); @@ -192,7 +152,7 @@ impl LineIndex { /// Returns the [byte offset](TextSize) of the `line`'s end. /// The offset is the end of the line, excluding the newline character ending the line (if any). - pub fn line_end_exclusive(&self, line: OneIndexed, contents: &str) -> TextSize { + pub(crate) fn line_end_exclusive(&self, line: OneIndexed, contents: &str) -> TextSize { let row_index = line.to_zero_indexed(); let starts = self.line_starts(); @@ -207,7 +167,7 @@ impl LineIndex { /// Returns the [`TextRange`] of the `line` with the given index. /// The start points to the first character's [byte offset](TextSize), the end up to, and including /// the newline character ending the line (if any). - pub fn line_range(&self, line: OneIndexed, contents: &str) -> TextRange { + pub(crate) fn line_range(&self, line: OneIndexed, contents: &str) -> TextRange { let starts = self.line_starts(); if starts.len() == line.to_zero_indexed() { @@ -221,58 +181,7 @@ impl LineIndex { } /// Returns the [byte offset](TextSize) at `line` and `column`. - /// - /// ## Examples - /// - /// ### ASCII - /// - /// ``` - /// use source_file::{LineIndex, OneIndexed}; - /// use biome_text_size::TextSize; - /// let source = r#"a = 4 - /// c = "some string" - /// x = b"#; - /// - /// let index = LineIndex::from_source_text(source); - /// - /// // First line, first column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::from(0)); - /// - /// // Second line, 4th column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(4), source), TextSize::from(10)); - /// - /// // Offset past the end of the first line - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(10), source), TextSize::from(6)); - /// - /// // Offset past the end of the file - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::from(29)); - /// ``` - /// - /// ### UTF8 - /// - /// ``` - /// use source_file::{LineIndex, OneIndexed}; - /// use biome_text_size::TextSize; - /// let source = r#"a = 4 - /// c = "❤️" - /// x = b"#; - /// - /// let index = LineIndex::from_source_text(source); - /// - /// // First line, first column - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0), source), TextSize::from(0)); - /// - /// // Third line, 2nd column, after emoji - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(2), OneIndexed::from_zero_indexed(1), source), TextSize::from(20)); - /// - /// // Offset past the end of the second line - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(10), source), TextSize::from(19)); - /// - /// // Offset past the end of the file - /// assert_eq!(index.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0), source), TextSize::from(24)); - /// ``` - /// - pub fn offset(&self, line: OneIndexed, column: OneIndexed, contents: &str) -> TextSize { + pub(crate) fn offset(&self, line: OneIndexed, column: OneIndexed, contents: &str) -> TextSize { // If start-of-line position after last line if line.to_zero_indexed() > self.line_starts().len() { return contents.text_len(); diff --git a/crates/source_file/src/source_file.rs b/crates/source_file/src/source_file.rs new file mode 100644 index 00000000..473a8e2e --- /dev/null +++ b/crates/source_file/src/source_file.rs @@ -0,0 +1,193 @@ +use biome_text_size::TextRange; +use biome_text_size::TextSize; + +use crate::line_index::LineIndex; +use crate::OneIndexed; +use crate::SourceLocation; + +/// Manager of a single source file +/// +/// Builds a [LineIndex] on creation, and associates that index with the source it +/// was created from for future method calls. +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct SourceFile { + contents: String, + index: LineIndex, +} + +impl SourceFile { + /// Builds the [`SourceFile`] from the contents of a file. + pub fn new(contents: String) -> Self { + let index = LineIndex::from_source_text(&contents); + Self { contents, index } + } + + /// Returns a reference to the contents in the source file. + pub fn contents(&self) -> &str { + &self.contents + } + + /// Consumes the source file, returning only the contents. + pub fn into_contents(self) -> String { + self.contents + } + + /// Replace text in the source file and rebuild the line index afterwards. + pub fn replace_range(&mut self, range: R, replace_with: &str) + where + R: std::ops::RangeBounds, + { + self.contents.replace_range(range, replace_with); + self.index = LineIndex::from_source_text(&self.contents); + } + + /// Returns the row and column index for an offset. + /// + /// ## Examples + /// + /// ``` + /// # use biome_text_size::TextSize; + /// # use source_file::{SourceFile, OneIndexed, SourceLocation}; + /// let source = "def a():\n pass".to_string(); + /// let source = SourceFile::new(source); + /// + /// assert_eq!( + /// source.source_location(TextSize::from(0)), + /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } + /// ); + /// + /// assert_eq!( + /// source.source_location(TextSize::from(4)), + /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } + /// ); + /// assert_eq!( + /// source.source_location(TextSize::from(13)), + /// SourceLocation { row: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) } + /// ); + /// ``` + /// + /// ## Panics + /// + /// If the offset is out of bounds. + pub fn source_location(&self, offset: TextSize) -> SourceLocation { + self.index.source_location(offset, self.contents()) + } + + /// Return the number of lines in the source code. + pub fn line_count(&self) -> usize { + self.index.line_count() + } + + /// Returns `true` if the text only consists of ASCII characters + pub fn is_ascii(&self) -> bool { + self.index.is_ascii() + } + + /// Returns the row number for a given offset. + /// + /// ## Examples + /// + /// ``` + /// # use biome_text_size::TextSize; + /// # use source_file::{SourceFile, OneIndexed, SourceLocation}; + /// let source = "def a():\n pass".to_string(); + /// let source = SourceFile::new(source); + /// + /// assert_eq!(source.line_index(TextSize::from(0)), OneIndexed::from_zero_indexed(0)); + /// assert_eq!(source.line_index(TextSize::from(4)), OneIndexed::from_zero_indexed(0)); + /// assert_eq!(source.line_index(TextSize::from(13)), OneIndexed::from_zero_indexed(1)); + /// ``` + /// + /// ## Panics + /// + /// If the offset is out of bounds. + pub fn line_index(&self, offset: TextSize) -> OneIndexed { + self.index.line_index(offset) + } + + /// Returns the [byte offset](TextSize) for the `line` with the given index. + pub fn line_start(&self, line: OneIndexed) -> TextSize { + self.index.line_start(line, self.contents()) + } + + /// Returns the [byte offset](TextSize) of the `line`'s end. + /// The offset is the end of the line, up to and including the newline character ending the line (if any). + pub fn line_end(&self, line: OneIndexed) -> TextSize { + self.index.line_end(line, self.contents()) + } + + /// Returns the [byte offset](TextSize) of the `line`'s end. + /// The offset is the end of the line, excluding the newline character ending the line (if any). + pub fn line_end_exclusive(&self, line: OneIndexed) -> TextSize { + self.index.line_end_exclusive(line, self.contents()) + } + + /// Returns the [`TextRange`] of the `line` with the given index. + /// The start points to the first character's [byte offset](TextSize), the end up to, and including + /// the newline character ending the line (if any). + pub fn line_range(&self, line: OneIndexed) -> TextRange { + self.index.line_range(line, self.contents()) + } + + /// Returns the [byte offset](TextSize) at `line` and `column`. + /// + /// ## Examples + /// + /// ### ASCII + /// + /// ``` + /// use source_file::{SourceFile, OneIndexed}; + /// use biome_text_size::TextSize; + /// let source = r#"a = 4 + /// c = "some string" + /// x = b"#.to_string(); + /// + /// let source = SourceFile::new(source); + /// + /// // First line, first column + /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0)), TextSize::from(0)); + /// + /// // Second line, 4th column + /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(4)), TextSize::from(10)); + /// + /// // Offset past the end of the first line + /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(10)), TextSize::from(6)); + /// + /// // Offset past the end of the file + /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0)), TextSize::from(29)); + /// ``` + /// + /// ### UTF8 + /// + /// ``` + /// use source_file::{SourceFile, OneIndexed}; + /// use biome_text_size::TextSize; + /// let source = r#"a = 4 + /// c = "❤️" + /// x = b"#.to_string(); + /// + /// let source = SourceFile::new(source); + /// + /// // First line, first column + /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0)), TextSize::from(0)); + /// + /// // Third line, 2nd column, after emoji + /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(2), OneIndexed::from_zero_indexed(1)), TextSize::from(20)); + /// + /// // Offset past the end of the second line + /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(10)), TextSize::from(19)); + /// + /// // Offset past the end of the file + /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0)), TextSize::from(24)); + /// ``` + /// + pub fn offset(&self, line: OneIndexed, column: OneIndexed) -> TextSize { + self.index.offset(line, column, self.contents()) + } + + /// Returns the [byte offsets](TextSize) for every line + pub fn line_starts(&self) -> &[TextSize] { + self.index.line_starts() + } +} From f7c251f0bcb05f005294baf64fedcbfa51b413d1 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 13:10:18 -0500 Subject: [PATCH 27/44] Rename `edit` to `document` --- crates/server/src/document.rs | 13 ++++++++ .../src/{edit.rs => document/encoding.rs} | 30 ++----------------- crates/server/src/document/key.rs | 20 +++++++++++++ .../src/{edit => document}/text_diff.rs | 0 .../src/{edit => document}/text_document.rs | 4 +-- .../src/{edit => document}/text_edit.rs | 0 crates/server/src/lib.rs | 2 +- crates/server/src/proto/text_edit.rs | 6 ++-- crates/server/src/proto/text_range.rs | 2 +- crates/server/src/proto/text_size.rs | 2 +- crates/server/src/server.rs | 2 +- .../src/server/api/notifications/did_open.rs | 2 +- .../server/src/server/api/requests/format.rs | 6 ++-- .../src/server/api/requests/format_range.rs | 6 ++-- crates/server/src/session.rs | 2 +- crates/server/src/session/index.rs | 2 +- crates/server/src/test/client_ext.rs | 8 ++--- 17 files changed, 58 insertions(+), 49 deletions(-) create mode 100644 crates/server/src/document.rs rename crates/server/src/{edit.rs => document/encoding.rs} (59%) create mode 100644 crates/server/src/document/key.rs rename crates/server/src/{edit => document}/text_diff.rs (100%) rename crates/server/src/{edit => document}/text_document.rs (98%) rename crates/server/src/{edit => document}/text_edit.rs (100%) diff --git a/crates/server/src/document.rs b/crates/server/src/document.rs new file mode 100644 index 00000000..21572a4f --- /dev/null +++ b/crates/server/src/document.rs @@ -0,0 +1,13 @@ +//! Types and utilities for working with documents + +mod encoding; +mod key; +mod text_diff; +mod text_document; +mod text_edit; + +pub(crate) use encoding::PositionEncoding; +pub(crate) use key::DocumentKey; +pub(crate) use text_document::DocumentVersion; +pub(crate) use text_document::TextDocument; +pub(crate) use text_edit::{Indel, TextEdit}; diff --git a/crates/server/src/edit.rs b/crates/server/src/document/encoding.rs similarity index 59% rename from crates/server/src/edit.rs rename to crates/server/src/document/encoding.rs index c998552d..6d0ddba6 100644 --- a/crates/server/src/edit.rs +++ b/crates/server/src/document/encoding.rs @@ -1,18 +1,9 @@ -//! Types and utilities for working with text and modifying source files +use lsp_types::PositionEncodingKind; -mod text_diff; -mod text_document; -mod text_edit; - -use lsp_types::{PositionEncodingKind, Url}; -pub(crate) use text_document::DocumentVersion; -pub(crate) use text_document::TextDocument; -pub(crate) use text_edit::{Indel, TextEdit}; - -/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`]. +/// A convenient enumeration for supported [lsp_types::Position] encodings. Can be converted to [`lsp_types::PositionEncodingKind`]. // Please maintain the order from least to greatest priority for the derived `Ord` impl. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub enum PositionEncoding { +pub(crate) enum PositionEncoding { /// UTF 16 is the encoding supported by all LSP clients. #[default] UTF16, @@ -24,21 +15,6 @@ pub enum PositionEncoding { UTF8, } -/// A unique document ID, derived from a URL passed as part of an LSP request. -/// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook. -#[derive(Clone, Debug)] -pub enum DocumentKey { - Text(Url), -} - -impl std::fmt::Display for DocumentKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Text(url) => url.fmt(f), - } - } -} - impl From for lsp_types::PositionEncodingKind { fn from(value: PositionEncoding) -> Self { match value { diff --git a/crates/server/src/document/key.rs b/crates/server/src/document/key.rs new file mode 100644 index 00000000..d5665668 --- /dev/null +++ b/crates/server/src/document/key.rs @@ -0,0 +1,20 @@ +use url::Url; + +/// A unique document ID, derived from a URL passed as part of an LSP request. +/// This document ID currently always points to an R file, but eventually can also +/// point to a full notebook, or a cell within a notebook. +#[derive(Clone, Debug)] +pub(crate) enum DocumentKey { + Text(Url), + // If we ever want to support notebooks, start here: + // Notebook(Url), + // NotebookCell(Url), +} + +impl std::fmt::Display for DocumentKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Text(url) => url.fmt(f), + } + } +} diff --git a/crates/server/src/edit/text_diff.rs b/crates/server/src/document/text_diff.rs similarity index 100% rename from crates/server/src/edit/text_diff.rs rename to crates/server/src/document/text_diff.rs diff --git a/crates/server/src/edit/text_document.rs b/crates/server/src/document/text_document.rs similarity index 98% rename from crates/server/src/edit/text_document.rs rename to crates/server/src/document/text_document.rs index 76d05cfa..6e1540b0 100644 --- a/crates/server/src/edit/text_document.rs +++ b/crates/server/src/document/text_document.rs @@ -3,7 +3,7 @@ use lsp_types::TextDocumentContentChangeEvent; use source_file::LineEnding; use source_file::SourceFile; -use crate::edit::PositionEncoding; +use crate::document::PositionEncoding; use crate::proto::TextRangeExt; pub(crate) type DocumentVersion = i32; @@ -119,7 +119,7 @@ impl TextDocument { #[cfg(test)] mod tests { - use crate::edit::{PositionEncoding, TextDocument}; + use crate::document::{PositionEncoding, TextDocument}; use lsp_types::{Position, TextDocumentContentChangeEvent}; #[test] diff --git a/crates/server/src/edit/text_edit.rs b/crates/server/src/document/text_edit.rs similarity index 100% rename from crates/server/src/edit/text_edit.rs rename to crates/server/src/document/text_edit.rs diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index d27c75f9..72d6cf5b 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -6,7 +6,7 @@ pub use server::Server; mod message; mod crates; -mod edit; +mod document; mod error; mod logging; mod proto; diff --git a/crates/server/src/proto/text_edit.rs b/crates/server/src/proto/text_edit.rs index 916914b1..cc74a46b 100644 --- a/crates/server/src/proto/text_edit.rs +++ b/crates/server/src/proto/text_edit.rs @@ -1,9 +1,9 @@ use source_file::LineEnding; use source_file::SourceFile; -use crate::edit::Indel; -use crate::edit::PositionEncoding; -use crate::edit::TextEdit; +use crate::document::Indel; +use crate::document::PositionEncoding; +use crate::document::TextEdit; use crate::proto::TextRangeExt; impl TextEdit { diff --git a/crates/server/src/proto/text_range.rs b/crates/server/src/proto/text_range.rs index 900364e5..17938863 100644 --- a/crates/server/src/proto/text_range.rs +++ b/crates/server/src/proto/text_range.rs @@ -1,4 +1,4 @@ -use crate::edit::PositionEncoding; +use crate::document::PositionEncoding; use crate::proto::TextSizeExt; use biome_text_size::{TextRange, TextSize}; use lsp_types as types; diff --git a/crates/server/src/proto/text_size.rs b/crates/server/src/proto/text_size.rs index 759a66ad..368172f2 100644 --- a/crates/server/src/proto/text_size.rs +++ b/crates/server/src/proto/text_size.rs @@ -1,4 +1,4 @@ -use crate::edit::PositionEncoding; +use crate::document::PositionEncoding; use biome_rowan::TextRange; use biome_text_size::TextSize; use lsp_types as types; diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index 8e6bfc70..10aac60c 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -19,7 +19,7 @@ use self::connection::Connection; use self::schedule::event_loop_thread; use self::schedule::Scheduler; use self::schedule::Task; -use crate::edit::PositionEncoding; +use crate::document::PositionEncoding; use crate::message::try_show_message; use crate::server::connection::ConnectionInitializer; use crate::session::ResolvedClientCapabilities; diff --git a/crates/server/src/server/api/notifications/did_open.rs b/crates/server/src/server/api/notifications/did_open.rs index c1528218..ce12b20c 100644 --- a/crates/server/src/server/api/notifications/did_open.rs +++ b/crates/server/src/server/api/notifications/did_open.rs @@ -1,4 +1,4 @@ -use crate::edit::TextDocument; +use crate::document::TextDocument; use crate::server::client::{Notifier, Requester}; use crate::server::Result; use crate::session::Session; diff --git a/crates/server/src/server/api/requests/format.rs b/crates/server/src/server/api/requests/format.rs index 2acaa5be..a6c13681 100644 --- a/crates/server/src/server/api/requests/format.rs +++ b/crates/server/src/server/api/requests/format.rs @@ -3,8 +3,8 @@ use biome_formatter::LineEnding; use lsp_types::{self as types, request as req}; use workspace::settings::FormatSettings; -use crate::edit::TextEdit; -use crate::edit::{PositionEncoding, TextDocument}; +use crate::document::TextEdit; +use crate::document::{PositionEncoding, TextDocument}; use crate::server::api::LSPResult; use crate::server::{client::Notifier, Result}; use crate::session::{DocumentQuery, DocumentSnapshot}; @@ -95,7 +95,7 @@ fn format_source( #[cfg(test)] mod tests { - use crate::edit::TextDocument; + use crate::document::TextDocument; use crate::{test::init_test_client, test::TestClientExt}; #[test] diff --git a/crates/server/src/server/api/requests/format_range.rs b/crates/server/src/server/api/requests/format_range.rs index a159b39d..1e5aa813 100644 --- a/crates/server/src/server/api/requests/format_range.rs +++ b/crates/server/src/server/api/requests/format_range.rs @@ -11,8 +11,8 @@ use biome_text_size::{TextRange, TextSize}; use lsp_types::{self as types, request as req, Range}; use workspace::settings::FormatSettings; -use crate::edit::TextEdit; -use crate::edit::{PositionEncoding, TextDocument}; +use crate::document::TextEdit; +use crate::document::{PositionEncoding, TextDocument}; use crate::proto::TextRangeExt; use crate::server::api::LSPResult; use crate::server::{client::Notifier, Result}; @@ -269,7 +269,7 @@ fn find_expression_lists(node: &RSyntaxNode, offset: TextSize, end: bool) -> Vec #[cfg(test)] mod tests { - use crate::edit::TextDocument; + use crate::document::TextDocument; use crate::{test::init_test_client, test::TestClientExt}; #[test] diff --git a/crates/server/src/session.rs b/crates/server/src/session.rs index 4836a40b..ddef1766 100644 --- a/crates/server/src/session.rs +++ b/crates/server/src/session.rs @@ -3,7 +3,7 @@ use lsp_types::Url; use lsp_types::WorkspaceFolder; -use crate::edit::{DocumentKey, DocumentVersion, PositionEncoding, TextDocument}; +use crate::document::{DocumentKey, DocumentVersion, PositionEncoding, TextDocument}; pub(crate) use self::capabilities::ResolvedClientCapabilities; pub use self::index::DocumentQuery; diff --git a/crates/server/src/session/index.rs b/crates/server/src/session/index.rs index 129d4281..9d1401a5 100644 --- a/crates/server/src/session/index.rs +++ b/crates/server/src/session/index.rs @@ -8,7 +8,7 @@ use rustc_hash::FxHashMap; use workspace::settings::Settings; -use crate::edit::{DocumentKey, DocumentVersion, PositionEncoding, TextDocument}; +use crate::document::{DocumentKey, DocumentVersion, PositionEncoding, TextDocument}; use crate::session::workspaces::WorkspaceSettingsResolver; /// Stores and tracks all open documents in a session, along with their associated settings. diff --git a/crates/server/src/test/client_ext.rs b/crates/server/src/test/client_ext.rs index 5596a478..36a1f91a 100644 --- a/crates/server/src/test/client_ext.rs +++ b/crates/server/src/test/client_ext.rs @@ -1,8 +1,8 @@ use biome_text_size::TextRange; use server_test::TestClient; -use crate::edit::PositionEncoding; -use crate::edit::TextDocument; +use crate::document::PositionEncoding; +use crate::document::TextDocument; use crate::proto::TextRangeExt; pub(crate) trait TestClientExt { @@ -128,8 +128,8 @@ impl TestClientExt for TestClient { fn apply_text_edits( mut edits: Vec, - doc: &crate::edit::TextDocument, - encoding: crate::edit::PositionEncoding, + doc: &crate::document::TextDocument, + encoding: crate::document::PositionEncoding, ) -> anyhow::Result { use std::ops::Range; From 10ba9ce4eab0c252c1fca525d71a876c6854fee4 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Thu, 2 Jan 2025 14:48:59 -0500 Subject: [PATCH 28/44] Use a global test client accessible through `with_client()` Allowing us to remove our special casing code in logging/messaging --- crates/server/Cargo.toml | 2 +- crates/server/src/logging.rs | 40 ++-- crates/server/src/message.rs | 14 +- crates/server/src/server.rs | 13 +- .../server/src/server/api/requests/format.rs | 43 ++-- .../src/server/api/requests/format_range.rs | 211 ++++++++---------- crates/server/src/test.rs | 2 +- crates/server/src/test/client.rs | 106 ++++----- crates/server/tests/initialization.rs | 63 ++++++ crates/server_test/src/lib.rs | 1 + 10 files changed, 245 insertions(+), 250 deletions(-) create mode 100644 crates/server/tests/initialization.rs diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 609cf26c..e693f564 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -32,6 +32,7 @@ lsp-types = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +server_test = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["ansi", "local-time"] } tree-sitter = { workspace = true } @@ -42,7 +43,6 @@ uuid = { workspace = true, features = ["v4"] } [dev-dependencies] assert_matches = { workspace = true } insta = { workspace = true } -server_test = { workspace = true } [target.'cfg(target_vendor = "apple")'.dependencies] libc = { workspace = true } diff --git a/crates/server/src/logging.rs b/crates/server/src/logging.rs index 0f476e20..ad203fb8 100644 --- a/crates/server/src/logging.rs +++ b/crates/server/src/logging.rs @@ -39,6 +39,7 @@ use core::str; use lsp_server::Message; use lsp_types::notification::LogMessage; use lsp_types::notification::Notification; +use lsp_types::ClientInfo; use lsp_types::LogMessageParams; use lsp_types::MessageType; use serde::Deserialize; @@ -120,30 +121,21 @@ impl<'a> MakeWriter<'a> for LogWriterMaker { } } -/// We use a special `TestWriter` during tests to be compatible with `cargo test`'s -/// typical output capturing behavior. -/// -/// Important notes: -/// - `cargo test` swallows all logs unless you use `-- --nocapture`. -/// - Tests run in parallel, so logs can be interleaved unless you run `--test-threads 1`. -/// -/// We use `cargo test -- --nocapture --test-threads 1` on CI because of all of this. pub(crate) fn init_logging( client_tx: ClientSender, log_level: Option, dependency_log_levels: Option, - client_name: Option, - is_test_client: bool, + client_info: Option, ) { let log_level = resolve_log_level(log_level); let dependency_log_levels = resolve_dependency_log_levels(dependency_log_levels); - let writer = if client_name.is_some_and(|client_name| { - client_name.starts_with("Zed") || client_name.starts_with("Visual Studio Code") + let writer = if client_info.as_ref().is_some_and(|client_info| { + client_info.name.starts_with("Zed") || client_info.name.starts_with("Visual Studio Code") }) { // These IDEs are known to support `window/logMessage` well BoxMakeWriter::new(LogWriterMaker::new(client_tx)) - } else if is_test_client { + } else if is_test_client(client_info.as_ref()) { // Ensures a standard `cargo test` captures output unless `-- --nocapture` is used BoxMakeWriter::new(TestWriter::default()) } else { @@ -176,18 +168,24 @@ pub(crate) fn init_logging( let subscriber = tracing_subscriber::Registry::default().with(layer); - if is_test_client { - // During parallel testing, `set_global_default()` gets called multiple times - // per process. That causes it to error, but we ignore this. - tracing::subscriber::set_global_default(subscriber).ok(); - } else { - tracing::subscriber::set_global_default(subscriber) - .expect("Should be able to set the global subscriber exactly once."); - } + tracing::subscriber::set_global_default(subscriber) + .expect("Should be able to set the global subscriber exactly once."); tracing::info!("Logging initialized with level: {log_level}"); } +/// We use a special `TestWriter` during tests to be compatible with `cargo test`'s +/// typical output capturing behavior (even during integration tests!). +/// +/// Importantly, note that `cargo test` swallows all logs unless you use `-- --nocapture`, +/// which is the correct expected behavior. We use `cargo test -- --nocapture` on CI +/// because of this. +fn is_test_client(client_info: Option<&ClientInfo>) -> bool { + client_info.map_or(false, |client_info| { + client_info.name == server_test::TEST_CLIENT_NAME + }) +} + fn log_filter(log_level: LogLevel, dependency_log_levels: Option) -> filter::Targets { // Initialize `filter` from dependency log levels. // If nothing is supplied, dependency logs are completely off. diff --git a/crates/server/src/message.rs b/crates/server/src/message.rs index 52bcfecd..d8675800 100644 --- a/crates/server/src/message.rs +++ b/crates/server/src/message.rs @@ -6,16 +6,10 @@ use crate::server::ClientSender; static MESSENGER: OnceLock = OnceLock::new(); -pub(crate) fn init_messenger(client_sender: ClientSender, is_test_client: bool) { - let result = MESSENGER.set(client_sender); - - // During testing, `init_messenger()` will be called multiple times - // within the same process, potentially at the same time across threads. - // This probably isn't great, because if we call `show_err_msg!()` from a - // test thread where the `ClientSender` has been shutdown, then we will panic. - if !is_test_client { - result.expect("Messenger should only be initialized once"); - } +pub(crate) fn init_messenger(client_sender: ClientSender) { + MESSENGER + .set(client_sender) + .expect("Messenger should only be initialized once"); } pub(crate) fn show_message(message: String, message_type: lsp_types::MessageType) { diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index 10aac60c..526e2f5b 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -71,14 +71,6 @@ impl Server { let workspace_folders = workspace_folders.unwrap_or_default(); - let client_name = client_info - .as_ref() - .map(|client_info| client_info.name.clone()); - - let is_test_client = client_name - .as_ref() - .map_or(false, |client_name| client_name == "AirTestClient"); - // TODO: Get user specified options from `initialization_options` let log_level = None; let dependency_log_levels = None; @@ -87,11 +79,10 @@ impl Server { connection.make_sender(), log_level, dependency_log_levels, - client_name, - is_test_client, + client_info, ); - crate::message::init_messenger(connection.make_sender(), is_test_client); + crate::message::init_messenger(connection.make_sender()); Ok(Self { connection, diff --git a/crates/server/src/server/api/requests/format.rs b/crates/server/src/server/api/requests/format.rs index a6c13681..2c6bf45b 100644 --- a/crates/server/src/server/api/requests/format.rs +++ b/crates/server/src/server/api/requests/format.rs @@ -96,48 +96,41 @@ fn format_source( #[cfg(test)] mod tests { use crate::document::TextDocument; - use crate::{test::init_test_client, test::TestClientExt}; + use crate::{test::with_client, test::TestClientExt}; #[test] fn test_format() { - let mut client = init_test_client(); - - #[rustfmt::skip] - let doc = TextDocument::doodle( -" -1 + with_client(|client| { + #[rustfmt::skip] + let doc = TextDocument::doodle( +"1 2+2 3 + 3 + 3", - ); - - let formatted = client.format_document(&doc); - insta::assert_snapshot!(formatted); + ); - client.shutdown(); - client.exit(); + let formatted = client.format_document(&doc); + insta::assert_snapshot!(formatted); + }); } // https://github.com/posit-dev/air/issues/61 #[test] fn test_format_minimal_diff() { - let mut client = init_test_client(); - - #[rustfmt::skip] - let doc = TextDocument::doodle( + with_client(|client| { + #[rustfmt::skip] + let doc = TextDocument::doodle( "1 2+2 3 ", - ); - - let edits = client.format_document_edits(&doc).unwrap(); - assert!(edits.len() == 1); + ); - let edit = &edits[0]; - assert_eq!(edit.new_text, " + "); + let edits = client.format_document_edits(&doc).unwrap(); + assert_eq!(edits.len(), 1); - client.shutdown(); - client.exit(); + let edit = &edits[0]; + assert_eq!(edit.new_text, " + "); + }); } } diff --git a/crates/server/src/server/api/requests/format_range.rs b/crates/server/src/server/api/requests/format_range.rs index 1e5aa813..8564622e 100644 --- a/crates/server/src/server/api/requests/format_range.rs +++ b/crates/server/src/server/api/requests/format_range.rs @@ -270,57 +270,53 @@ fn find_expression_lists(node: &RSyntaxNode, offset: TextSize, end: bool) -> Vec #[cfg(test)] mod tests { use crate::document::TextDocument; - use crate::{test::init_test_client, test::TestClientExt}; + use crate::{test::with_client, test::TestClientExt}; #[test] fn test_format_range_none() { - let mut client = init_test_client(); - - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + with_client(|client| { + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "<<>>", - ); + ); - let output = client.format_document_range(&doc, range); - insta::assert_snapshot!(output); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "<< >>", - ); + ); - let output = client.format_document_range(&doc, range); - insta::assert_snapshot!(output); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "<<1 >>", - ); + ); - let output = client.format_document_range(&doc, range); - insta::assert_snapshot!(output); - - client.shutdown(); - client.exit(); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + }); } #[test] fn test_format_range_logical_lines() { - let mut client = init_test_client(); - - // 2+2 is the logical line to format - #[rustfmt::skip] + with_client(|client| { + // 2+2 is the logical line to format + #[rustfmt::skip] let (doc, range) = TextDocument::doodle_and_range( "1+1 <<2+2>> ", ); - let output = client.format_document_range(&doc, range); - insta::assert_snapshot!(output); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); - #[rustfmt::skip] + #[rustfmt::skip] let (doc, range) = TextDocument::doodle_and_range( "1+1 # @@ -328,32 +324,32 @@ mod tests { ", ); - let output = client.format_document_range(&doc, range); - insta::assert_snapshot!(output); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); - // The element in the braced expression is a logical line - // FIXME: Should this be the whole `{2+2}` instead? - #[rustfmt::skip] + // The element in the braced expression is a logical line + // FIXME: Should this be the whole `{2+2}` instead? + #[rustfmt::skip] let (doc, range) = TextDocument::doodle_and_range( "1+1 {<<2+2>>} ", ); - let output = client.format_document_range(&doc, range); - insta::assert_snapshot!(output); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); - #[rustfmt::skip] + #[rustfmt::skip] let (doc, range) = TextDocument::doodle_and_range( "1+1 <<{2+2}>> ", ); - let output = client.format_document_range(&doc, range); - insta::assert_snapshot!(output); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); - // The deepest element in the braced expression is our target - #[rustfmt::skip] + // The deepest element in the braced expression is our target + #[rustfmt::skip] let (doc, range) = TextDocument::doodle_and_range( "1+1 { @@ -365,78 +361,69 @@ mod tests { ", ); - let output = client.format_document_range(&doc, range); - insta::assert_snapshot!(output); - - client.shutdown(); - client.exit(); + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); + }); } #[test] fn test_format_range_mismatched_indent() { - let mut client = init_test_client(); - - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + with_client(|client| { + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "1 <<2+2>> ", - ); + ); - // We don't change indentation when `2+2` is formatted - let output = client.format_document_range(&doc, range); - insta::assert_snapshot!(output); + // We don't change indentation when `2+2` is formatted + let output = client.format_document_range(&doc, range); + insta::assert_snapshot!(output); - // Debatable: Should we make an effort to remove unneeded indentation - // when it's part of the range? - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + // Debatable: Should we make an effort to remove unneeded indentation + // when it's part of the range? + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "1 << 2+2>> ", - ); - let output_wide = client.format_document_range(&doc, range); - assert_eq!(output, output_wide); - - client.shutdown(); - client.exit(); + ); + let output_wide = client.format_document_range(&doc, range); + assert_eq!(output, output_wide); + }); } #[test] fn test_format_range_multiple_lines() { - let mut client = init_test_client(); - - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + with_client(|client| { + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "1+1 <<# 2+2>> ", - ); + ); - let output1 = client.format_document_range(&doc, range); - insta::assert_snapshot!(output1); + let output1 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output1); - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "<<1+1 # 2+2>> ", - ); - let output2 = client.format_document_range(&doc, range); - insta::assert_snapshot!(output2); - - client.shutdown(); - client.exit(); + ); + let output2 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output2); + }); } #[test] fn test_format_range_unmatched_lists() { - let mut client = init_test_client(); - - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + with_client(|client| { + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "0+0 <<1+1 { @@ -444,13 +431,13 @@ mod tests { } 3+3 ", - ); + ); - let output1 = client.format_document_range(&doc, range); - insta::assert_snapshot!(output1); + let output1 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output1); - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "0+0 <<1+1 { @@ -458,12 +445,12 @@ mod tests { } 3+3 ", - ); - let output2 = client.format_document_range(&doc, range); - insta::assert_snapshot!(output2); + ); + let output2 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output2); - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "0+0 <<1+1 { @@ -471,12 +458,12 @@ mod tests { } >>3+3 ", - ); - let output3 = client.format_document_range(&doc, range); - insta::assert_snapshot!(output3); + ); + let output3 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output3); - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "0+0 1+1 { @@ -484,31 +471,29 @@ mod tests { } >>3+3 ", - ); - let output4 = client.format_document_range(&doc, range); - insta::assert_snapshot!(output4); + ); + let output4 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output4); - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "<<1+1>> 2+2 ", - ); + ); - let output5 = client.format_document_range(&doc, range); - insta::assert_snapshot!(output5); + let output5 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output5); - #[rustfmt::skip] - let (doc, range) = TextDocument::doodle_and_range( + #[rustfmt::skip] + let (doc, range) = TextDocument::doodle_and_range( "1+1 <<2+2>> ", - ); - - let output6 = client.format_document_range(&doc, range); - insta::assert_snapshot!(output6); + ); - client.shutdown(); - client.exit(); + let output6 = client.format_document_range(&doc, range); + insta::assert_snapshot!(output6); + }); } } diff --git a/crates/server/src/test.rs b/crates/server/src/test.rs index d240fc6b..f2a9279e 100644 --- a/crates/server/src/test.rs +++ b/crates/server/src/test.rs @@ -2,6 +2,6 @@ mod client; mod client_ext; mod utils; -pub(crate) use client::init_test_client; +pub(crate) use client::with_client; pub(crate) use client_ext::TestClientExt; pub(crate) use utils::extract_marked_range; diff --git a/crates/server/src/test/client.rs b/crates/server/src/test/client.rs index d1b13b18..a965634d 100644 --- a/crates/server/src/test/client.rs +++ b/crates/server/src/test/client.rs @@ -1,78 +1,48 @@ +use std::sync::LazyLock; +use std::sync::Mutex; + +use server_test::TestClient; + use crate::Server; -pub(crate) fn init_test_client() -> server_test::TestClient { - let mut client = start_test_client(); +/// Global test client used by all unit tests +/// +/// The language [Server] has per-process global state, such as the global `tracing` +/// subscriber and a global `MESSENGER` to send `ShowMessage` notifications to the client. +/// Because of this, we cannot just repeatedly call [test_client()] to start up a new +/// client/server pair per unit test. Instead, unit tests use [with_client()] to access +/// the global test client, which they can then manipulate. Synchronization is managed +/// through a [Mutex], ensuring that multiple unit tests that need to mutate the client +/// can't run simultaneously (while still allowing other unit tests to run in parallel). +/// Unit tests should be careful not to put the client/server pair into a state that +/// prevents other unit tests from running successfully. +/// +/// If you need to modify a client/server pair in such a way that no other unit tests will +/// be able to use it, create an integration test instead, which runs in its own process. +static TEST_CLIENT: LazyLock> = LazyLock::new(|| Mutex::new(test_client())); + +pub(crate) fn with_client(f: F) +where + F: FnOnce(&mut server_test::TestClient), +{ + let mut client = TEST_CLIENT.lock().unwrap(); + f(&mut client) +} + +fn test_client() -> server_test::TestClient { + let mut client = + server_test::TestClient::new(|worker_threads, connection, connection_threads| { + let server = Server::new(worker_threads, connection, connection_threads).unwrap(); + server.run().unwrap(); + }); + // Initialize and wait for the server response let id = client.initialize(); let response = client.recv_response(); assert_eq!(id, response.id); + + // Notify the server we have received its initialize response client.initialized(); client } - -fn start_test_client() -> server_test::TestClient { - server_test::TestClient::new(|worker_threads, connection, connection_threads| { - let server = Server::new(worker_threads, connection, connection_threads).unwrap(); - server.run().unwrap(); - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use assert_matches::assert_matches; - use lsp_types::PositionEncodingKind; - use lsp_types::ServerCapabilities; - use lsp_types::ServerInfo; - use lsp_types::TextDocumentSyncCapability; - use lsp_types::TextDocumentSyncKind; - use lsp_types::TextDocumentSyncOptions; - - #[test] - fn test_init() { - let mut client = start_test_client(); - - let id = client.initialize(); - - let value = client.recv_response(); - assert_eq!(id, value.id); - let value: lsp_types::InitializeResult = - serde_json::from_value(value.result.unwrap().clone()).unwrap(); - - client.initialized(); - - assert_matches!( - value, - lsp_types::InitializeResult { - capabilities, - server_info - } => { - assert_matches!(capabilities, ServerCapabilities { - position_encoding, - text_document_sync, - .. - } => { - assert_eq!(position_encoding, Some(PositionEncodingKind::UTF8)); - assert_eq!(text_document_sync, Some(TextDocumentSyncCapability::Options( - TextDocumentSyncOptions { - open_close: Some(true), - change: Some(TextDocumentSyncKind::INCREMENTAL), - will_save: Some(false), - will_save_wait_until: Some(false), - ..Default::default() - }, - ))); - }); - - assert_matches!(server_info, Some(ServerInfo { name, version }) => { - assert!(name.contains("Air Language Server")); - assert!(version.is_some()); - }); - } - ); - - client.shutdown(); - client.exit(); - } -} diff --git a/crates/server/tests/initialization.rs b/crates/server/tests/initialization.rs new file mode 100644 index 00000000..a4a14b3f --- /dev/null +++ b/crates/server/tests/initialization.rs @@ -0,0 +1,63 @@ +use assert_matches::assert_matches; +use lsp_types::PositionEncodingKind; +use lsp_types::ServerCapabilities; +use lsp_types::ServerInfo; +use lsp_types::TextDocumentSyncCapability; +use lsp_types::TextDocumentSyncKind; +use lsp_types::TextDocumentSyncOptions; + +// Normal usage of `with_client()` handles client initialization, so to test it we have +// to run this particular test in its own process and manually start up and initialize +// the client. This also gives us a chance to test the shutdown/exit procedure. + +#[test] +fn test_initialization_and_shutdown() { + let mut client = + server_test::TestClient::new(|worker_threads, connection, connection_threads| { + let server = + server::Server::new(worker_threads, connection, connection_threads).unwrap(); + server.run().unwrap(); + }); + + let id = client.initialize(); + + let value = client.recv_response(); + assert_eq!(id, value.id); + let value: lsp_types::InitializeResult = + serde_json::from_value(value.result.unwrap().clone()).unwrap(); + + client.initialized(); + + assert_matches!( + value, + lsp_types::InitializeResult { + capabilities, + server_info + } => { + assert_matches!(capabilities, ServerCapabilities { + position_encoding, + text_document_sync, + .. + } => { + assert_eq!(position_encoding, Some(PositionEncodingKind::UTF8)); + assert_eq!(text_document_sync, Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + will_save: Some(false), + will_save_wait_until: Some(false), + ..Default::default() + }, + ))); + }); + + assert_matches!(server_info, Some(ServerInfo { name, version }) => { + assert!(name.contains("Air Language Server")); + assert!(version.is_some()); + }); + } + ); + + client.shutdown(); + client.exit(); +} diff --git a/crates/server_test/src/lib.rs b/crates/server_test/src/lib.rs index ac986b8d..988e382f 100644 --- a/crates/server_test/src/lib.rs +++ b/crates/server_test/src/lib.rs @@ -1,3 +1,4 @@ mod lsp_client; pub use lsp_client::TestClient; +pub use lsp_client::TEST_CLIENT_NAME; From 8368ea279cfe59bbc569c96f1014482eebf51331 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 09:39:12 -0500 Subject: [PATCH 29/44] Separate settings discovery from `SettingsResolver` --- crates/air/src/commands/format.rs | 17 ++- crates/server/src/session/workspaces.rs | 38 ++++-- crates/workspace/src/discovery.rs | 151 ++++++++++++++++++++++++ crates/workspace/src/lib.rs | 1 + crates/workspace/src/resolve.rs | 144 ---------------------- 5 files changed, 195 insertions(+), 156 deletions(-) create mode 100644 crates/workspace/src/discovery.rs diff --git a/crates/air/src/commands/format.rs b/crates/air/src/commands/format.rs index ae944c6d..aeae2817 100644 --- a/crates/air/src/commands/format.rs +++ b/crates/air/src/commands/format.rs @@ -11,8 +11,10 @@ use fs::relativize_path; use itertools::Either; use itertools::Itertools; use thiserror::Error; -use workspace::resolve::discover_r_file_paths; -use workspace::resolve::SettingsResolver; +use workspace::discovery::discover_r_file_paths; +use workspace::discovery::discover_settings; +use workspace::discovery::DiscoveredSettings; +use workspace::resolve::PathResolver; use workspace::settings::FormatSettings; use workspace::settings::Settings; @@ -24,8 +26,15 @@ pub(crate) fn format(command: FormatCommand) -> anyhow::Result { let paths = discover_r_file_paths(&command.paths); - let mut resolver = SettingsResolver::new(Settings::default()); - resolver.load_from_paths(&command.paths)?; + let mut resolver = PathResolver::new(Settings::default()); + + for DiscoveredSettings { + directory, + settings, + } in discover_settings(&command.paths)? + { + resolver.add(&directory, settings); + } let (actions, errors): (Vec<_>, Vec<_>) = paths .into_iter() diff --git a/crates/server/src/session/workspaces.rs b/crates/server/src/session/workspaces.rs index 94288e12..9f940280 100644 --- a/crates/server/src/session/workspaces.rs +++ b/crates/server/src/session/workspaces.rs @@ -3,10 +3,13 @@ use std::path::PathBuf; use lsp_types::Url; use lsp_types::WorkspaceFolder; +use workspace::discovery::discover_settings; +use workspace::discovery::DiscoveredSettings; use workspace::resolve::PathResolver; -use workspace::resolve::SettingsResolver; use workspace::settings::Settings; +type SettingsResolver = PathResolver; + /// Resolver for retrieving [`Settings`] associated with a workspace specific [`Path`] #[derive(Debug, Default)] pub(crate) struct WorkspaceSettingsResolver { @@ -61,7 +64,14 @@ impl WorkspaceSettingsResolver { let fallback = Settings::default(); let mut settings_resolver = SettingsResolver::new(fallback); - settings_resolver.load_from_paths(&[&path])?; + + for DiscoveredSettings { + directory, + settings, + } in discover_settings(&[&path])? + { + settings_resolver.add(&directory, settings); + } tracing::trace!("Adding workspace settings: {}", path.display()); self.path_to_settings_resolver.add(&path, settings_resolver); @@ -139,12 +149,24 @@ impl WorkspaceSettingsResolver { settings_resolver.clear(); - if let Err(error) = settings_resolver.load_from_paths(&[workspace_path]) { - tracing::error!( - "Failed to reload workspace settings for {path}:\n{error}", - path = workspace_path.display(), - error = error - ); + let discovered_settings = match discover_settings(&[workspace_path]) { + Ok(discovered_settings) => discovered_settings, + Err(error) => { + tracing::error!( + "Failed to reload workspace settings for {path}:\n{error}", + path = workspace_path.display(), + error = error + ); + continue; + } + }; + + for DiscoveredSettings { + directory, + settings, + } in discovered_settings + { + settings_resolver.add(&directory, settings); } } } diff --git a/crates/workspace/src/discovery.rs b/crates/workspace/src/discovery.rs new file mode 100644 index 00000000..fd005fb7 --- /dev/null +++ b/crates/workspace/src/discovery.rs @@ -0,0 +1,151 @@ +use ignore::DirEntry; +use rustc_hash::FxHashSet; +use std::path::Path; +use std::path::PathBuf; +use thiserror::Error; + +use crate::settings::Settings; +use crate::toml::find_air_toml_in_directory; +use crate::toml::parse_air_toml; +use crate::toml::ParseTomlError; + +#[derive(Debug, Error)] +pub enum DiscoverSettingsError { + #[error(transparent)] + ParseToml(#[from] ParseTomlError), +} + +#[derive(Debug)] +pub struct DiscoveredSettings { + pub directory: PathBuf, + pub settings: Settings, +} + +/// This is the core function for walking a set of `paths` looking for `air.toml`s. +/// +/// You typically follow this function up by loading the set of returned path into a +/// [crate::resolve::PathResolver]. +/// +/// For each `path`, we: +/// - Walk up its ancestors, looking for an `air.toml` +/// - TODO(hierarchical): Walk down its children, looking for nested `air.toml`s +pub fn discover_settings>( + paths: &[P], +) -> Result, DiscoverSettingsError> { + let paths: Vec = paths.iter().map(fs::normalize_path).collect(); + + let mut seen = FxHashSet::default(); + let mut discovered_settings = Vec::with_capacity(paths.len()); + + // Load the `resolver` with `Settings` associated with each `path` + for path in &paths { + for ancestor in path.ancestors() { + let is_new_ancestor = seen.insert(ancestor); + + if !is_new_ancestor { + // We already visited this ancestor, we can stop here. + break; + } + + if let Some(toml) = find_air_toml_in_directory(ancestor) { + let settings = parse_settings(&toml)?; + discovered_settings.push(DiscoveredSettings { + directory: ancestor.to_path_buf(), + settings, + }); + break; + } + } + } + + // TODO(hierarchical): Also iterate through the directories and collect `air.toml` + // found nested withing the directories for hierarchical support + + Ok(discovered_settings) +} + +/// Parse [Settings] from a given `air.toml` +// TODO(hierarchical): Allow for an `extends` option in `air.toml`, which will make things +// more complex, but will be very useful once we support hierarchical configuration as a +// way of "inheriting" most top level configuration while slightly tweaking it in a nested directory. +fn parse_settings(toml: &Path) -> Result { + let options = parse_air_toml(toml)?; + let settings = options.into_settings(); + Ok(settings) +} + +/// For each provided `path`, recursively search for any R files within that `path` +/// that match our inclusion criteria +/// +/// NOTE: Make sure that the inclusion criteria that guide `path` discovery are also +/// consistently applied to [discover_settings()]. +pub fn discover_r_file_paths>(paths: &[P]) -> Vec> { + let paths: Vec = paths.iter().map(fs::normalize_path).collect(); + + let Some((first_path, paths)) = paths.split_first() else { + // No paths provided + return Vec::new(); + }; + + // TODO: Parallel directory visitor + let mut builder = ignore::WalkBuilder::new(first_path); + + for path in paths { + builder.add(path); + } + + // TODO: Make these configurable options (possibly just one?) + // Right now we explicitly call them even though they are `true` by default + // to remind us to expose them. + // + // "This toggles, as a group, all the filters that are enabled by default" + // builder.standard_filters(true) + builder.hidden(true); + builder.parents(true); + builder.ignore(false); + builder.git_ignore(true); + builder.git_global(true); + builder.git_exclude(true); + + let mut paths = Vec::new(); + + // Walk all `paths` recursively, collecting R files that we can format + for path in builder.build() { + match path { + Ok(entry) => { + if let Some(path) = is_match(entry) { + paths.push(Ok(path)); + } + } + Err(err) => { + paths.push(Err(err)); + } + } + } + + paths +} + +// Decide whether or not to accept an `entry` based on include/exclude rules. +fn is_match(entry: DirEntry) -> Option { + // Ignore directories + if entry.file_type().map_or(true, |ft| ft.is_dir()) { + return None; + } + + // Accept all files that are passed-in directly, even non-R files + if entry.depth() == 0 { + let path = entry.into_path(); + return Some(path); + } + + // Otherwise check if we should accept this entry + // TODO: Many other checks based on user exclude/includes + let path = entry.into_path(); + + if !fs::has_r_extension(&path) { + return None; + } + + Some(path) +} diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index cb9835a0..a185a3c1 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -1,3 +1,4 @@ +pub mod discovery; pub mod resolve; pub mod settings; pub mod toml; diff --git a/crates/workspace/src/resolve.rs b/crates/workspace/src/resolve.rs index 3e275549..9c129431 100644 --- a/crates/workspace/src/resolve.rs +++ b/crates/workspace/src/resolve.rs @@ -4,15 +4,6 @@ use std::collections::btree_map::RangeMut; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use ignore::DirEntry; -use rustc_hash::FxHashSet; -use thiserror::Error; - -use crate::settings::Settings; -use crate::toml::find_air_toml_in_directory; -use crate::toml::parse_air_toml; -use crate::toml::ParseTomlError; - /// Resolves a [`Path`] to its associated `T` /// /// To use a [`PathResolver`]: @@ -109,138 +100,3 @@ impl PathResolver { self.map.range_mut(..path.to_path_buf()) } } - -pub type SettingsResolver = PathResolver; - -#[derive(Debug, Error)] -pub enum SettingsResolverError { - #[error(transparent)] - ParseToml(#[from] ParseTomlError), -} - -impl SettingsResolver { - /// This is the core function for walking a set of `paths` looking for `air.toml`s - /// and loading in any directories it finds - /// - /// For each `path`, we: - /// - Walk up its ancestors, looking for an `air.toml` - /// - TODO(hierarchical): Walk down its children, looking for nested `air.toml`s - /// - /// Whenever we find an `air.toml`, we add the directory it was found in and - /// the parsed [`Settings`] into the resolver. - pub fn load_from_paths>( - &mut self, - paths: &[P], - ) -> Result<(), SettingsResolverError> { - let paths: Vec = paths.iter().map(fs::normalize_path).collect(); - - let mut seen = FxHashSet::default(); - - // Load the `resolver` with `Settings` associated with each `path` - for path in &paths { - for ancestor in path.ancestors() { - if seen.insert(ancestor) { - if let Some(toml) = find_air_toml_in_directory(ancestor) { - let settings = Self::parse_settings(&toml)?; - self.add(ancestor, settings); - break; - } - } else { - // We already visited this ancestor, we can stop here. - break; - } - } - } - - // TODO(hierarchical): Also iterate through the directories and collect `air.toml` - // found nested withing the directories for hierarchical support - - Ok(()) - } - - /// Parse [Settings] from a given `air.toml` - // TODO(hierarchical): Allow for an `extends` option in `air.toml`, which will make things - // more complex, but will be very useful once we support hierarchical configuration as a - // way of "inheriting" most top level configuration while slightly tweaking it in a nested directory. - fn parse_settings(toml: &Path) -> Result { - let options = parse_air_toml(toml)?; - let settings = options.into_settings(); - Ok(settings) - } -} - -/// For each provided `path`, recursively search for any R files within that `path` -/// that match our inclusion criteria -/// -/// NOTE: Make sure that the inclusion criteria that guide `path` discovery are also -/// consistently applied to [SettingsResolver::load_from_paths()]. -pub fn discover_r_file_paths>(paths: &[P]) -> Vec> { - let paths: Vec = paths.iter().map(fs::normalize_path).collect(); - - let Some((first_path, paths)) = paths.split_first() else { - // No paths provided - return Vec::new(); - }; - - // TODO: Parallel directory visitor - let mut builder = ignore::WalkBuilder::new(first_path); - - for path in paths { - builder.add(path); - } - - // TODO: Make these configurable options (possibly just one?) - // Right now we explicitly call them even though they are `true` by default - // to remind us to expose them. - // - // "This toggles, as a group, all the filters that are enabled by default" - // builder.standard_filters(true) - builder.hidden(true); - builder.parents(true); - builder.ignore(false); - builder.git_ignore(true); - builder.git_global(true); - builder.git_exclude(true); - - let mut paths = Vec::new(); - - // Walk all `paths` recursively, collecting R files that we can format - for path in builder.build() { - match path { - Ok(entry) => { - if let Some(path) = is_match(entry) { - paths.push(Ok(path)); - } - } - Err(err) => { - paths.push(Err(err)); - } - } - } - - paths -} - -// Decide whether or not to accept an `entry` based on include/exclude rules. -fn is_match(entry: DirEntry) -> Option { - // Ignore directories - if entry.file_type().map_or(true, |ft| ft.is_dir()) { - return None; - } - - // Accept all files that are passed-in directly, even non-R files - if entry.depth() == 0 { - let path = entry.into_path(); - return Some(path); - } - - // Otherwise check if we should accept this entry - // TODO: Many other checks based on user exclude/includes - let path = entry.into_path(); - - if !fs::has_r_extension(&path) { - return None; - } - - Some(path) -} From ca41f9256c6e0047a422d8cdd717a056718886ec Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 09:55:32 -0500 Subject: [PATCH 30/44] Use `Arc` in the LSP To be able to even more cheaply create a `DocumentQuery` --- crates/server/src/session/index.rs | 12 ++++++------ crates/server/src/session/workspaces.rs | 23 ++++++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/server/src/session/index.rs b/crates/server/src/session/index.rs index 9d1401a5..cd720849 100644 --- a/crates/server/src/session/index.rs +++ b/crates/server/src/session/index.rs @@ -36,8 +36,7 @@ pub enum DocumentQuery { Text { file_url: Url, document: Arc, - // TODO: This should be `Arc` - settings: Settings, + settings: Arc, }, } @@ -101,10 +100,10 @@ impl Index { pub(super) fn make_document_ref(&self, key: DocumentKey) -> Option { let url = self.url_for_key(&key)?.clone(); - let document_settings = self.settings_for_url(&url).clone(); + let settings = self.settings_for_url(&url); let controller = self.documents.get(&url)?; - Some(controller.make_ref(url, document_settings)) + Some(controller.make_ref(url, settings)) } /// Reloads relevant existing settings files based on a changed settings file path. @@ -165,7 +164,7 @@ impl Index { } } - fn settings_for_url(&self, url: &Url) -> &Settings { + fn settings_for_url(&self, url: &Url) -> Arc { self.settings.settings_for_url(url) } } @@ -175,7 +174,7 @@ impl DocumentController { Self::Text(Arc::new(document)) } - fn make_ref(&self, file_url: Url, settings: Settings) -> DocumentQuery { + fn make_ref(&self, file_url: Url, settings: Arc) -> DocumentQuery { match &self { Self::Text(document) => DocumentQuery::Text { file_url, @@ -201,6 +200,7 @@ impl DocumentController { impl DocumentQuery { /// Get the document settings associated with this query. pub(crate) fn settings(&self) -> &Settings { + // Note that `&Arc` nicely derefs to `&Settings` here automatically match self { Self::Text { settings, .. } => settings, } diff --git a/crates/server/src/session/workspaces.rs b/crates/server/src/session/workspaces.rs index 9f940280..c57b8d46 100644 --- a/crates/server/src/session/workspaces.rs +++ b/crates/server/src/session/workspaces.rs @@ -1,5 +1,6 @@ use std::path::Path; use std::path::PathBuf; +use std::sync::Arc; use lsp_types::Url; use lsp_types::WorkspaceFolder; @@ -8,7 +9,11 @@ use workspace::discovery::DiscoveredSettings; use workspace::resolve::PathResolver; use workspace::settings::Settings; -type SettingsResolver = PathResolver; +/// Convenience type for the inner resolver of path -> [`Settings`] +/// +/// We store [`Settings`] in an [`Arc`] so we can easily share them across threads in a +/// `DocumentQuery` without needing to clone. +type SettingsResolver = PathResolver>; /// Resolver for retrieving [`Settings`] associated with a workspace specific [`Path`] #[derive(Debug, Default)] @@ -22,7 +27,7 @@ impl WorkspaceSettingsResolver { /// Construct a new workspace settings resolver from an initial set of workspace folders pub(crate) fn from_workspace_folders(workspace_folders: Vec) -> Self { // How to do better here? - let fallback = Settings::default(); + let fallback = Arc::new(Settings::default()); let settings_resolver_fallback = SettingsResolver::new(fallback); let path_to_settings_resolver = PathResolver::new(settings_resolver_fallback); @@ -61,7 +66,7 @@ impl WorkspaceSettingsResolver { }; // How to do better here? - let fallback = Settings::default(); + let fallback = Arc::new(Settings::default()); let mut settings_resolver = SettingsResolver::new(fallback); @@ -70,7 +75,7 @@ impl WorkspaceSettingsResolver { settings, } in discover_settings(&[&path])? { - settings_resolver.add(&directory, settings); + settings_resolver.add(&directory, Arc::new(settings)); } tracing::trace!("Adding workspace settings: {}", path.display()); @@ -100,7 +105,7 @@ impl WorkspaceSettingsResolver { } /// Return the appropriate [`Settings`] for a given document [`Url`]. - pub(crate) fn settings_for_url(&self, url: &Url) -> &Settings { + pub(crate) fn settings_for_url(&self, url: &Url) -> Arc { if let Ok(Some(path)) = Self::url_to_path(url) { return self.settings_for_path(&path); } @@ -116,7 +121,7 @@ impl WorkspaceSettingsResolver { } tracing::trace!("Using default settings for non-file URL: {url}"); - self.path_to_settings_resolver.fallback().fallback() + self.path_to_settings_resolver.fallback().fallback().clone() } /// Reloads all workspaces matched by the [`Url`] @@ -166,7 +171,7 @@ impl WorkspaceSettingsResolver { settings, } in discovered_settings { - settings_resolver.add(&directory, settings); + settings_resolver.add(&directory, Arc::new(settings)); } } } @@ -178,9 +183,9 @@ impl WorkspaceSettingsResolver { /// resolver to actually resolve the `Settings` for this path. We do it this way /// to ensure we can easily add and remove workspaces (including all of their /// hierarchical paths). - fn settings_for_path(&self, path: &Path) -> &Settings { + fn settings_for_path(&self, path: &Path) -> Arc { let settings_resolver = self.path_to_settings_resolver.resolve_or_fallback(path); - settings_resolver.resolve_or_fallback(path) + settings_resolver.resolve_or_fallback(path).clone() } fn url_to_path(url: &Url) -> anyhow::Result> { From cf6715a2a2210b7ddbac1b73ee54069e06a6d0b2 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 10:02:58 -0500 Subject: [PATCH 31/44] Add a test for `AIR_CRATE_NAMES` --- crates/server/src/crates.rs | 10 +++++++++ .../server__crates__tests__crate_names.snap | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 crates/server/src/snapshots/server__crates__tests__crate_names.snap diff --git a/crates/server/src/crates.rs b/crates/server/src/crates.rs index 053fe3c1..c6e4066b 100644 --- a/crates/server/src/crates.rs +++ b/crates/server/src/crates.rs @@ -1,3 +1,13 @@ // Generates `AIR_CRATE_NAMES`, a const array of the crate names in the air workspace, // see `server/src/build.rs` include!(concat!(env!("OUT_DIR"), "/crates.rs")); + +#[cfg(test)] +mod tests { + use crate::crates::AIR_CRATE_NAMES; + + #[test] + fn test_crate_names() { + insta::assert_debug_snapshot!(AIR_CRATE_NAMES); + } +} diff --git a/crates/server/src/snapshots/server__crates__tests__crate_names.snap b/crates/server/src/snapshots/server__crates__tests__crate_names.snap new file mode 100644 index 00000000..77435a14 --- /dev/null +++ b/crates/server/src/snapshots/server__crates__tests__crate_names.snap @@ -0,0 +1,21 @@ +--- +source: crates/server/src/crates.rs +expression: AIR_CRATE_NAMES +--- +[ + "air", + "air_r_formatter", + "air_r_syntax", + "air_formatter_test", + "source_file", + "air_r_parser", + "air_r_factory", + "tests_macros", + "fs", + "server", + "workspace", + "server_test", + "biome_ungrammar", + "xtask_codegen", + "xtask", +] From 13914632629f0368bfb7fe368ccde3b310dea775 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 10:12:15 -0500 Subject: [PATCH 32/44] Adjust `LICENSE` to reflect `server` fork --- LICENSE | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/LICENSE b/LICENSE index b188949d..e1654192 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,33 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +The `server` crate is a fork of `ruff_server`: + +- URL: https://github.com/astral-sh/ruff/tree/main/crates/ruff_server +- Commit: aa429b413f8a7de9eeee14f8e54fdcb3b199b7b7 +- License: MIT + +MIT License + +Copyright (c) 2022 Charles Marsh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 32f8dcf1ebe04b7420701cb35477e12836f80775 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 11:04:38 -0500 Subject: [PATCH 33/44] Internally handle errors when opening/closing/reloading workspace settings Allowing us to show useful toast notifications when something goes wrong --- crates/server/src/error.rs | 52 ----------- crates/server/src/lib.rs | 1 - .../api/notifications/did_change_workspace.rs | 14 +-- crates/server/src/session.rs | 7 +- crates/server/src/session/index.rs | 11 ++- crates/server/src/session/workspaces.rs | 89 +++++++++++-------- crates/workspace/src/toml.rs | 14 +-- 7 files changed, 68 insertions(+), 120 deletions(-) delete mode 100644 crates/server/src/error.rs diff --git a/crates/server/src/error.rs b/crates/server/src/error.rs deleted file mode 100644 index fe19c650..00000000 --- a/crates/server/src/error.rs +++ /dev/null @@ -1,52 +0,0 @@ -/// A tool for collecting multiple anyhow errors into a single [`anyhow::Result`] -/// -/// Only applicable if the intended `Ok()` value at the end is `()`. -#[derive(Debug, Default)] -pub(crate) struct ErrorVec { - errors: Vec, -} - -impl ErrorVec { - pub(crate) fn new() -> Self { - Self::default() - } - - /// Conditionally push to the error vector if the `result` is an `Err` case - pub(crate) fn push_err(&mut self, result: anyhow::Result) { - match result { - Ok(_) => (), - Err(error) => self.push(error), - } - } - - /// Push a new error to the error vector - pub(crate) fn push(&mut self, error: anyhow::Error) { - self.errors.push(error); - } - - /// Convert a error vector into a single [`anyhow::Result`] that knows how to print - /// each of the individual errors - pub(crate) fn into_result(self) -> anyhow::Result<()> { - if self.errors.is_empty() { - Ok(()) - } else { - Err(anyhow::anyhow!(self)) - } - } -} - -impl std::error::Error for ErrorVec {} - -impl std::fmt::Display for ErrorVec { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.errors.len() > 1 { - f.write_str("Multiple errors:\n")?; - } - - for error in &self.errors { - std::fmt::Display::fmt(error, f)?; - } - - Ok(()) - } -} diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 72d6cf5b..24fd399f 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -7,7 +7,6 @@ mod message; mod crates; mod document; -mod error; mod logging; mod proto; mod server; diff --git a/crates/server/src/server/api/notifications/did_change_workspace.rs b/crates/server/src/server/api/notifications/did_change_workspace.rs index 29bd5217..d44b00b9 100644 --- a/crates/server/src/server/api/notifications/did_change_workspace.rs +++ b/crates/server/src/server/api/notifications/did_change_workspace.rs @@ -1,5 +1,3 @@ -use crate::error::ErrorVec; -use crate::server::api::LSPResult; use crate::server::client::{Notifier, Requester}; use crate::server::Result; use crate::session::Session; @@ -19,18 +17,12 @@ impl super::SyncNotificationHandler for DidChangeWorkspace { _requester: &mut Requester, params: types::DidChangeWorkspaceFoldersParams, ) -> Result<()> { - // Collect all `errors` to ensure we don't drop any events if we encounter an error - let mut errors = ErrorVec::new(); - for types::WorkspaceFolder { uri, .. } in params.event.added { - errors.push_err(session.open_workspace_folder(&uri)); + session.open_workspace_folder(&uri); } for types::WorkspaceFolder { uri, .. } in params.event.removed { - errors.push_err(session.close_workspace_folder(&uri)); + session.close_workspace_folder(&uri); } - - errors - .into_result() - .with_failure_code(lsp_server::ErrorCode::InvalidParams) + Ok(()) } } diff --git a/crates/server/src/session.rs b/crates/server/src/session.rs index ddef1766..feca05c1 100644 --- a/crates/server/src/session.rs +++ b/crates/server/src/session.rs @@ -95,14 +95,13 @@ impl Session { } /// Open a workspace folder at the given `url`. - pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { + pub(crate) fn open_workspace_folder(&mut self, url: &Url) { self.index.open_workspace_folder(url) } /// Close a workspace folder at the given `url`. - pub(crate) fn close_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { - self.index.close_workspace_folder(url)?; - Ok(()) + pub(crate) fn close_workspace_folder(&mut self, url: &Url) { + self.index.close_workspace_folder(url); } #[allow(dead_code)] diff --git a/crates/server/src/session/index.rs b/crates/server/src/session/index.rs index cd720849..77b4355c 100644 --- a/crates/server/src/session/index.rs +++ b/crates/server/src/session/index.rs @@ -80,10 +80,14 @@ impl Index { DocumentKey::Text(url) } - pub(super) fn open_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { + pub(super) fn open_workspace_folder(&mut self, url: &Url) { self.settings.open_workspace_folder(url) } + pub(super) fn close_workspace_folder(&mut self, url: &Url) { + self.settings.close_workspace_folder(url) + } + pub(super) fn num_documents(&self) -> usize { self.documents.len() } @@ -92,11 +96,6 @@ impl Index { self.settings.len() } - pub(super) fn close_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { - self.settings.close_workspace_folder(url)?; - Ok(()) - } - pub(super) fn make_document_ref(&self, key: DocumentKey) -> Option { let url = self.url_for_key(&key)?.clone(); diff --git a/crates/server/src/session/workspaces.rs b/crates/server/src/session/workspaces.rs index c57b8d46..010573f3 100644 --- a/crates/server/src/session/workspaces.rs +++ b/crates/server/src/session/workspaces.rs @@ -37,31 +37,45 @@ impl WorkspaceSettingsResolver { }; // Add each workspace folder's settings into the resolver. - // If we fail for any reason (i.e. parse failure of an `air.toml`) then - // we log an error and try to resolve the remaining workspace folders. We don't want - // to propagate an error here because we don't want to prevent the server from - // starting up entirely. - // TODO: This is one place it would be nice to show a toast notification back - // to the user, but we probably need to add support to the Aux thread for that? for workspace_folder in workspace_folders { - if let Err(error) = resolver.open_workspace_folder(&workspace_folder.uri) { - tracing::error!( - "Failed to load workspace settings for '{uri}':\n{error}", - uri = workspace_folder.uri.as_str(), - error = error - ); - } + resolver.open_workspace_folder(&workspace_folder.uri) } resolver } - pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> anyhow::Result<()> { - let path = match Self::url_to_path(url)? { - Some(path) => path, - None => { - tracing::warn!("Ignoring non-file workspace URL: {url}"); - return Ok(()); + /// Open a workspace folder + /// + /// If we fail for any reason (i.e. parse failure of an `air.toml`), we handle the + /// failure internally. This allows us to: + /// - Avoid preventing the server from starting up at all (which would happen if we + /// propagated an error up) + /// - Control the toast notification sent to the user + pub(crate) fn open_workspace_folder(&mut self, url: &Url) { + let failed_to_open_workspace_folder = |url, error| { + show_err_msg!( + "Failed to open workspace folder for '{url}'. Check the logs for more information." + ); + tracing::error!("Failed to open workspace folder for '{url}':\n{error}"); + }; + + let path = match Self::url_to_path(url) { + Ok(Some(path)) => path, + Ok(None) => { + tracing::warn!("Ignoring non-file workspace URL '{url}'"); + return; + } + Err(error) => { + failed_to_open_workspace_folder(url, error); + return; + } + }; + + let discovered_settings = match discover_settings(&[&path]) { + Ok(discovered_settings) => discovered_settings, + Err(error) => { + failed_to_open_workspace_folder(url, error.into()); + return; } }; @@ -73,29 +87,29 @@ impl WorkspaceSettingsResolver { for DiscoveredSettings { directory, settings, - } in discover_settings(&[&path])? + } in discovered_settings { settings_resolver.add(&directory, Arc::new(settings)); } tracing::trace!("Adding workspace settings: {}", path.display()); self.path_to_settings_resolver.add(&path, settings_resolver); - - Ok(()) } - pub(crate) fn close_workspace_folder( - &mut self, - url: &Url, - ) -> anyhow::Result> { - match Self::url_to_path(url)? { - Some(path) => { + pub(crate) fn close_workspace_folder(&mut self, url: &Url) { + match Self::url_to_path(url) { + Ok(Some(path)) => { tracing::trace!("Removing workspace settings: {}", path.display()); - Ok(self.path_to_settings_resolver.remove(&path)) + self.path_to_settings_resolver.remove(&path); } - None => { + Ok(None) => { tracing::warn!("Ignoring non-file workspace URL: {url}"); - Ok(None) + } + Err(error) => { + show_err_msg!( + "Failed to close workspace folder for '{url}'. Check the logs for more information." + ); + tracing::error!("Failed to close workspace folder for '{url}':\n{error}"); } } } @@ -136,7 +150,10 @@ impl WorkspaceSettingsResolver { return; } Err(error) => { - tracing::error!("Failed to reload workspaces associated with {url}:\n{error}"); + show_err_msg!( + "Failed to reload workspaces associated with '{url}'. Check the logs for more information." + ); + tracing::error!("Failed to reload workspaces associated with '{url}':\n{error}"); return; } }; @@ -157,11 +174,11 @@ impl WorkspaceSettingsResolver { let discovered_settings = match discover_settings(&[workspace_path]) { Ok(discovered_settings) => discovered_settings, Err(error) => { - tracing::error!( - "Failed to reload workspace settings for {path}:\n{error}", - path = workspace_path.display(), - error = error + let workspace_path = workspace_path.display(); + show_err_msg!( + "Failed to reload workspace for '{workspace_path}'. Check the logs for more information." ); + tracing::error!("Failed to reload workspace for '{workspace_path}':\n{error}"); continue; } }; diff --git a/crates/workspace/src/toml.rs b/crates/workspace/src/toml.rs index 98ffb238..2cfb5ca9 100644 --- a/crates/workspace/src/toml.rs +++ b/crates/workspace/src/toml.rs @@ -26,19 +26,13 @@ impl std::error::Error for ParseTomlError {} impl Display for ParseTomlError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { + // It's nicer if we don't make these paths relative, so we can quickly + // jump to the TOML file to see what is wrong Self::Read(path, err) => { - write!( - f, - "Failed to read {path}:\n{err}", - path = fs::relativize_path(path), - ) + write!(f, "Failed to read {path}:\n{err}", path = path.display()) } Self::Deserialize(path, err) => { - write!( - f, - "Failed to parse {path}:\n{err}", - path = fs::relativize_path(path), - ) + write!(f, "Failed to parse {path}:\n{err}", path = path.display()) } } } From bbd2a47c73cca54e29a4982605f194d3b415fdd4 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 11:38:36 -0500 Subject: [PATCH 34/44] Remove not needed `clone()` --- crates/server/src/server/api/notifications/did_open.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/server/src/server/api/notifications/did_open.rs b/crates/server/src/server/api/notifications/did_open.rs index ce12b20c..943d0a78 100644 --- a/crates/server/src/server/api/notifications/did_open.rs +++ b/crates/server/src/server/api/notifications/did_open.rs @@ -25,7 +25,7 @@ impl super::SyncNotificationHandler for DidOpen { ) -> Result<()> { let document = TextDocument::new(text, version); - session.open_text_document(uri.clone(), document); + session.open_text_document(uri, document); Ok(()) } From 951794cbd302e8c279a319cc23a15714f4dd4b4b Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 11:38:46 -0500 Subject: [PATCH 35/44] Remove unused clippy allow --- crates/server/src/server.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index 526e2f5b..44d8d3ec 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -152,7 +152,6 @@ impl Server { .join() } - #[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet. fn event_loop( connection: &Connection, resolved_client_capabilities: &ResolvedClientCapabilities, From 2c58036ba3a33e41a09c9b639602f80830a017d2 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 11:45:58 -0500 Subject: [PATCH 36/44] Simplify by letting `apply_changes()` handle the empty case --- crates/server/src/document/text_document.rs | 2 +- crates/server/src/session/index.rs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/crates/server/src/document/text_document.rs b/crates/server/src/document/text_document.rs index 6e1540b0..a7f1371c 100644 --- a/crates/server/src/document/text_document.rs +++ b/crates/server/src/document/text_document.rs @@ -110,7 +110,7 @@ impl TextDocument { self.update_version(new_version); } - pub fn update_version(&mut self, new_version: DocumentVersion) { + fn update_version(&mut self, new_version: DocumentVersion) { let old_version = self.version; self.version = new_version; debug_assert!(self.version >= old_version); diff --git a/crates/server/src/session/index.rs b/crates/server/src/session/index.rs index 77b4355c..9e143f14 100644 --- a/crates/server/src/session/index.rs +++ b/crates/server/src/session/index.rs @@ -66,12 +66,6 @@ impl Index { let Some(document) = controller.as_text_mut() else { anyhow::bail!("Text document URI does not point to a text document"); }; - - if content_changes.is_empty() { - document.update_version(new_version); - return Ok(()); - } - document.apply_changes(content_changes, new_version, encoding); Ok(()) } From a9599828a60739624d2959ca39be07bbc3e08816 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 11:52:09 -0500 Subject: [PATCH 37/44] Add some documentation around the `make_mut()` call --- crates/server/src/session/index.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/server/src/session/index.rs b/crates/server/src/session/index.rs index 9e143f14..d3e25ade 100644 --- a/crates/server/src/session/index.rs +++ b/crates/server/src/session/index.rs @@ -183,6 +183,17 @@ impl DocumentController { } } + /// Request mutable access to the [`TextDocument`] managed by this controller + /// + /// [`Arc::make_mut`] is the key to this: + /// - If `document` is not being referenced by anyone else, this just returns the + /// `document` directly. + /// - If `document` is also referenced by someone else, this clones the `document` + /// that lives inside the `Arc` and updates the `Arc` in place, then provides access + /// to that. This allows any background tasks to continue working with their version + /// of the document, while we are free to modify the "source of truth" document. + /// + /// This effectively implements "copy on modify" semantics for `TextDocument`. pub(crate) fn as_text_mut(&mut self) -> Option<&mut TextDocument> { Some(match self { Self::Text(document) => Arc::make_mut(document), From 8c1ae65b6ddd8329f9cfddbc825215b13e000984 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 12:00:50 -0500 Subject: [PATCH 38/44] We use >=1.83 MSRV, so can use `PanicHookInfo` directly --- crates/server/src/server.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index 44d8d3ec..b5d2dd3e 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -4,9 +4,7 @@ use lsp_server as lsp; use lsp_types as types; use lsp_types::InitializeParams; use std::num::NonZeroUsize; -// The new PanicInfoHook name requires MSRV >= 1.82 -#[allow(deprecated)] -use std::panic::PanicInfo; +use std::panic::PanicHookInfo; use types::DidChangeWatchedFilesRegistrationOptions; use types::FileSystemWatcher; use types::OneOf; @@ -97,13 +95,12 @@ impl Server { } pub fn run(self) -> anyhow::Result<()> { - // The new PanicInfoHook name requires MSRV >= 1.82 - #[allow(deprecated)] - type PanicHook = Box) + 'static + Sync + Send>; + // Unregister any previously registered panic hook. + // The hook will be restored when this function exits. + type PanicHook = Box) + 'static + Sync + Send>; struct RestorePanicHook { hook: Option, } - impl Drop for RestorePanicHook { fn drop(&mut self) { if let Some(hook) = self.hook.take() { @@ -111,9 +108,6 @@ impl Server { } } } - - // unregister any previously registered panic hook - // The hook will be restored when this function exits. let _ = RestorePanicHook { hook: Some(std::panic::take_hook()), }; From b11f2523cc0abacc13b2fa99b65dfcb4c52e2c91 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 12:15:21 -0500 Subject: [PATCH 39/44] Sort packages in `AIR_CRATE_NAMES` --- crates/server/build.rs | 11 +++++++++-- .../server__crates__tests__crate_names.snap | 16 ++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/server/build.rs b/crates/server/build.rs index 17cde13a..025d58a7 100644 --- a/crates/server/build.rs +++ b/crates/server/build.rs @@ -18,10 +18,17 @@ fn write_workspace_crate_names() { cmd.no_deps(); let metadata = cmd.exec().unwrap(); - let packages: Vec = metadata + let mut packages: Vec = metadata .workspace_packages() - .iter() + .into_iter() .map(|package| package.name.clone()) + .collect(); + + // Sort for stability across `cargo metadata` versions + packages.sort(); + + let packages: Vec = packages + .into_iter() .map(|package| String::from("\"") + package.as_str() + "\",") .collect(); diff --git a/crates/server/src/snapshots/server__crates__tests__crate_names.snap b/crates/server/src/snapshots/server__crates__tests__crate_names.snap index 77435a14..42180ad5 100644 --- a/crates/server/src/snapshots/server__crates__tests__crate_names.snap +++ b/crates/server/src/snapshots/server__crates__tests__crate_names.snap @@ -4,18 +4,18 @@ expression: AIR_CRATE_NAMES --- [ "air", - "air_r_formatter", - "air_r_syntax", "air_formatter_test", - "source_file", - "air_r_parser", "air_r_factory", - "tests_macros", + "air_r_formatter", + "air_r_parser", + "air_r_syntax", + "biome_ungrammar", "fs", "server", - "workspace", "server_test", - "biome_ungrammar", - "xtask_codegen", + "source_file", + "tests_macros", + "workspace", "xtask", + "xtask_codegen", ] From 931f0518bdcc5e23c683ae6d8fa93da16247bb6b Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 12:34:07 -0500 Subject: [PATCH 40/44] No longer need `--test-threads 1` Since our client tests are now synchronized through a mutex --- .github/workflows/test-linux.yml | 3 +-- .github/workflows/test-mac.yml | 3 +-- .github/workflows/test-windows.yml | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index 2e084756..14e12490 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -54,5 +54,4 @@ jobs: env: AIR_LOG_LEVEL: trace # `--nocapture` to see our own `tracing` logs - # `--test-threads 1` to ensure `tracing` logs aren't interleaved - run: cargo test -- --nocapture --test-threads 1 + run: cargo test -- --nocapture diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml index dbe44a94..b6fe1ca2 100644 --- a/.github/workflows/test-mac.yml +++ b/.github/workflows/test-mac.yml @@ -32,5 +32,4 @@ jobs: env: AIR_LOG_LEVEL: trace # `--nocapture` to see our own `tracing` logs - # `--test-threads 1` to ensure `tracing` logs aren't interleaved - run: cargo test -- --nocapture --test-threads 1 + run: cargo test -- --nocapture diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index fa95a5b2..ede3e23f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -32,5 +32,4 @@ jobs: env: AIR_LOG_LEVEL: trace # `--nocapture` to see our own `tracing` logs - # `--test-threads 1` to ensure `tracing` logs aren't interleaved - run: cargo test -- --nocapture --test-threads 1 + run: cargo test -- --nocapture From 50af6f9992658759f58774ade675f336bd4f0607 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Fri, 3 Jan 2025 13:04:59 -0500 Subject: [PATCH 41/44] Trim dependencies in the workspace toml --- Cargo.toml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d3b374a9..67391c8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,47 +45,31 @@ biome_rowan = { git = "https://github.com/biomejs/biome", rev = "2648fa4201be4af biome_string_case = { git = "https://github.com/biomejs/biome", rev = "2648fa4201be4afd26f44eca1a4e77aac0a67272" } biome_text_size = { git = "https://github.com/biomejs/biome", rev = "2648fa4201be4afd26f44eca1a4e77aac0a67272" } biome_unicode_table = { git = "https://github.com/biomejs/biome", rev = "2648fa4201be4afd26f44eca1a4e77aac0a67272" } -bytes = "1.8.0" cargo_metadata = "0.19.1" clap = { version = "4.5.20", features = ["derive"] } crossbeam = "0.8.4" dissimilar = "1.0.9" -futures = "0.3.31" -futures-util = "0.3.31" -httparse = "1.9.5" ignore = "0.4.23" insta = "1.40.0" itertools = "0.13.0" jod-thread = "0.1.2" libc = "0.2.153" -line-index = "0.1.2" lsp-server = "0.7.8" lsp-types = "0.95.1" memchr = "2.7.4" path-absolutize = "3.1.1" proc-macro2 = "1.0.86" -regex = "1.11.1" rustc-hash = "2.1.0" -schemars = "0.8.21" serde = "1.0.215" serde_json = "1.0.132" -serde_test = "1.0.177" -static_assertions = "1.1.0" -struct-field-names-as-array = "0.3.0" -strum = "0.26" tempfile = "3.9.0" time = "0.3.37" thiserror = "2.0.5" -tokio = { version = "1.41.1" } -tokio-util = "0.7.12" toml = "0.8.19" -# For https://github.com/ebkalderon/tower-lsp/pull/428 -tower-lsp = { git = "https://github.com/lionel-/tower-lsp", branch = "bugfix/patches" } tracing = { version = "0.1.40", default-features = false, features = ["std"] } tracing-subscriber = "0.3.19" tree-sitter = "0.23.0" tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", rev = "a0d3e3307489c3ca54da8c7b5b4e0c5f5fd6953a" } -triomphe = "0.1.14" url = "2.5.3" uuid = { version = "1.11.0", features = ["v4"] } From 1f31c686b790093a072dbfafc114ec46fea0cee3 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Mon, 2 Jun 2025 13:51:32 -0400 Subject: [PATCH 42/44] tmp --- Cargo.lock | 1 - crates/server/src/proto/text_size.rs | 181 ++++--- crates/source_file/Cargo.toml | 9 - crates/source_file/src/lib.rs | 8 +- crates/source_file/src/line_index.rs | 556 +++++++++++++--------- crates/source_file/src/one_indexed.rs | 100 ---- crates/source_file/src/source_file.rs | 189 +++++--- crates/source_file/src/source_location.rs | 94 +++- 8 files changed, 633 insertions(+), 505 deletions(-) delete mode 100644 crates/source_file/src/one_indexed.rs diff --git a/Cargo.lock b/Cargo.lock index 374a2867..dbda3ff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1696,7 +1696,6 @@ version = "0.0.0" dependencies = [ "biome_text_size", "memchr", - "serde", ] [[package]] diff --git a/crates/server/src/proto/text_size.rs b/crates/server/src/proto/text_size.rs index 368172f2..068a42c1 100644 --- a/crates/server/src/proto/text_size.rs +++ b/crates/server/src/proto/text_size.rs @@ -1,8 +1,9 @@ use crate::document::PositionEncoding; -use biome_rowan::TextRange; use biome_text_size::TextSize; use lsp_types as types; -use source_file::OneIndexed; +use source_file::LineNumber; +use source_file::LineOffset; +use source_file::LineOffsetEncoding; use source_file::{SourceFile, SourceLocation}; // We don't own this type so we need a helper trait @@ -18,7 +19,11 @@ pub(crate) trait TextSizeExt { impl TextSizeExt for TextSize { fn into_proto(self, source: &SourceFile, encoding: PositionEncoding) -> types::Position { - source_location_to_position(&offset_to_source_location(self, source, encoding)) + let source_location = source.source_location(self, remap_encoding(encoding)); + types::Position { + line: source_location.line_number().into(), + character: source_location.line_offset().raw(), + } } fn from_proto( @@ -26,101 +31,87 @@ impl TextSizeExt for TextSize { source: &SourceFile, encoding: PositionEncoding, ) -> Self { - let line = source.line_range(OneIndexed::from_zero_indexed(u32_index_to_usize( - position.line, - ))); - - let column_offset = match encoding { - PositionEncoding::UTF8 => TextSize::from(position.character), - - PositionEncoding::UTF16 => { - // Fast path for ASCII only documents - if source.is_ascii() { - TextSize::from(position.character) - } else { - // UTF-16 encodes characters either as one or two 16 bit words. - // The `position` is the 16-bit word offset from the start of the line (and not the character offset) - utf8_column_offset(position.character, &source.contents()[line]) - } - } - - PositionEncoding::UTF32 => { - // UTF-32 uses 4 bytes for each character. Meaning, the position is a character offset. - return source.offset( - OneIndexed::from_zero_indexed(u32_index_to_usize(position.line)), - OneIndexed::from_zero_indexed(u32_index_to_usize(position.character)), - ); - } - }; - - line.start() + column_offset.clamp(TextSize::from(0), line.end()) - } -} - -fn u32_index_to_usize(index: u32) -> usize { - usize::try_from(index).expect("u32 fits in usize") -} - -/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number. -fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize { - let mut utf8_code_unit_offset = TextSize::from(0); - - let mut i = 0u32; - - for c in line.chars() { - if i >= utf16_code_unit_offset { - break; - } - - // Count characters encoded as two 16 bit words as 2 characters. - { - utf8_code_unit_offset += - TextSize::from(u32::try_from(c.len_utf8()).expect("utf8 len always <=4")); - i += u32::try_from(c.len_utf16()).expect("utf16 len always <=2"); - } + let source_location = SourceLocation::new( + LineNumber::from(position.line), + LineOffset::new(position.character, remap_encoding(encoding)), + ); + source.offset(source_location) } - - utf8_code_unit_offset } -fn offset_to_source_location( - offset: TextSize, - source: &SourceFile, - encoding: PositionEncoding, -) -> SourceLocation { +/// Here's how to think about these conversions: +/// +/// [lsp_types::Position] contains a location encoded as `row` and `character`, +/// where: +/// - `row` represents the 0-indexed line number +/// - `character` represents the 0-indexed column offset, with precise meaning decided +/// by [lsp_types::PositionEncodingKind] +/// +/// `character` is interpreted as: +/// - With [lsp_types::PositionEncodingKind::UTF8], the number of UTF-8 code units. +/// - With [lsp_types::PositionEncodingKind::UTF16], the number of UTF-16 code units. +/// - With [lsp_types::PositionEncodingKind::UTF32], the number of UTF-32 code units. +/// +/// Now, for some definitions: +/// +/// - Code unit: The minimal bit combination that can represent a single character. +/// - UTF-8: +/// - 1 code unit = 1 byte = 8 bits +/// - UTF-16: +/// - 1 code unit = 2 bytes = 16 bits +/// - UTF-32: +/// - 1 code unit = 4 bytes = 32 bits +/// +/// - Character: A combination of code units that construct a single UTF element. +/// - UTF-8: +/// - 1 character = 1,2,3,4 code units = 1,2,3,4 bytes = 8,16,24,32 bits +/// - UTF-16: +/// - 1 character = 1,2 code units = 2,4 bytes = 16,32 bits +/// - UTF-16: +/// - 1 character = 1 code units = 4 bytes = 32 bits +/// +/// - Unicode Scalar Value: Any Unicode Code Point other than a Surrogate Code Point ( +/// which are only used by UTF-16). Technically this means any value in the range of +/// [0 to 0x10FFFF] excluding the slice of [0xD800 to 0xDFFF]. +/// +/// - Unicode Code Point: Any value in the Unicode code space of [0 to 0x10FFFF]. This +/// means that something representing an arbitrary code point must be 4 bytes, implying +/// that something representing a Unicode Scalar Value must also be 4 bytes. +/// +/// In Rust, [String] and [str] are in UTF-8. Figuring out how to go from the, say, +/// 8th column `Position.character` of a line to the byte offset on that line requires +/// knowing both the UTF-8 content of that line and the `PositionEncodingKind` that +/// `Position.character` is encoded in. +/// +/// Note that `chars()` returns an iterator over the individual `char` contained within a +/// string. And each `char` is a Unicode Scalar Value. This means that each `char` is +/// internally represented as a `u32` of exactly 4 bytes. It also means that you can +/// think of iterating over `chars()` as equivalent to iterating over UTF-32 Characters +/// or UTF-32 Code Points. +/// +/// Also relevant is that [char::len_utf16] returns the number of UTF-16 code units that +/// would be required to represent the `char`, and [char::len_utf8] returns the number +/// of UTF-8 code units (and therefore bytes) that would be required to represent the +/// `char`. +/// +/// # Converting `character` UTF-8/16/32 code points -> UTF-8 String byte offset +/// +/// An arbitrary algorithm to find the number of UTF-8 bytes required to represent `character` column offset would be: +/// - Iterate over `chars()` +/// - Figure out how many `char`s are required TODO?? +/// +/// - With [lsp_types::PositionEncodingKind::UTF8]: +/// - `character` is the number of UTF-8 code units +/// - 1 UTF-8 code unit is just 1 UTF-8 byte, so just return `character` +/// - With [lsp_types::PositionEncodingKind::UTF16]: +/// - `character` is the number of UTF-16 code units +/// - 1 UTF-16 code unit must +/// - With [lsp_types::PositionEncodingKind::UTF32]: +/// +fn remap_encoding(encoding: PositionEncoding) -> LineOffsetEncoding { match encoding { - PositionEncoding::UTF8 => { - let row = source.line_index(offset); - let column = offset - source.line_start(row); - - SourceLocation { - column: OneIndexed::from_zero_indexed(column.into()), - row, - } - } - PositionEncoding::UTF16 => { - let row = source.line_index(offset); - - let column = if source.is_ascii() { - (offset - source.line_start(row)).into() - } else { - let up_to_line = &source.contents()[TextRange::new(source.line_start(row), offset)]; - up_to_line.encode_utf16().count() - }; - - SourceLocation { - column: OneIndexed::from_zero_indexed(column), - row, - } - } - PositionEncoding::UTF32 => source.source_location(offset), - } -} - -fn source_location_to_position(location: &SourceLocation) -> types::Position { - types::Position { - line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"), - character: u32::try_from(location.column.to_zero_indexed()) - .expect("character usize fits in u32"), + PositionEncoding::UTF16 => LineOffsetEncoding::UTF16, + PositionEncoding::UTF32 => LineOffsetEncoding::UTF32, + PositionEncoding::UTF8 => LineOffsetEncoding::UTF8, } } diff --git a/crates/source_file/Cargo.toml b/crates/source_file/Cargo.toml index 87529da5..13c1d320 100644 --- a/crates/source_file/Cargo.toml +++ b/crates/source_file/Cargo.toml @@ -9,18 +9,9 @@ homepage = { workspace = true } repository = { workspace = true } license = { workspace = true } -[lib] - [dependencies] biome_text_size = { workspace = true } - memchr = { workspace = true } -serde = { workspace = true, optional = true } - -[dev-dependencies] - -[features] -serde = ["dep:serde", "biome_text_size/serde"] [lints] workspace = true diff --git a/crates/source_file/src/lib.rs b/crates/source_file/src/lib.rs index 897a77e0..f768dd8b 100644 --- a/crates/source_file/src/lib.rs +++ b/crates/source_file/src/lib.rs @@ -1,13 +1,11 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - pub use crate::newlines::{find_newline, infer_line_ending, normalize_newlines, LineEnding}; -pub use crate::one_indexed::OneIndexed; pub use crate::source_file::SourceFile; +pub use crate::source_location::LineNumber; +pub use crate::source_location::LineOffset; +pub use crate::source_location::LineOffsetEncoding; pub use crate::source_location::SourceLocation; mod line_index; mod newlines; -mod one_indexed; mod source_file; mod source_location; diff --git a/crates/source_file/src/line_index.rs b/crates/source_file/src/line_index.rs index 78f1969a..5e433ef6 100644 --- a/crates/source_file/src/line_index.rs +++ b/crates/source_file/src/line_index.rs @@ -1,13 +1,12 @@ use std::fmt; use std::fmt::{Debug, Formatter}; -use std::ops::Deref; use std::sync::Arc; use biome_text_size::{TextLen, TextRange, TextSize}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use crate::OneIndexed; +use crate::source_location::LineNumber; +use crate::source_location::LineOffset; +use crate::source_location::LineOffsetEncoding; use crate::SourceLocation; /// Index for fast [byte offset](TextSize) to [`SourceLocation`] conversions. @@ -28,7 +27,7 @@ impl LineIndex { /// Builds the [`LineIndex`] from the source text of a file. pub(crate) fn from_source_text(text: &str) -> Self { let mut line_starts: Vec = Vec::with_capacity(text.len() / 88); - line_starts.push(TextSize::default()); + line_starts.push(TextSize::from(0)); let bytes = text.as_bytes(); let mut utf8 = false; @@ -65,36 +64,43 @@ impl LineIndex { self.inner.kind } - /// Returns the row and column index for an offset. - pub(crate) fn source_location(&self, offset: TextSize, content: &str) -> SourceLocation { - match self.line_starts().binary_search(&offset) { - // Offset is at the start of a line - Ok(row) => SourceLocation { - row: OneIndexed::from_zero_indexed(row), - column: OneIndexed::from_zero_indexed(0), - }, - Err(next_row) => { - // SAFETY: Safe because the index always contains an entry for the offset 0 - let row = next_row - 1; - let mut line_start = self.line_starts()[row]; - - let column = if self.kind().is_ascii() { - usize::from(offset) - usize::from(line_start) - } else { - // Don't count the BOM character as a column. - if line_start == TextSize::from(0) && content.starts_with('\u{feff}') { - line_start = '\u{feff}'.text_len(); - } - - content[TextRange::new(line_start, offset)].chars().count() - }; - - SourceLocation { - row: OneIndexed::from_zero_indexed(row), - column: OneIndexed::from_zero_indexed(column), + /// Returns the [SourceLocation] for an offset. + pub(crate) fn source_location( + &self, + offset: TextSize, + content: &str, + encoding: LineOffsetEncoding, + ) -> SourceLocation { + let line_number = self.line_number(offset); + let line_start = self.line_start(line_number, content); + + let line_offset = if self.is_ascii() { + LineOffset::new((offset - line_start).into(), encoding) + } else { + match encoding { + LineOffsetEncoding::UTF8 => LineOffset::new((offset - line_start).into(), encoding), + LineOffsetEncoding::UTF16 => { + let line_contents_up_to_offset = &content[TextRange::new(line_start, offset)]; + let offset = line_contents_up_to_offset + .encode_utf16() + .count() + .try_into() + .expect("A single line's offset should fit in u32"); + LineOffset::new(offset, encoding) + } + LineOffsetEncoding::UTF32 => { + let line_contents_up_to_offset = &content[TextRange::new(line_start, offset)]; + let offset = line_contents_up_to_offset + .chars() + .count() + .try_into() + .expect("A single line's offset should fit in u32"); + LineOffset::new(offset, encoding) } } - } + }; + + SourceLocation::new(line_number, line_offset) } /// Return the number of lines in the source code. @@ -102,110 +108,107 @@ impl LineIndex { self.line_starts().len() } - /// Returns `true` if the text only consists of ASCII characters + /// Returns `true` if the text only consists of ASCII characters. pub(crate) fn is_ascii(&self) -> bool { self.kind().is_ascii() } - /// Returns the row number for a given offset. - /// - /// ## Panics - /// - /// If the offset is out of bounds. - pub(crate) fn line_index(&self, offset: TextSize) -> OneIndexed { - match self.line_starts().binary_search(&offset) { - // Offset is at the start of a line - Ok(row) => OneIndexed::from_zero_indexed(row), - Err(row) => { + /// Returns the line number for a given offset. + pub(crate) fn line_number(&self, offset: TextSize) -> LineNumber { + let line = match self.line_starts().binary_search(&offset) { + // `offset` is at the start of a line + Ok(row) => row, + Err(next_row) => { // SAFETY: Safe because the index always contains an entry for the offset 0 - OneIndexed::from_zero_indexed(row - 1) + next_row - 1 } - } - } - - /// Returns the [byte offset](TextSize) for the `line` with the given index. - pub(crate) fn line_start(&self, line: OneIndexed, contents: &str) -> TextSize { - let row_index = line.to_zero_indexed(); - let starts = self.line_starts(); + }; - // If start-of-line position after last line - if row_index == starts.len() { - contents.text_len() - } else { - starts[row_index] - } + LineNumber::try_from(line).expect("Number of line starts should fit in a `LineNumber`") } - /// Returns the [byte offset](TextSize) of the `line`'s end. - /// The offset is the end of the line, up to and including the newline character ending the line (if any). - pub(crate) fn line_end(&self, line: OneIndexed, contents: &str) -> TextSize { - let row_index = line.to_zero_indexed(); + /// Returns the [byte offset](TextSize) for the `line`'s start. + pub(crate) fn line_start(&self, line_number: LineNumber, contents: &str) -> TextSize { + let line_number = usize::from(line_number); let starts = self.line_starts(); // If start-of-line position after last line - if row_index.saturating_add(1) >= starts.len() { + if line_number >= starts.len() { contents.text_len() } else { - starts[row_index + 1] + starts[line_number] } } /// Returns the [byte offset](TextSize) of the `line`'s end. - /// The offset is the end of the line, excluding the newline character ending the line (if any). - pub(crate) fn line_end_exclusive(&self, line: OneIndexed, contents: &str) -> TextSize { - let row_index = line.to_zero_indexed(); + /// The offset is the end of the line, up to and including the newline character ending the line (if any). + pub(crate) fn line_end(&self, line_number: LineNumber, contents: &str) -> TextSize { + let line_number = usize::from(line_number); let starts = self.line_starts(); // If start-of-line position after last line - if row_index.saturating_add(1) >= starts.len() { + if line_number.saturating_add(1) >= starts.len() { contents.text_len() } else { - starts[row_index + 1] - TextSize::from(1) + starts[line_number + 1] } } - /// Returns the [`TextRange`] of the `line` with the given index. + /// Returns the [`TextRange`] of the `line`. /// The start points to the first character's [byte offset](TextSize), the end up to, and including /// the newline character ending the line (if any). - pub(crate) fn line_range(&self, line: OneIndexed, contents: &str) -> TextRange { - let starts = self.line_starts(); + pub(crate) fn line_range(&self, line_number: LineNumber, contents: &str) -> TextRange { + TextRange::new( + self.line_start(line_number, contents), + self.line_end(line_number, contents), + ) + } + + /// Returns the [byte offset](TextSize) at this [SourceLocation]. + pub(crate) fn offset(&self, source_location: SourceLocation, contents: &str) -> TextSize { + let line_number = source_location.line_number(); + let line_offset = source_location.line_offset(); - if starts.len() == line.to_zero_indexed() { - TextRange::empty(contents.text_len()) + let line_range = self.line_range(line_number, contents); + + let offset = if self.is_ascii() { + TextSize::from(line_offset.raw()) } else { - TextRange::new( - self.line_start(line, contents), - self.line_start(line.saturating_add(1), contents), - ) - } - } + match line_offset.encoding() { + LineOffsetEncoding::UTF8 => TextSize::from(line_offset.raw()), + LineOffsetEncoding::UTF16 => { + let n_code_units = line_offset.raw(); + let line_contents = &contents[line_range]; + + let mut i = 0; + let mut offset = 0; + + for c in line_contents.chars() { + if i >= n_code_units { + break; + } + i += c.len_utf16() as u32; + offset += c.len_utf8() as u32; + } - /// Returns the [byte offset](TextSize) at `line` and `column`. - pub(crate) fn offset(&self, line: OneIndexed, column: OneIndexed, contents: &str) -> TextSize { - // If start-of-line position after last line - if line.to_zero_indexed() > self.line_starts().len() { - return contents.text_len(); - } + TextSize::from(offset) + } + LineOffsetEncoding::UTF32 => { + let n_code_units = line_offset.raw(); + let line_contents = &contents[line_range]; - let line_range = self.line_range(line, contents); + let mut offset: u32 = 0; - match self.kind() { - IndexKind::Ascii => { - line_range.start() - + TextSize::try_from(column.to_zero_indexed()) - .unwrap_or(line_range.len()) - .clamp(TextSize::from(0), line_range.len()) - } - IndexKind::Utf8 => { - let rest = &contents[line_range]; - let column_offset: TextSize = rest - .chars() - .take(column.to_zero_indexed()) - .map(biome_text_size::TextLen::text_len) - .sum(); - line_range.start() + column_offset + for c in line_contents.chars().take(n_code_units as usize) { + offset += c.len_utf8() as u32; + } + + TextSize::from(offset) + } } - } + }; + + line_range.start() + offset.clamp(TextSize::from(0), line_range.len()) } /// Returns the [byte offsets](TextSize) for every line @@ -214,14 +217,6 @@ impl LineIndex { } } -impl Deref for LineIndex { - type Target = [TextSize]; - - fn deref(&self) -> &Self::Target { - self.line_starts() - } -} - impl Debug for LineIndex { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_list().entries(self.line_starts()).finish() @@ -248,7 +243,10 @@ mod tests { use biome_text_size::TextSize; use crate::line_index::LineIndex; - use crate::{OneIndexed, SourceLocation}; + use crate::source_location::LineNumber; + use crate::source_location::LineOffset; + use crate::source_location::LineOffsetEncoding; + use crate::SourceLocation; #[test] fn ascii_index() { @@ -279,32 +277,32 @@ mod tests { let index = LineIndex::from_source_text(contents); // First row. - let loc = index.source_location(TextSize::from(2), contents); + let loc = index.source_location(TextSize::from(2), contents, LineOffsetEncoding::UTF8); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), - column: OneIndexed::from_zero_indexed(2) - } + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(2, LineOffsetEncoding::UTF8) + ) ); // Second row. - let loc = index.source_location(TextSize::from(6), contents); + let loc = index.source_location(TextSize::from(6), contents, LineOffsetEncoding::UTF8); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(0) - } + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) ); - let loc = index.source_location(TextSize::from(11), contents); + let loc = index.source_location(TextSize::from(11), contents, LineOffsetEncoding::UTF8); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(5) - } + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(5, LineOffsetEncoding::UTF8) + ) ); } @@ -315,25 +313,25 @@ mod tests { assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]); assert_eq!( - index.source_location(TextSize::from(4), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), - column: OneIndexed::from_zero_indexed(4) - } + index.source_location(TextSize::from(4), contents, LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(4, LineOffsetEncoding::UTF8) + ) ); assert_eq!( - index.source_location(TextSize::from(6), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(0) - } + index.source_location(TextSize::from(6), contents, LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) ); assert_eq!( - index.source_location(TextSize::from(7), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(1) - } + index.source_location(TextSize::from(7), contents, LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF8) + ) ); } @@ -344,25 +342,25 @@ mod tests { assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(7)]); assert_eq!( - index.source_location(TextSize::from(4), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), - column: OneIndexed::from_zero_indexed(4) - } + index.source_location(TextSize::from(4), contents, LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(4, LineOffsetEncoding::UTF8) + ) ); assert_eq!( - index.source_location(TextSize::from(7), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(0) - } + index.source_location(TextSize::from(7), contents, LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) ); assert_eq!( - index.source_location(TextSize::from(8), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(1) - } + index.source_location(TextSize::from(8), contents, LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF8) + ) ); } @@ -409,27 +407,73 @@ mod tests { &[TextSize::from(0), TextSize::from(11)] ); - // Second ' + // Second ', UTF8 assert_eq!( - index.source_location(TextSize::from(9), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), - column: OneIndexed::from_zero_indexed(6) - } + index.source_location(TextSize::from(9), contents, LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(9, LineOffsetEncoding::UTF8) + ) ); assert_eq!( - index.source_location(TextSize::from(11), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(0) - } + index.source_location(TextSize::from(11), contents, LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) ); assert_eq!( - index.source_location(TextSize::from(12), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(1) - } + index.source_location(TextSize::from(12), contents, LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF8) + ) + ); + + // Second ', UTF16 + assert_eq!( + index.source_location(TextSize::from(9), contents, LineOffsetEncoding::UTF16), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(7, LineOffsetEncoding::UTF16) + ) + ); + assert_eq!( + index.source_location(TextSize::from(11), contents, LineOffsetEncoding::UTF16), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF16) + ) + ); + assert_eq!( + index.source_location(TextSize::from(12), contents, LineOffsetEncoding::UTF16), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF16) + ) + ); + + // Second ', UTF32 + assert_eq!( + index.source_location(TextSize::from(9), contents, LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(6, LineOffsetEncoding::UTF32) + ) + ); + assert_eq!( + index.source_location(TextSize::from(11), contents, LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF32) + ) + ); + assert_eq!( + index.source_location(TextSize::from(12), contents, LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF32) + ) ); } @@ -445,25 +489,25 @@ mod tests { // Second ' assert_eq!( - index.source_location(TextSize::from(9), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(0), - column: OneIndexed::from_zero_indexed(6) - } + index.source_location(TextSize::from(9), contents, LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(6, LineOffsetEncoding::UTF32) + ) ); assert_eq!( - index.source_location(TextSize::from(12), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(0) - } + index.source_location(TextSize::from(12), contents, LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF32) + ) ); assert_eq!( - index.source_location(TextSize::from(13), contents), - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(1) - } + index.source_location(TextSize::from(13), contents, LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF32) + ) ); } @@ -476,52 +520,134 @@ mod tests { &[TextSize::from(0), TextSize::from(10)] ); - // First row. - let loc = index.source_location(TextSize::from(0), contents); + // First row, start + let loc = index.source_location(TextSize::from(0), contents, LineOffsetEncoding::UTF8); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), - column: OneIndexed::from_zero_indexed(0) - } + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) + ); + let loc = index.source_location(TextSize::from(0), contents, LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(0, LineOffsetEncoding::UTF16) + ) + ); + let loc = index.source_location(TextSize::from(0), contents, LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(0, LineOffsetEncoding::UTF32) + ) ); - let loc = index.source_location(TextSize::from(5), contents); + // First row, right before + let loc = index.source_location(TextSize::from(5), contents, LineOffsetEncoding::UTF8); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), - column: OneIndexed::from_zero_indexed(5) - } + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(5, LineOffsetEncoding::UTF8) + ) + ); + let loc = index.source_location(TextSize::from(5), contents, LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(5, LineOffsetEncoding::UTF16) + ) + ); + let loc = index.source_location(TextSize::from(5), contents, LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(5, LineOffsetEncoding::UTF32) + ) ); - let loc = index.source_location(TextSize::from(8), contents); + // First row, right after + let loc = index.source_location(TextSize::from(8), contents, LineOffsetEncoding::UTF8); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(0), - column: OneIndexed::from_zero_indexed(6) - } + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(8, LineOffsetEncoding::UTF8) + ) + ); + let loc = index.source_location(TextSize::from(8), contents, LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(6, LineOffsetEncoding::UTF16) + ) + ); + let loc = index.source_location(TextSize::from(8), contents, LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(6, LineOffsetEncoding::UTF32) + ) ); - // Second row. - let loc = index.source_location(TextSize::from(10), contents); + // Second row, start + let loc = index.source_location(TextSize::from(10), contents, LineOffsetEncoding::UTF8); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(0) - } + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) + ); + let loc = index.source_location(TextSize::from(10), contents, LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF16) + ) + ); + let loc = index.source_location(TextSize::from(10), contents, LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF32) + ) ); // One-past-the-end. - let loc = index.source_location(TextSize::from(15), contents); + let loc = index.source_location(TextSize::from(15), contents, LineOffsetEncoding::UTF8); assert_eq!( loc, - SourceLocation { - row: OneIndexed::from_zero_indexed(1), - column: OneIndexed::from_zero_indexed(5) - } + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(5, LineOffsetEncoding::UTF8) + ) + ); + let loc = index.source_location(TextSize::from(15), contents, LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(5, LineOffsetEncoding::UTF16) + ) + ); + let loc = index.source_location(TextSize::from(15), contents, LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(5, LineOffsetEncoding::UTF32) + ) ); } } diff --git a/crates/source_file/src/one_indexed.rs b/crates/source_file/src/one_indexed.rs deleted file mode 100644 index d56de9b4..00000000 --- a/crates/source_file/src/one_indexed.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::fmt; -use std::fmt::Formatter; -use std::num::NonZeroUsize; -use std::num::ParseIntError; -use std::str::FromStr; - -/// Type-safe wrapper for a value whose logical range starts at `1`, for -/// instance the line or column numbers in a file -/// -/// Internally this is represented as a [`NonZeroUsize`], this enables some -/// memory optimizations -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct OneIndexed(NonZeroUsize); - -impl OneIndexed { - /// The largest value that can be represented by this integer type - pub const MAX: Self = unwrap(Self::new(usize::MAX)); - // SAFETY: These constants are being initialized with non-zero values - /// The smallest value that can be represented by this integer type. - pub const MIN: Self = unwrap(Self::new(1)); - pub const ONE: NonZeroUsize = unwrap(NonZeroUsize::new(1)); - - /// Creates a non-zero if the given value is not zero. - pub const fn new(value: usize) -> Option { - match NonZeroUsize::new(value) { - Some(value) => Some(Self(value)), - None => None, - } - } - - /// Construct a new [`OneIndexed`] from a zero-indexed value - pub const fn from_zero_indexed(value: usize) -> Self { - Self(Self::ONE.saturating_add(value)) - } - - /// Returns the value as a primitive type. - pub const fn get(self) -> usize { - self.0.get() - } - - /// Return the zero-indexed primitive value for this [`OneIndexed`] - pub const fn to_zero_indexed(self) -> usize { - self.0.get() - 1 - } - - /// Saturating integer addition. Computes `self + rhs`, saturating at - /// the numeric bounds instead of overflowing. - #[must_use] - pub const fn saturating_add(self, rhs: usize) -> Self { - match NonZeroUsize::new(self.0.get().saturating_add(rhs)) { - Some(value) => Self(value), - None => Self::MAX, - } - } - - /// Saturating integer subtraction. Computes `self - rhs`, saturating - /// at the numeric bounds instead of overflowing. - #[must_use] - pub const fn saturating_sub(self, rhs: usize) -> Self { - match NonZeroUsize::new(self.0.get().saturating_sub(rhs)) { - Some(value) => Self(value), - None => Self::MIN, - } - } - - /// Checked addition. Returns `None` if overflow occurred. - #[must_use] - pub fn checked_add(self, rhs: Self) -> Option { - self.0.checked_add(rhs.0.get()).map(Self) - } - - /// Checked subtraction. Returns `None` if overflow occurred. - #[must_use] - pub fn checked_sub(self, rhs: Self) -> Option { - self.0.get().checked_sub(rhs.get()).and_then(Self::new) - } -} - -impl fmt::Display for OneIndexed { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - std::fmt::Debug::fmt(&self.0.get(), f) - } -} - -/// A const `Option::unwrap` without nightly features: -/// [Tracking issue](https://github.com/rust-lang/rust/issues/67441) -const fn unwrap(option: Option) -> T { - match option { - Some(value) => value, - None => panic!("unwrapping None"), - } -} - -impl FromStr for OneIndexed { - type Err = ParseIntError; - fn from_str(s: &str) -> Result { - Ok(OneIndexed(NonZeroUsize::from_str(s)?)) - } -} diff --git a/crates/source_file/src/source_file.rs b/crates/source_file/src/source_file.rs index 473a8e2e..2803a332 100644 --- a/crates/source_file/src/source_file.rs +++ b/crates/source_file/src/source_file.rs @@ -2,7 +2,8 @@ use biome_text_size::TextRange; use biome_text_size::TextSize; use crate::line_index::LineIndex; -use crate::OneIndexed; +use crate::source_location::LineNumber; +use crate::source_location::LineOffsetEncoding; use crate::SourceLocation; /// Manager of a single source file @@ -10,7 +11,6 @@ use crate::SourceLocation; /// Builds a [LineIndex] on creation, and associates that index with the source it /// was created from for future method calls. #[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SourceFile { contents: String, index: LineIndex, @@ -42,36 +42,46 @@ impl SourceFile { self.index = LineIndex::from_source_text(&self.contents); } - /// Returns the row and column index for an offset. + /// Returns the [SourceLocation] for an offset. /// /// ## Examples /// /// ``` - /// # use biome_text_size::TextSize; - /// # use source_file::{SourceFile, OneIndexed, SourceLocation}; - /// let source = "def a():\n pass".to_string(); + /// use biome_text_size::TextSize; + /// use source_file::{SourceFile, SourceLocation, LineNumber, LineOffset, Encoding}; + /// + /// let source = "x <- function()\n NULL".to_string(); /// let source = SourceFile::new(source); /// /// assert_eq!( - /// source.source_location(TextSize::from(0)), - /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) } + /// source.source_location(TextSize::from(0), Encoding::UTF8), + /// SourceLocation::new( + /// LineNumber::from(0), + /// LineOffset::new(0, Encoding::UTF8) + /// ) /// ); - /// /// assert_eq!( - /// source.source_location(TextSize::from(4)), - /// SourceLocation { row: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) } + /// source.source_location(TextSize::from(4), Encoding::UTF8), + /// SourceLocation::new( + /// LineNumber::from(0), + /// LineOffset::new(4, Encoding::UTF8) + /// ) /// ); /// assert_eq!( - /// source.source_location(TextSize::from(13)), - /// SourceLocation { row: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) } + /// source.source_location(TextSize::from(20), Encoding::UTF8), + /// SourceLocation::new( + /// LineNumber::from(1), + /// LineOffset::new(4, Encoding::UTF8) + /// ) /// ); /// ``` - /// - /// ## Panics - /// - /// If the offset is out of bounds. - pub fn source_location(&self, offset: TextSize) -> SourceLocation { - self.index.source_location(offset, self.contents()) + pub fn source_location( + &self, + offset: TextSize, + encoding: LineOffsetEncoding, + ) -> SourceLocation { + self.index + .source_location(offset, self.contents(), encoding) } /// Return the number of lines in the source code. @@ -89,56 +99,48 @@ impl SourceFile { /// ## Examples /// /// ``` - /// # use biome_text_size::TextSize; - /// # use source_file::{SourceFile, OneIndexed, SourceLocation}; + /// use biome_text_size::TextSize; + /// use source_file::{SourceFile, SourceLocation, LineNumber}; + /// /// let source = "def a():\n pass".to_string(); /// let source = SourceFile::new(source); /// - /// assert_eq!(source.line_index(TextSize::from(0)), OneIndexed::from_zero_indexed(0)); - /// assert_eq!(source.line_index(TextSize::from(4)), OneIndexed::from_zero_indexed(0)); - /// assert_eq!(source.line_index(TextSize::from(13)), OneIndexed::from_zero_indexed(1)); + /// assert_eq!(source.line_number(TextSize::from(0)), LineNumber::from(0)); + /// assert_eq!(source.line_number(TextSize::from(4)), LineNumber::from(0)); + /// assert_eq!(source.line_number(TextSize::from(13)), LineNumber::from(1)); /// ``` - /// - /// ## Panics - /// - /// If the offset is out of bounds. - pub fn line_index(&self, offset: TextSize) -> OneIndexed { - self.index.line_index(offset) + pub fn line_number(&self, offset: TextSize) -> LineNumber { + self.index.line_number(offset) } - /// Returns the [byte offset](TextSize) for the `line` with the given index. - pub fn line_start(&self, line: OneIndexed) -> TextSize { - self.index.line_start(line, self.contents()) + /// Returns the [byte offset](TextSize) for the `line_number`'s start. + pub fn line_start(&self, line_number: LineNumber) -> TextSize { + self.index.line_start(line_number, self.contents()) } - /// Returns the [byte offset](TextSize) of the `line`'s end. + /// Returns the [byte offset](TextSize) of the `line_number`'s end. /// The offset is the end of the line, up to and including the newline character ending the line (if any). - pub fn line_end(&self, line: OneIndexed) -> TextSize { - self.index.line_end(line, self.contents()) - } - - /// Returns the [byte offset](TextSize) of the `line`'s end. - /// The offset is the end of the line, excluding the newline character ending the line (if any). - pub fn line_end_exclusive(&self, line: OneIndexed) -> TextSize { - self.index.line_end_exclusive(line, self.contents()) + pub fn line_end(&self, line_number: LineNumber) -> TextSize { + self.index.line_end(line_number, self.contents()) } - /// Returns the [`TextRange`] of the `line` with the given index. + /// Returns the [`TextRange`] of the `line_number`. /// The start points to the first character's [byte offset](TextSize), the end up to, and including /// the newline character ending the line (if any). - pub fn line_range(&self, line: OneIndexed) -> TextRange { - self.index.line_range(line, self.contents()) + pub fn line_range(&self, line_number: LineNumber) -> TextRange { + self.index.line_range(line_number, self.contents()) } - /// Returns the [byte offset](TextSize) at `line` and `column`. + /// Returns the [byte offset](TextSize) at the [SourceLocation]. /// /// ## Examples /// /// ### ASCII /// /// ``` - /// use source_file::{SourceFile, OneIndexed}; + /// use source_file::{SourceFile, SourceLocation, LineNumber, LineOffset, Encoding}; /// use biome_text_size::TextSize; + /// /// let source = r#"a = 4 /// c = "some string" /// x = b"#.to_string(); @@ -146,23 +148,48 @@ impl SourceFile { /// let source = SourceFile::new(source); /// /// // First line, first column - /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0)), TextSize::from(0)); + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(0), + /// LineOffset::new(0, Encoding::UTF8) + /// )), + /// TextSize::from(0) + /// ); /// /// // Second line, 4th column - /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(4)), TextSize::from(10)); + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(1), + /// LineOffset::new(4, Encoding::UTF8) + /// )), + /// TextSize::from(10) + /// ); /// /// // Offset past the end of the first line - /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(10)), TextSize::from(6)); + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(0), + /// LineOffset::new(10, Encoding::UTF8) + /// )), + /// TextSize::from(6) + /// ); /// /// // Offset past the end of the file - /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0)), TextSize::from(29)); + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(3), + /// LineOffset::new(0, Encoding::UTF8) + /// )), + /// TextSize::from(29) + /// ); /// ``` /// /// ### UTF8 /// /// ``` - /// use source_file::{SourceFile, OneIndexed}; + /// use source_file::{SourceFile, SourceLocation, LineNumber, LineOffset, Encoding}; /// use biome_text_size::TextSize; + /// /// let source = r#"a = 4 /// c = "❤️" /// x = b"#.to_string(); @@ -170,20 +197,62 @@ impl SourceFile { /// let source = SourceFile::new(source); /// /// // First line, first column - /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(0), OneIndexed::from_zero_indexed(0)), TextSize::from(0)); + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(0), + /// LineOffset::new(0, Encoding::UTF8) + /// )), + /// TextSize::from(0) + /// ); + /// + /// // Third line, 2nd column, after emoji, UTF8 + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(2), + /// LineOffset::new(1, Encoding::UTF8) + /// )), + /// TextSize::from(20) + /// ); /// - /// // Third line, 2nd column, after emoji - /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(2), OneIndexed::from_zero_indexed(1)), TextSize::from(20)); + /// // Third line, 2nd column, after emoji, UTF16 + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(2), + /// LineOffset::new(1, Encoding::UTF16) + /// )), + /// TextSize::from(20) + /// ); /// - /// // Offset past the end of the second line - /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(1), OneIndexed::from_zero_indexed(10)), TextSize::from(19)); + /// // Offset past the end of the second line, UTF8 + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(1), + /// LineOffset::new(10, Encoding::UTF8) + /// )), + /// TextSize::from(16) + /// ); + /// + /// // Offset past the end of the second line, UTF32 + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(1), + /// LineOffset::new(10, Encoding::UTF32) + /// )), + /// TextSize::from(19) + /// ); /// /// // Offset past the end of the file - /// assert_eq!(source.offset(OneIndexed::from_zero_indexed(3), OneIndexed::from_zero_indexed(0)), TextSize::from(24)); + /// assert_eq!( + /// source.offset(SourceLocation::new( + /// LineNumber::from(3), + /// LineOffset::new(0, Encoding::UTF32) + /// )), + /// TextSize::from(24) + /// ); /// ``` /// - pub fn offset(&self, line: OneIndexed, column: OneIndexed) -> TextSize { - self.index.offset(line, column, self.contents()) + pub fn offset(&self, source_location: SourceLocation) -> TextSize { + self.index.offset(source_location, self.contents()) } /// Returns the [byte offsets](TextSize) for every line diff --git a/crates/source_file/src/source_location.rs b/crates/source_file/src/source_location.rs index fdc8fab8..34be78b2 100644 --- a/crates/source_file/src/source_location.rs +++ b/crates/source_file/src/source_location.rs @@ -1,34 +1,88 @@ -use std::fmt::{Debug, Formatter}; +use std::fmt::Debug; -use crate::OneIndexed; - -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct SourceLocation { - pub row: OneIndexed, - pub column: OneIndexed, + line_number: LineNumber, + line_offset: LineOffset, } -impl Default for SourceLocation { - fn default() -> Self { +impl SourceLocation { + pub fn new(line_number: LineNumber, line_offset: LineOffset) -> Self { Self { - row: OneIndexed::MIN, - column: OneIndexed::MIN, + line_number, + line_offset, } } + + pub fn line_number(&self) -> LineNumber { + self.line_number + } + + pub fn line_offset(&self) -> LineOffset { + self.line_offset + } +} + +/// A 0-indexed line number +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct LineNumber(u32); + +impl From for LineNumber { + fn from(value: u32) -> Self { + LineNumber(value) + } +} + +impl TryFrom for LineNumber { + type Error = std::num::TryFromIntError; + + fn try_from(value: usize) -> Result { + Ok(LineNumber(u32::try_from(value)?)) + } } -impl Debug for SourceLocation { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SourceLocation") - .field("row", &self.row.get()) - .field("column", &self.column.get()) - .finish() +impl From for u32 { + fn from(value: LineNumber) -> Self { + value.0 } } -impl std::fmt::Display for SourceLocation { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{row}:{column}", row = self.row, column = self.column) +impl From for usize { + fn from(value: LineNumber) -> Self { + value.0 as usize } } + +/// A 0-indexed offset into a line, represented as a number of code units under one of the +/// three possible [LineOffsetEncoding]s +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct LineOffset { + raw: u32, + encoding: LineOffsetEncoding, +} + +impl LineOffset { + pub fn new(raw: u32, encoding: LineOffsetEncoding) -> Self { + Self { raw, encoding } + } + + pub fn raw(&self) -> u32 { + self.raw + } + + pub fn encoding(&self) -> LineOffsetEncoding { + self.encoding + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LineOffsetEncoding { + /// Preferred encoding, as Rust [String]s are UTF-8 + UTF8, + + /// UTF-16 is the encoding supported by all LSP clients, but is most expensive to translate + UTF16, + + /// Second choice because UTF-32 uses a fixed 4 byte encoding for each character (makes conversion relatively easy) + UTF32, +} From 6a6e3c5a0b6673f62afaf656f3fafea945f6af41 Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Mon, 2 Jun 2025 15:49:01 -0400 Subject: [PATCH 43/44] Collapse `LineIndex` into `SourceFile` --- crates/server/src/proto/text_size.rs | 69 -- crates/source_file/src/lib.rs | 10 +- crates/source_file/src/line_index.rs | 653 ----------------- crates/source_file/src/source_file.rs | 824 ++++++++++++++++++++-- crates/source_file/src/source_location.rs | 4 + 5 files changed, 777 insertions(+), 783 deletions(-) delete mode 100644 crates/source_file/src/line_index.rs diff --git a/crates/server/src/proto/text_size.rs b/crates/server/src/proto/text_size.rs index 068a42c1..9f38227d 100644 --- a/crates/server/src/proto/text_size.rs +++ b/crates/server/src/proto/text_size.rs @@ -39,75 +39,6 @@ impl TextSizeExt for TextSize { } } -/// Here's how to think about these conversions: -/// -/// [lsp_types::Position] contains a location encoded as `row` and `character`, -/// where: -/// - `row` represents the 0-indexed line number -/// - `character` represents the 0-indexed column offset, with precise meaning decided -/// by [lsp_types::PositionEncodingKind] -/// -/// `character` is interpreted as: -/// - With [lsp_types::PositionEncodingKind::UTF8], the number of UTF-8 code units. -/// - With [lsp_types::PositionEncodingKind::UTF16], the number of UTF-16 code units. -/// - With [lsp_types::PositionEncodingKind::UTF32], the number of UTF-32 code units. -/// -/// Now, for some definitions: -/// -/// - Code unit: The minimal bit combination that can represent a single character. -/// - UTF-8: -/// - 1 code unit = 1 byte = 8 bits -/// - UTF-16: -/// - 1 code unit = 2 bytes = 16 bits -/// - UTF-32: -/// - 1 code unit = 4 bytes = 32 bits -/// -/// - Character: A combination of code units that construct a single UTF element. -/// - UTF-8: -/// - 1 character = 1,2,3,4 code units = 1,2,3,4 bytes = 8,16,24,32 bits -/// - UTF-16: -/// - 1 character = 1,2 code units = 2,4 bytes = 16,32 bits -/// - UTF-16: -/// - 1 character = 1 code units = 4 bytes = 32 bits -/// -/// - Unicode Scalar Value: Any Unicode Code Point other than a Surrogate Code Point ( -/// which are only used by UTF-16). Technically this means any value in the range of -/// [0 to 0x10FFFF] excluding the slice of [0xD800 to 0xDFFF]. -/// -/// - Unicode Code Point: Any value in the Unicode code space of [0 to 0x10FFFF]. This -/// means that something representing an arbitrary code point must be 4 bytes, implying -/// that something representing a Unicode Scalar Value must also be 4 bytes. -/// -/// In Rust, [String] and [str] are in UTF-8. Figuring out how to go from the, say, -/// 8th column `Position.character` of a line to the byte offset on that line requires -/// knowing both the UTF-8 content of that line and the `PositionEncodingKind` that -/// `Position.character` is encoded in. -/// -/// Note that `chars()` returns an iterator over the individual `char` contained within a -/// string. And each `char` is a Unicode Scalar Value. This means that each `char` is -/// internally represented as a `u32` of exactly 4 bytes. It also means that you can -/// think of iterating over `chars()` as equivalent to iterating over UTF-32 Characters -/// or UTF-32 Code Points. -/// -/// Also relevant is that [char::len_utf16] returns the number of UTF-16 code units that -/// would be required to represent the `char`, and [char::len_utf8] returns the number -/// of UTF-8 code units (and therefore bytes) that would be required to represent the -/// `char`. -/// -/// # Converting `character` UTF-8/16/32 code points -> UTF-8 String byte offset -/// -/// An arbitrary algorithm to find the number of UTF-8 bytes required to represent `character` column offset would be: -/// - Iterate over `chars()` -/// - Figure out how many `char`s are required TODO?? -/// -/// - With [lsp_types::PositionEncodingKind::UTF8]: -/// - `character` is the number of UTF-8 code units -/// - 1 UTF-8 code unit is just 1 UTF-8 byte, so just return `character` -/// - With [lsp_types::PositionEncodingKind::UTF16]: -/// - `character` is the number of UTF-16 code units -/// - 1 UTF-16 code unit must -/// - With [lsp_types::PositionEncodingKind::UTF32]: -/// fn remap_encoding(encoding: PositionEncoding) -> LineOffsetEncoding { match encoding { PositionEncoding::UTF16 => LineOffsetEncoding::UTF16, diff --git a/crates/source_file/src/lib.rs b/crates/source_file/src/lib.rs index f768dd8b..41bbf6de 100644 --- a/crates/source_file/src/lib.rs +++ b/crates/source_file/src/lib.rs @@ -1,3 +1,12 @@ +//! Tools for managing a single source file +//! +//! In particular, [SourceFile] manages the conversions between UTF-8 byte offsets into a +//! [String], and line number + line offset (also known as row/column or row/character) +//! backed [SourceLocation]s, where the line offset is measured in UTF code units and is +//! dependent on the [LineOffsetEncoding] used. [SourceLocation]s are meant to easily map +//! to LSP `Position`s, and can handle the common `PositionEncodingKind`s of UTF-8, +//! UTF-16, and UTF-32. + pub use crate::newlines::{find_newline, infer_line_ending, normalize_newlines, LineEnding}; pub use crate::source_file::SourceFile; pub use crate::source_location::LineNumber; @@ -5,7 +14,6 @@ pub use crate::source_location::LineOffset; pub use crate::source_location::LineOffsetEncoding; pub use crate::source_location::SourceLocation; -mod line_index; mod newlines; mod source_file; mod source_location; diff --git a/crates/source_file/src/line_index.rs b/crates/source_file/src/line_index.rs deleted file mode 100644 index 5e433ef6..00000000 --- a/crates/source_file/src/line_index.rs +++ /dev/null @@ -1,653 +0,0 @@ -use std::fmt; -use std::fmt::{Debug, Formatter}; -use std::sync::Arc; - -use biome_text_size::{TextLen, TextRange, TextSize}; - -use crate::source_location::LineNumber; -use crate::source_location::LineOffset; -use crate::source_location::LineOffsetEncoding; -use crate::SourceLocation; - -/// Index for fast [byte offset](TextSize) to [`SourceLocation`] conversions. -/// -/// Cloning a [`LineIndex`] is cheap because it only requires bumping a reference count. -#[derive(Clone, Eq, PartialEq)] -pub(crate) struct LineIndex { - inner: Arc, -} - -#[derive(Eq, PartialEq)] -struct LineIndexInner { - line_starts: Vec, - kind: IndexKind, -} - -impl LineIndex { - /// Builds the [`LineIndex`] from the source text of a file. - pub(crate) fn from_source_text(text: &str) -> Self { - let mut line_starts: Vec = Vec::with_capacity(text.len() / 88); - line_starts.push(TextSize::from(0)); - - let bytes = text.as_bytes(); - let mut utf8 = false; - - assert!(u32::try_from(bytes.len()).is_ok()); - - for (i, byte) in bytes.iter().enumerate() { - utf8 |= !byte.is_ascii(); - - match byte { - // Only track one line break for `\r\n`. - b'\r' if bytes.get(i + 1) == Some(&b'\n') => continue, - b'\n' | b'\r' => { - // SAFETY: Assertion above guarantees `i <= u32::MAX` - #[allow(clippy::cast_possible_truncation)] - line_starts.push(TextSize::from(i as u32) + TextSize::from(1)); - } - _ => {} - } - } - - let kind = if utf8 { - IndexKind::Utf8 - } else { - IndexKind::Ascii - }; - - Self { - inner: Arc::new(LineIndexInner { line_starts, kind }), - } - } - - fn kind(&self) -> IndexKind { - self.inner.kind - } - - /// Returns the [SourceLocation] for an offset. - pub(crate) fn source_location( - &self, - offset: TextSize, - content: &str, - encoding: LineOffsetEncoding, - ) -> SourceLocation { - let line_number = self.line_number(offset); - let line_start = self.line_start(line_number, content); - - let line_offset = if self.is_ascii() { - LineOffset::new((offset - line_start).into(), encoding) - } else { - match encoding { - LineOffsetEncoding::UTF8 => LineOffset::new((offset - line_start).into(), encoding), - LineOffsetEncoding::UTF16 => { - let line_contents_up_to_offset = &content[TextRange::new(line_start, offset)]; - let offset = line_contents_up_to_offset - .encode_utf16() - .count() - .try_into() - .expect("A single line's offset should fit in u32"); - LineOffset::new(offset, encoding) - } - LineOffsetEncoding::UTF32 => { - let line_contents_up_to_offset = &content[TextRange::new(line_start, offset)]; - let offset = line_contents_up_to_offset - .chars() - .count() - .try_into() - .expect("A single line's offset should fit in u32"); - LineOffset::new(offset, encoding) - } - } - }; - - SourceLocation::new(line_number, line_offset) - } - - /// Return the number of lines in the source code. - pub(crate) fn line_count(&self) -> usize { - self.line_starts().len() - } - - /// Returns `true` if the text only consists of ASCII characters. - pub(crate) fn is_ascii(&self) -> bool { - self.kind().is_ascii() - } - - /// Returns the line number for a given offset. - pub(crate) fn line_number(&self, offset: TextSize) -> LineNumber { - let line = match self.line_starts().binary_search(&offset) { - // `offset` is at the start of a line - Ok(row) => row, - Err(next_row) => { - // SAFETY: Safe because the index always contains an entry for the offset 0 - next_row - 1 - } - }; - - LineNumber::try_from(line).expect("Number of line starts should fit in a `LineNumber`") - } - - /// Returns the [byte offset](TextSize) for the `line`'s start. - pub(crate) fn line_start(&self, line_number: LineNumber, contents: &str) -> TextSize { - let line_number = usize::from(line_number); - let starts = self.line_starts(); - - // If start-of-line position after last line - if line_number >= starts.len() { - contents.text_len() - } else { - starts[line_number] - } - } - - /// Returns the [byte offset](TextSize) of the `line`'s end. - /// The offset is the end of the line, up to and including the newline character ending the line (if any). - pub(crate) fn line_end(&self, line_number: LineNumber, contents: &str) -> TextSize { - let line_number = usize::from(line_number); - let starts = self.line_starts(); - - // If start-of-line position after last line - if line_number.saturating_add(1) >= starts.len() { - contents.text_len() - } else { - starts[line_number + 1] - } - } - - /// Returns the [`TextRange`] of the `line`. - /// The start points to the first character's [byte offset](TextSize), the end up to, and including - /// the newline character ending the line (if any). - pub(crate) fn line_range(&self, line_number: LineNumber, contents: &str) -> TextRange { - TextRange::new( - self.line_start(line_number, contents), - self.line_end(line_number, contents), - ) - } - - /// Returns the [byte offset](TextSize) at this [SourceLocation]. - pub(crate) fn offset(&self, source_location: SourceLocation, contents: &str) -> TextSize { - let line_number = source_location.line_number(); - let line_offset = source_location.line_offset(); - - let line_range = self.line_range(line_number, contents); - - let offset = if self.is_ascii() { - TextSize::from(line_offset.raw()) - } else { - match line_offset.encoding() { - LineOffsetEncoding::UTF8 => TextSize::from(line_offset.raw()), - LineOffsetEncoding::UTF16 => { - let n_code_units = line_offset.raw(); - let line_contents = &contents[line_range]; - - let mut i = 0; - let mut offset = 0; - - for c in line_contents.chars() { - if i >= n_code_units { - break; - } - i += c.len_utf16() as u32; - offset += c.len_utf8() as u32; - } - - TextSize::from(offset) - } - LineOffsetEncoding::UTF32 => { - let n_code_units = line_offset.raw(); - let line_contents = &contents[line_range]; - - let mut offset: u32 = 0; - - for c in line_contents.chars().take(n_code_units as usize) { - offset += c.len_utf8() as u32; - } - - TextSize::from(offset) - } - } - }; - - line_range.start() + offset.clamp(TextSize::from(0), line_range.len()) - } - - /// Returns the [byte offsets](TextSize) for every line - pub fn line_starts(&self) -> &[TextSize] { - &self.inner.line_starts - } -} - -impl Debug for LineIndex { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_list().entries(self.line_starts()).finish() - } -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -enum IndexKind { - /// Optimized index for an ASCII only document - Ascii, - - /// Index for UTF8 documents - Utf8, -} - -impl IndexKind { - const fn is_ascii(self) -> bool { - matches!(self, IndexKind::Ascii) - } -} - -#[cfg(test)] -mod tests { - use biome_text_size::TextSize; - - use crate::line_index::LineIndex; - use crate::source_location::LineNumber; - use crate::source_location::LineOffset; - use crate::source_location::LineOffsetEncoding; - use crate::SourceLocation; - - #[test] - fn ascii_index() { - let index = LineIndex::from_source_text(""); - assert_eq!(index.line_starts(), &[TextSize::from(0)]); - - let index = LineIndex::from_source_text("x = 1"); - assert_eq!(index.line_starts(), &[TextSize::from(0)]); - - let index = LineIndex::from_source_text("x = 1\n"); - assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]); - - let index = LineIndex::from_source_text("x = 1\ny = 2\nz = x + y\n"); - assert_eq!( - index.line_starts(), - &[ - TextSize::from(0), - TextSize::from(6), - TextSize::from(12), - TextSize::from(22) - ] - ); - } - - #[test] - fn ascii_source_location() { - let contents = "x = 1\ny = 2"; - let index = LineIndex::from_source_text(contents); - - // First row. - let loc = index.source_location(TextSize::from(2), contents, LineOffsetEncoding::UTF8); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(2, LineOffsetEncoding::UTF8) - ) - ); - - // Second row. - let loc = index.source_location(TextSize::from(6), contents, LineOffsetEncoding::UTF8); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF8) - ) - ); - - let loc = index.source_location(TextSize::from(11), contents, LineOffsetEncoding::UTF8); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(5, LineOffsetEncoding::UTF8) - ) - ); - } - - #[test] - fn ascii_carriage_return() { - let contents = "x = 4\ry = 3"; - let index = LineIndex::from_source_text(contents); - assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]); - - assert_eq!( - index.source_location(TextSize::from(4), contents, LineOffsetEncoding::UTF8), - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(4, LineOffsetEncoding::UTF8) - ) - ); - assert_eq!( - index.source_location(TextSize::from(6), contents, LineOffsetEncoding::UTF8), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF8) - ) - ); - assert_eq!( - index.source_location(TextSize::from(7), contents, LineOffsetEncoding::UTF8), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(1, LineOffsetEncoding::UTF8) - ) - ); - } - - #[test] - fn ascii_carriage_return_newline() { - let contents = "x = 4\r\ny = 3"; - let index = LineIndex::from_source_text(contents); - assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(7)]); - - assert_eq!( - index.source_location(TextSize::from(4), contents, LineOffsetEncoding::UTF8), - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(4, LineOffsetEncoding::UTF8) - ) - ); - assert_eq!( - index.source_location(TextSize::from(7), contents, LineOffsetEncoding::UTF8), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF8) - ) - ); - assert_eq!( - index.source_location(TextSize::from(8), contents, LineOffsetEncoding::UTF8), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(1, LineOffsetEncoding::UTF8) - ) - ); - } - - #[test] - fn utf8_index() { - let index = LineIndex::from_source_text("x = '🫣'"); - assert_eq!(index.line_count(), 1); - assert_eq!(index.line_starts(), &[TextSize::from(0)]); - - let index = LineIndex::from_source_text("x = '🫣'\n"); - assert_eq!(index.line_count(), 2); - assert_eq!( - index.line_starts(), - &[TextSize::from(0), TextSize::from(11)] - ); - - let index = LineIndex::from_source_text("x = '🫣'\ny = 2\nz = x + y\n"); - assert_eq!(index.line_count(), 4); - assert_eq!( - index.line_starts(), - &[ - TextSize::from(0), - TextSize::from(11), - TextSize::from(17), - TextSize::from(27) - ] - ); - - let index = LineIndex::from_source_text("# 🫣\nclass Foo:\n \"\"\".\"\"\""); - assert_eq!(index.line_count(), 3); - assert_eq!( - index.line_starts(), - &[TextSize::from(0), TextSize::from(7), TextSize::from(18)] - ); - } - - #[test] - fn utf8_carriage_return() { - let contents = "x = '🫣'\ry = 3"; - let index = LineIndex::from_source_text(contents); - assert_eq!(index.line_count(), 2); - assert_eq!( - index.line_starts(), - &[TextSize::from(0), TextSize::from(11)] - ); - - // Second ', UTF8 - assert_eq!( - index.source_location(TextSize::from(9), contents, LineOffsetEncoding::UTF8), - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(9, LineOffsetEncoding::UTF8) - ) - ); - assert_eq!( - index.source_location(TextSize::from(11), contents, LineOffsetEncoding::UTF8), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF8) - ) - ); - assert_eq!( - index.source_location(TextSize::from(12), contents, LineOffsetEncoding::UTF8), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(1, LineOffsetEncoding::UTF8) - ) - ); - - // Second ', UTF16 - assert_eq!( - index.source_location(TextSize::from(9), contents, LineOffsetEncoding::UTF16), - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(7, LineOffsetEncoding::UTF16) - ) - ); - assert_eq!( - index.source_location(TextSize::from(11), contents, LineOffsetEncoding::UTF16), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF16) - ) - ); - assert_eq!( - index.source_location(TextSize::from(12), contents, LineOffsetEncoding::UTF16), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(1, LineOffsetEncoding::UTF16) - ) - ); - - // Second ', UTF32 - assert_eq!( - index.source_location(TextSize::from(9), contents, LineOffsetEncoding::UTF32), - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(6, LineOffsetEncoding::UTF32) - ) - ); - assert_eq!( - index.source_location(TextSize::from(11), contents, LineOffsetEncoding::UTF32), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF32) - ) - ); - assert_eq!( - index.source_location(TextSize::from(12), contents, LineOffsetEncoding::UTF32), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(1, LineOffsetEncoding::UTF32) - ) - ); - } - - #[test] - fn utf8_carriage_return_newline() { - let contents = "x = '🫣'\r\ny = 3"; - let index = LineIndex::from_source_text(contents); - assert_eq!(index.line_count(), 2); - assert_eq!( - index.line_starts(), - &[TextSize::from(0), TextSize::from(12)] - ); - - // Second ' - assert_eq!( - index.source_location(TextSize::from(9), contents, LineOffsetEncoding::UTF32), - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(6, LineOffsetEncoding::UTF32) - ) - ); - assert_eq!( - index.source_location(TextSize::from(12), contents, LineOffsetEncoding::UTF32), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF32) - ) - ); - assert_eq!( - index.source_location(TextSize::from(13), contents, LineOffsetEncoding::UTF32), - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(1, LineOffsetEncoding::UTF32) - ) - ); - } - - #[test] - fn utf8_byte_offset() { - let contents = "x = '☃'\ny = 2"; - let index = LineIndex::from_source_text(contents); - assert_eq!( - index.line_starts(), - &[TextSize::from(0), TextSize::from(10)] - ); - - // First row, start - let loc = index.source_location(TextSize::from(0), contents, LineOffsetEncoding::UTF8); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(0, LineOffsetEncoding::UTF8) - ) - ); - let loc = index.source_location(TextSize::from(0), contents, LineOffsetEncoding::UTF16); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(0, LineOffsetEncoding::UTF16) - ) - ); - let loc = index.source_location(TextSize::from(0), contents, LineOffsetEncoding::UTF32); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(0, LineOffsetEncoding::UTF32) - ) - ); - - // First row, right before - let loc = index.source_location(TextSize::from(5), contents, LineOffsetEncoding::UTF8); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(5, LineOffsetEncoding::UTF8) - ) - ); - let loc = index.source_location(TextSize::from(5), contents, LineOffsetEncoding::UTF16); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(5, LineOffsetEncoding::UTF16) - ) - ); - let loc = index.source_location(TextSize::from(5), contents, LineOffsetEncoding::UTF32); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(5, LineOffsetEncoding::UTF32) - ) - ); - - // First row, right after - let loc = index.source_location(TextSize::from(8), contents, LineOffsetEncoding::UTF8); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(8, LineOffsetEncoding::UTF8) - ) - ); - let loc = index.source_location(TextSize::from(8), contents, LineOffsetEncoding::UTF16); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(6, LineOffsetEncoding::UTF16) - ) - ); - let loc = index.source_location(TextSize::from(8), contents, LineOffsetEncoding::UTF32); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(0), - LineOffset::new(6, LineOffsetEncoding::UTF32) - ) - ); - - // Second row, start - let loc = index.source_location(TextSize::from(10), contents, LineOffsetEncoding::UTF8); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF8) - ) - ); - let loc = index.source_location(TextSize::from(10), contents, LineOffsetEncoding::UTF16); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF16) - ) - ); - let loc = index.source_location(TextSize::from(10), contents, LineOffsetEncoding::UTF32); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(0, LineOffsetEncoding::UTF32) - ) - ); - - // One-past-the-end. - let loc = index.source_location(TextSize::from(15), contents, LineOffsetEncoding::UTF8); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(5, LineOffsetEncoding::UTF8) - ) - ); - let loc = index.source_location(TextSize::from(15), contents, LineOffsetEncoding::UTF16); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(5, LineOffsetEncoding::UTF16) - ) - ); - let loc = index.source_location(TextSize::from(15), contents, LineOffsetEncoding::UTF32); - assert_eq!( - loc, - SourceLocation::new( - LineNumber::from(1), - LineOffset::new(5, LineOffsetEncoding::UTF32) - ) - ); - } -} diff --git a/crates/source_file/src/source_file.rs b/crates/source_file/src/source_file.rs index 2803a332..ae7ab7ac 100644 --- a/crates/source_file/src/source_file.rs +++ b/crates/source_file/src/source_file.rs @@ -1,26 +1,206 @@ +//! This top level documentation details the algorithms and terminology behind +//! [SourceFile::offset] and [SourceFile::source_location]. The primary goal of +//! these functions (and really this whole module) is to handle conversion between +//! a [byte offset](TextSize) and a [line number + line offset](SourceLocation), +//! including treatment of various UTF encodings. +//! +//! Both [TextSize] and [SourceLocation] are ways of pointing at a location within a +//! file: +//! +//! - A [TextSize] is a simple byte offset into a UTF-8 encoded [String]. +//! +//! - A [SourceLocation] contains a location encoded as `line_number` and `line_offset`, +//! where: +//! - `line_number` ([LineNumber]) represents the 0-indexed line number +//! - `line_offset` ([LineOffset]) represents the 0-indexed offset from the start of the +//! line represented by `line_number`. The offset itself is (critically) measured in a +//! UTF concept known as "code units", and is meaningless without the corresponding +//! [LineOffsetEncoding]. +//! +//! A [SourceLocation] is meant to map to an LSP `Position`, and a [LineOffsetEncoding] is +//! meant to map to an LSP `PositionEncodingKind`. We are typically handed an LSP +//! `Position`, must convert it to a [TextSize] (by going through [SourceLocation]), then +//! we use that [TextSize] to index into our [String] representing our document. On the +//! way out, we must convert from [TextSize] or [TextRange] back to LSP `Position` or LSP +//! `Range` by going back through [SourceLocation]. +//! +//! Now, for some definitions: +//! +//! - Code unit: The minimal bit combination that can represent a single character, +//! depending on the encoding used. +//! - UTF-8: +//! - 1 code unit = 1 byte = 8 bits +//! - UTF-16: +//! - 1 code unit = 2 bytes = 16 bits +//! - UTF-32: +//! - 1 code unit = 4 bytes = 32 bits +//! +//! - Character: A combination of code units that construct a single UTF element. +//! - UTF-8: +//! - 1 character = 1,2,3,4 code units = 1,2,3,4 bytes = 8,16,24,32 bits +//! - UTF-16: +//! - 1 character = 1,2 code units = 2,4 bytes = 16,32 bits +//! - UTF-32: +//! - 1 character = 1 code units = 4 bytes = 32 bits +//! +//! - Unicode Scalar Value: Any Unicode Code Point other than a Surrogate Code Point ( +//! which are only used by UTF-16). Technically, this means any value in the range of +//! [0 to 0x10FFFF] excluding the slice of [0xD800 to 0xDFFF]. The [char] type +//! represents these. +//! +//! - Unicode Code Point: Any value in the Unicode code space of [0 to 0x10FFFF]. This +//! means that something representing an arbitrary code point must be 4 bytes, implying +//! that something representing a Unicode Scalar Value must also be 4 bytes, and +//! practically a [char] has the same memory layout as a [u32] under the hood. +//! +//! - Rust [String] and [str] are in UTF-8, and all [byte offsets](TextSize) into them +//! assume the strings are encoded in UTF-8. +//! +//! One key thing to note is that `\n` (or `\r\n`) is the same in all encodings. This +//! means that finding the [LineNumber] you are on is easy, you are either given it +//! through [SourceLocation::line_number], or it can be easily computed from a UTF-8 [byte +//! offset](TextSize) by doing a binary search into an ordered vector of line start +//! locations. That isolates the "hard" details of encoding translation to the +//! [LineOffset], which is typically an extremely small slice of the overall file. +//! +//! # Implementing [SourceFile::offset] +//! +//! ## UTF-8 code units -> UTF-8 byte offset +//! +//! Easy! 1 UTF-8 code unit maps directly to 1 byte in a UTF-8 string, so counting the +//! code units is equivalent to finding the byte offset into the UTF-8 string. +//! +//! ## UTF-16 code units -> UTF-8 byte offset +//! +//! 1 UTF-16 code unit is always 2 bytes if the string is encoded in UTF-16. But if +//! the string is encoded in UTF-8 as ours is, we don't immediately know if the +//! UTF-16 code unit would be represented by 1 or 2 bytes in a UTF-8 string. +//! +//! To do this, we iterate over the [str::chars()] of the string, which are Unicode Scalar +//! Values, i.e. a UTF-32 character, the widest of them all. To figure out the correct +//! amount of UTF-16 code units to count up, we compute the [char::len_utf16()] of each +//! character, which returns the number of UTF-16 code units required if the [char] +//! were instead encoded in UTF-16. Once we've reached the [LineOffset] offset, we've +//! found all the [char]s we care about. To find the byte offset in UTF-8 encoded space, +//! we sum up the [char::len_utf8()] of each of those [char]s. +//! +//! ## UTF-32 code units -> UTF-8 byte offset +//! +//! Very similar to UTF-16, except 1 UTF-32 code unit is always 4 bytes if the string +//! itself is encoded in UTF-32. +//! +//! This is slightly easier than UTF-16. Because [str::chars()] already returns Unicode +//! Scalar Values, also known as UTF-32 characters, and because 1 UTF-32 character is +//! the same size as 1 UTF-32 code unit, we just iterate over the [str::chars()] up to +//! the [LineOffset] value, summing the [char::len_utf8()] of each [char] along the way. +//! +//! # Implementing [SourceFile::source_location] +//! +//! ## UTF-8 byte offset -> UTF-8 code units +//! +//! Easy! Like with the other direction, UTF-8 byte offsets can be directly translated +//! to UTF-8 code units, so there is nothing to do. +//! +//! ## UTF-8 byte offset -> UTF-16 code units +//! +//! This is actually pretty easy. All we do is slice the [String] from its start up to +//! the UTF-8 byte offset in question, then call [str::encode_utf16()] and count the +//! number of UTF-16 code units it returns. +//! +//! This would be expensive if we had to reencode as UTF-16 from the beginning of the +//! file, but we actually just need to reencode as UTF-16 from the beginning of the +//! line that the offset is on, up to the offset position itself, which is a very small +//! slice. This works because the line number itself is not dependent on the encoding, +//! only the line offset into that line is. +//! +//! ## UTF-8 byte offset -> UTF-32 code units +//! +//! Same as with UTF-16, but rather than [str::encode_utf16()], we can use [str::chars()] +//! because it already returns Unicode Scalar Values, which are UTF-32 characters, which +//! are equivalent to UTF-32 code units. + +use biome_text_size::TextLen; use biome_text_size::TextRange; use biome_text_size::TextSize; -use crate::line_index::LineIndex; use crate::source_location::LineNumber; use crate::source_location::LineOffsetEncoding; +use crate::LineOffset; use crate::SourceLocation; /// Manager of a single source file /// -/// Builds a [LineIndex] on creation, and associates that index with the source it -/// was created from for future method calls. +/// Builds a vector of line start locations on creation, for use with +/// [TextSize] <-> [SourceLocation] conversions. In particular, see: +/// +/// - [Self::offset()] for [SourceLocation] -> [TextSize] +/// - [Self::source_location()] for [TextSize] -> [SourceLocation] #[derive(Debug, Clone, Eq, PartialEq)] pub struct SourceFile { contents: String, - index: LineIndex, + line_starts: Vec, + kind: SourceKind, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum SourceKind { + /// Optimized for an ASCII only document + Ascii, + + /// Document containing UTF-8 + Utf8, +} + +impl SourceKind { + const fn is_ascii(self) -> bool { + matches!(self, SourceKind::Ascii) + } } impl SourceFile { /// Builds the [`SourceFile`] from the contents of a file. pub fn new(contents: String) -> Self { - let index = LineIndex::from_source_text(&contents); - Self { contents, index } + let (line_starts, kind) = Self::analyze(&contents); + Self { + contents, + line_starts, + kind, + } + } + + fn analyze(contents: &str) -> (Vec, SourceKind) { + let mut line_starts: Vec = Vec::with_capacity(contents.len() / 88); + + // Always push a start for an offset of `0`, needed for an invariant in `line_number()` + line_starts.push(TextSize::from(0)); + + let mut utf8 = false; + + let bytes = contents.as_bytes(); + assert!(u32::try_from(bytes.len()).is_ok()); + + for (i, byte) in bytes.iter().enumerate() { + utf8 |= !byte.is_ascii(); + + match byte { + // Only track one line break for `\r\n`. + b'\r' if bytes.get(i + 1) == Some(&b'\n') => continue, + b'\n' | b'\r' => { + // SAFETY: Assertion above guarantees `i <= u32::MAX` + #[allow(clippy::cast_possible_truncation)] + line_starts.push(TextSize::from(i as u32) + TextSize::from(1)); + } + _ => {} + } + } + + let kind = if utf8 { + SourceKind::Utf8 + } else { + SourceKind::Ascii + }; + + (line_starts, kind) } /// Returns a reference to the contents in the source file. @@ -33,16 +213,98 @@ impl SourceFile { self.contents } - /// Replace text in the source file and rebuild the line index afterwards. + /// Replace text in the source file and reanalyze afterwards. pub fn replace_range(&mut self, range: R, replace_with: &str) where R: std::ops::RangeBounds, { self.contents.replace_range(range, replace_with); - self.index = LineIndex::from_source_text(&self.contents); + let (line_starts, kind) = Self::analyze(&self.contents); + self.line_starts = line_starts; + self.kind = kind; } - /// Returns the [SourceLocation] for an offset. + /// Returns `true` if the text only consists of ASCII characters + pub fn is_ascii(&self) -> bool { + self.kind.is_ascii() + } + + /// Return the number of lines in the source file. + pub fn line_count(&self) -> usize { + self.line_starts().len() + } + + /// Returns the row number for a given offset. + /// + /// ## Examples + /// + /// ``` + /// use biome_text_size::TextSize; + /// use source_file::{SourceFile, SourceLocation, LineNumber}; + /// + /// let source = "def a():\n pass".to_string(); + /// let source = SourceFile::new(source); + /// + /// assert_eq!(source.line_number(TextSize::from(0)), LineNumber::from(0)); + /// assert_eq!(source.line_number(TextSize::from(4)), LineNumber::from(0)); + /// assert_eq!(source.line_number(TextSize::from(13)), LineNumber::from(1)); + /// ``` + pub fn line_number(&self, offset: TextSize) -> LineNumber { + let line_number = match self.line_starts().binary_search(&offset) { + // `offset` is at the start of a line + Ok(line_number) => line_number, + Err(next_line_number) => { + // SAFETY: Safe because the line starts always contain an entry for the offset 0 + next_line_number - 1 + } + }; + + LineNumber::try_from(line_number) + .expect("Number of line starts should fit in a `LineNumber`") + } + + /// Returns the [byte offset](TextSize) for the line's start. + pub fn line_start(&self, line_number: LineNumber) -> TextSize { + let line_number = usize::from(line_number); + + if line_number >= self.line_count() { + // If asking for a line number past the last line, return last byte + self.contents().text_len() + } else { + self.line_starts()[line_number] + } + } + + /// Returns the [byte offset](TextSize) of the line's end. + /// + /// The offset is the end of the line, up to and including the newline character + /// ending the line (if any), making it equivalent to the next line's start. + pub(crate) fn line_end(&self, line_number: LineNumber) -> TextSize { + let line_number = usize::from(line_number); + + if line_number.saturating_add(1) >= self.line_count() { + // If asking for a line number past the last line, return last byte + self.contents().text_len() + } else { + self.line_starts()[line_number + 1] + } + } + + /// Returns the [`TextRange`] of the line. + /// + /// The start points to the first character's [byte offset](TextSize). The end points + /// up to, and including, the newline character ending the line (if any). This makes + /// the range a `[)` range. + pub fn line_range(&self, line_number: LineNumber) -> TextRange { + TextRange::new(self.line_start(line_number), self.line_end(line_number)) + } + + /// Returns the [byte offsets](TextSize) for every line + pub fn line_starts(&self) -> &[TextSize] { + &self.line_starts + } + + /// Returns the [SourceLocation] at the [byte offset](TextSize). /// /// ## Examples /// @@ -80,55 +342,38 @@ impl SourceFile { offset: TextSize, encoding: LineOffsetEncoding, ) -> SourceLocation { - self.index - .source_location(offset, self.contents(), encoding) - } - - /// Return the number of lines in the source code. - pub fn line_count(&self) -> usize { - self.index.line_count() - } - - /// Returns `true` if the text only consists of ASCII characters - pub fn is_ascii(&self) -> bool { - self.index.is_ascii() - } - - /// Returns the row number for a given offset. - /// - /// ## Examples - /// - /// ``` - /// use biome_text_size::TextSize; - /// use source_file::{SourceFile, SourceLocation, LineNumber}; - /// - /// let source = "def a():\n pass".to_string(); - /// let source = SourceFile::new(source); - /// - /// assert_eq!(source.line_number(TextSize::from(0)), LineNumber::from(0)); - /// assert_eq!(source.line_number(TextSize::from(4)), LineNumber::from(0)); - /// assert_eq!(source.line_number(TextSize::from(13)), LineNumber::from(1)); - /// ``` - pub fn line_number(&self, offset: TextSize) -> LineNumber { - self.index.line_number(offset) - } + let line_number = self.line_number(offset); + let line_range_up_to_offset = TextRange::new(self.line_start(line_number), offset); - /// Returns the [byte offset](TextSize) for the `line_number`'s start. - pub fn line_start(&self, line_number: LineNumber) -> TextSize { - self.index.line_start(line_number, self.contents()) - } + let line_offset = if self.is_ascii() { + LineOffset::new(line_range_up_to_offset.len().into(), encoding) + } else { + match encoding { + LineOffsetEncoding::UTF8 => { + LineOffset::new(line_range_up_to_offset.len().into(), encoding) + } + LineOffsetEncoding::UTF16 => { + let line_contents_up_to_offset = &self.contents()[line_range_up_to_offset]; + let offset = line_contents_up_to_offset + .encode_utf16() + .count() + .try_into() + .expect("A single line's `offset` should fit in a `u32`"); + LineOffset::new(offset, encoding) + } + LineOffsetEncoding::UTF32 => { + let line_contents_up_to_offset = &self.contents()[line_range_up_to_offset]; + let offset = line_contents_up_to_offset + .chars() + .count() + .try_into() + .expect("A single line's `offset` should fit in a `u32`"); + LineOffset::new(offset, encoding) + } + } + }; - /// Returns the [byte offset](TextSize) of the `line_number`'s end. - /// The offset is the end of the line, up to and including the newline character ending the line (if any). - pub fn line_end(&self, line_number: LineNumber) -> TextSize { - self.index.line_end(line_number, self.contents()) - } - - /// Returns the [`TextRange`] of the `line_number`. - /// The start points to the first character's [byte offset](TextSize), the end up to, and including - /// the newline character ending the line (if any). - pub fn line_range(&self, line_number: LineNumber) -> TextRange { - self.index.line_range(line_number, self.contents()) + SourceLocation::new(line_number, line_offset) } /// Returns the [byte offset](TextSize) at the [SourceLocation]. @@ -252,11 +497,470 @@ impl SourceFile { /// ``` /// pub fn offset(&self, source_location: SourceLocation) -> TextSize { - self.index.offset(source_location, self.contents()) + let (line_number, line_offset) = source_location.into_fields(); + + let line_range = self.line_range(line_number); + + let offset = if self.is_ascii() { + TextSize::from(line_offset.raw()) + } else { + match line_offset.encoding() { + LineOffsetEncoding::UTF8 => TextSize::from(line_offset.raw()), + LineOffsetEncoding::UTF16 => { + let n_code_units = line_offset.raw(); + let line_contents = &self.contents()[line_range]; + + let mut i = 0; + let mut offset = 0; + + for c in line_contents.chars() { + if i >= n_code_units { + break; + } + i += c.len_utf16() as u32; + offset += c.len_utf8() as u32; + } + + TextSize::from(offset) + } + LineOffsetEncoding::UTF32 => { + let n_code_units = line_offset.raw(); + let line_contents = &self.contents()[line_range]; + + let mut offset: u32 = 0; + + for c in line_contents.chars().take(n_code_units as usize) { + offset += c.len_utf8() as u32; + } + + TextSize::from(offset) + } + } + }; + + line_range.start() + offset.clamp(TextSize::from(0), line_range.len()) } +} - /// Returns the [byte offsets](TextSize) for every line - pub fn line_starts(&self) -> &[TextSize] { - self.index.line_starts() +#[cfg(test)] +mod tests { + use biome_text_size::TextSize; + + use crate::source_location::LineNumber; + use crate::source_location::LineOffset; + use crate::source_location::LineOffsetEncoding; + use crate::SourceFile; + use crate::SourceLocation; + + #[test] + fn ascii_source_file() { + let source = SourceFile::new(String::new()); + assert_eq!(source.line_starts(), &[TextSize::from(0)]); + + let source = SourceFile::new("x = 1".to_string()); + assert_eq!(source.line_starts(), &[TextSize::from(0)]); + + let source = SourceFile::new("x = 1\n".to_string()); + assert_eq!( + source.line_starts(), + &[TextSize::from(0), TextSize::from(6)] + ); + + let source = SourceFile::new("x = 1\ny = 2\nz = x + y\n".to_string()); + assert_eq!( + source.line_starts(), + &[ + TextSize::from(0), + TextSize::from(6), + TextSize::from(12), + TextSize::from(22) + ] + ); + } + + #[test] + fn ascii_source_location() { + let contents = "x = 1\ny = 2".to_string(); + let source = SourceFile::new(contents); + + // First row. + let loc = source.source_location(TextSize::from(2), LineOffsetEncoding::UTF8); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(2, LineOffsetEncoding::UTF8) + ) + ); + + // Second row. + let loc = source.source_location(TextSize::from(6), LineOffsetEncoding::UTF8); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) + ); + + let loc = source.source_location(TextSize::from(11), LineOffsetEncoding::UTF8); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(5, LineOffsetEncoding::UTF8) + ) + ); + } + + #[test] + fn ascii_carriage_return() { + let contents = "x = 4\ry = 3".to_string(); + let source = SourceFile::new(contents); + assert_eq!( + source.line_starts(), + &[TextSize::from(0), TextSize::from(6)] + ); + + assert_eq!( + source.source_location(TextSize::from(4), LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(4, LineOffsetEncoding::UTF8) + ) + ); + assert_eq!( + source.source_location(TextSize::from(6), LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) + ); + assert_eq!( + source.source_location(TextSize::from(7), LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF8) + ) + ); + } + + #[test] + fn ascii_carriage_return_newline() { + let contents = "x = 4\r\ny = 3".to_string(); + let source = SourceFile::new(contents); + assert_eq!( + source.line_starts(), + &[TextSize::from(0), TextSize::from(7)] + ); + + assert_eq!( + source.source_location(TextSize::from(4), LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(4, LineOffsetEncoding::UTF8) + ) + ); + assert_eq!( + source.source_location(TextSize::from(7), LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) + ); + assert_eq!( + source.source_location(TextSize::from(8), LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF8) + ) + ); + } + + #[test] + fn utf8_source_file() { + let source = SourceFile::new("x = '🫣'".to_string()); + assert_eq!(source.line_count(), 1); + assert_eq!(source.line_starts(), &[TextSize::from(0)]); + + let source = SourceFile::new("x = '🫣'\n".to_string()); + assert_eq!(source.line_count(), 2); + assert_eq!( + source.line_starts(), + &[TextSize::from(0), TextSize::from(11)] + ); + + let source = SourceFile::new("x = '🫣'\ny = 2\nz = x + y\n".to_string()); + assert_eq!(source.line_count(), 4); + assert_eq!( + source.line_starts(), + &[ + TextSize::from(0), + TextSize::from(11), + TextSize::from(17), + TextSize::from(27) + ] + ); + + let source = SourceFile::new("# 🫣\nclass Foo:\n \"\"\".\"\"\"".to_string()); + assert_eq!(source.line_count(), 3); + assert_eq!( + source.line_starts(), + &[TextSize::from(0), TextSize::from(7), TextSize::from(18)] + ); + } + + #[test] + fn utf8_carriage_return() { + let contents = "x = '🫣'\ry = 3".to_string(); + let source = SourceFile::new(contents); + assert_eq!(source.line_count(), 2); + assert_eq!( + source.line_starts(), + &[TextSize::from(0), TextSize::from(11)] + ); + + // Second ', UTF8 + assert_eq!( + source.source_location(TextSize::from(9), LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(9, LineOffsetEncoding::UTF8) + ) + ); + assert_eq!( + source.source_location(TextSize::from(11), LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) + ); + assert_eq!( + source.source_location(TextSize::from(12), LineOffsetEncoding::UTF8), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF8) + ) + ); + + // Second ', UTF16 + assert_eq!( + source.source_location(TextSize::from(9), LineOffsetEncoding::UTF16), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(7, LineOffsetEncoding::UTF16) + ) + ); + assert_eq!( + source.source_location(TextSize::from(11), LineOffsetEncoding::UTF16), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF16) + ) + ); + assert_eq!( + source.source_location(TextSize::from(12), LineOffsetEncoding::UTF16), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF16) + ) + ); + + // Second ', UTF32 + assert_eq!( + source.source_location(TextSize::from(9), LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(6, LineOffsetEncoding::UTF32) + ) + ); + assert_eq!( + source.source_location(TextSize::from(11), LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF32) + ) + ); + assert_eq!( + source.source_location(TextSize::from(12), LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF32) + ) + ); + } + + #[test] + fn utf8_carriage_return_newline() { + let contents = "x = '🫣'\r\ny = 3".to_string(); + let source = SourceFile::new(contents); + assert_eq!(source.line_count(), 2); + assert_eq!( + source.line_starts(), + &[TextSize::from(0), TextSize::from(12)] + ); + + // Second ' + assert_eq!( + source.source_location(TextSize::from(9), LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(6, LineOffsetEncoding::UTF32) + ) + ); + assert_eq!( + source.source_location(TextSize::from(12), LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF32) + ) + ); + assert_eq!( + source.source_location(TextSize::from(13), LineOffsetEncoding::UTF32), + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(1, LineOffsetEncoding::UTF32) + ) + ); + } + + #[test] + fn utf8_byte_offset() { + let contents = "x = '☃'\ny = 2".to_string(); + let source = SourceFile::new(contents); + assert_eq!( + source.line_starts(), + &[TextSize::from(0), TextSize::from(10)] + ); + + // First row, start + let loc = source.source_location(TextSize::from(0), LineOffsetEncoding::UTF8); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) + ); + let loc = source.source_location(TextSize::from(0), LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(0, LineOffsetEncoding::UTF16) + ) + ); + let loc = source.source_location(TextSize::from(0), LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(0, LineOffsetEncoding::UTF32) + ) + ); + + // First row, right before + let loc = source.source_location(TextSize::from(5), LineOffsetEncoding::UTF8); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(5, LineOffsetEncoding::UTF8) + ) + ); + let loc = source.source_location(TextSize::from(5), LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(5, LineOffsetEncoding::UTF16) + ) + ); + let loc = source.source_location(TextSize::from(5), LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(5, LineOffsetEncoding::UTF32) + ) + ); + + // First row, right after + let loc = source.source_location(TextSize::from(8), LineOffsetEncoding::UTF8); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(8, LineOffsetEncoding::UTF8) + ) + ); + let loc = source.source_location(TextSize::from(8), LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(6, LineOffsetEncoding::UTF16) + ) + ); + let loc = source.source_location(TextSize::from(8), LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(0), + LineOffset::new(6, LineOffsetEncoding::UTF32) + ) + ); + + // Second row, start + let loc = source.source_location(TextSize::from(10), LineOffsetEncoding::UTF8); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF8) + ) + ); + let loc = source.source_location(TextSize::from(10), LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF16) + ) + ); + let loc = source.source_location(TextSize::from(10), LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(0, LineOffsetEncoding::UTF32) + ) + ); + + // One-past-the-end. + let loc = source.source_location(TextSize::from(15), LineOffsetEncoding::UTF8); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(5, LineOffsetEncoding::UTF8) + ) + ); + let loc = source.source_location(TextSize::from(15), LineOffsetEncoding::UTF16); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(5, LineOffsetEncoding::UTF16) + ) + ); + let loc = source.source_location(TextSize::from(15), LineOffsetEncoding::UTF32); + assert_eq!( + loc, + SourceLocation::new( + LineNumber::from(1), + LineOffset::new(5, LineOffsetEncoding::UTF32) + ) + ); } } diff --git a/crates/source_file/src/source_location.rs b/crates/source_file/src/source_location.rs index 34be78b2..e31fb167 100644 --- a/crates/source_file/src/source_location.rs +++ b/crates/source_file/src/source_location.rs @@ -21,6 +21,10 @@ impl SourceLocation { pub fn line_offset(&self) -> LineOffset { self.line_offset } + + pub fn into_fields(self) -> (LineNumber, LineOffset) { + (self.line_number, self.line_offset) + } } /// A 0-indexed line number From dbd05fe3b94cd81e83670f51191f4182f021581a Mon Sep 17 00:00:00 2001 From: Davis Vaughan Date: Mon, 2 Jun 2025 15:50:49 -0400 Subject: [PATCH 44/44] Fix doc tests --- crates/source_file/src/source_file.rs | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/source_file/src/source_file.rs b/crates/source_file/src/source_file.rs index ae7ab7ac..3c887e80 100644 --- a/crates/source_file/src/source_file.rs +++ b/crates/source_file/src/source_file.rs @@ -310,30 +310,30 @@ impl SourceFile { /// /// ``` /// use biome_text_size::TextSize; - /// use source_file::{SourceFile, SourceLocation, LineNumber, LineOffset, Encoding}; + /// use source_file::{SourceFile, SourceLocation, LineNumber, LineOffset, LineOffsetEncoding}; /// /// let source = "x <- function()\n NULL".to_string(); /// let source = SourceFile::new(source); /// /// assert_eq!( - /// source.source_location(TextSize::from(0), Encoding::UTF8), + /// source.source_location(TextSize::from(0), LineOffsetEncoding::UTF8), /// SourceLocation::new( /// LineNumber::from(0), - /// LineOffset::new(0, Encoding::UTF8) + /// LineOffset::new(0, LineOffsetEncoding::UTF8) /// ) /// ); /// assert_eq!( - /// source.source_location(TextSize::from(4), Encoding::UTF8), + /// source.source_location(TextSize::from(4), LineOffsetEncoding::UTF8), /// SourceLocation::new( /// LineNumber::from(0), - /// LineOffset::new(4, Encoding::UTF8) + /// LineOffset::new(4, LineOffsetEncoding::UTF8) /// ) /// ); /// assert_eq!( - /// source.source_location(TextSize::from(20), Encoding::UTF8), + /// source.source_location(TextSize::from(20), LineOffsetEncoding::UTF8), /// SourceLocation::new( /// LineNumber::from(1), - /// LineOffset::new(4, Encoding::UTF8) + /// LineOffset::new(4, LineOffsetEncoding::UTF8) /// ) /// ); /// ``` @@ -383,7 +383,7 @@ impl SourceFile { /// ### ASCII /// /// ``` - /// use source_file::{SourceFile, SourceLocation, LineNumber, LineOffset, Encoding}; + /// use source_file::{SourceFile, SourceLocation, LineNumber, LineOffset, LineOffsetEncoding}; /// use biome_text_size::TextSize; /// /// let source = r#"a = 4 @@ -396,7 +396,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(0), - /// LineOffset::new(0, Encoding::UTF8) + /// LineOffset::new(0, LineOffsetEncoding::UTF8) /// )), /// TextSize::from(0) /// ); @@ -405,7 +405,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(1), - /// LineOffset::new(4, Encoding::UTF8) + /// LineOffset::new(4, LineOffsetEncoding::UTF8) /// )), /// TextSize::from(10) /// ); @@ -414,7 +414,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(0), - /// LineOffset::new(10, Encoding::UTF8) + /// LineOffset::new(10, LineOffsetEncoding::UTF8) /// )), /// TextSize::from(6) /// ); @@ -423,7 +423,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(3), - /// LineOffset::new(0, Encoding::UTF8) + /// LineOffset::new(0, LineOffsetEncoding::UTF8) /// )), /// TextSize::from(29) /// ); @@ -432,7 +432,7 @@ impl SourceFile { /// ### UTF8 /// /// ``` - /// use source_file::{SourceFile, SourceLocation, LineNumber, LineOffset, Encoding}; + /// use source_file::{SourceFile, SourceLocation, LineNumber, LineOffset, LineOffsetEncoding}; /// use biome_text_size::TextSize; /// /// let source = r#"a = 4 @@ -445,7 +445,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(0), - /// LineOffset::new(0, Encoding::UTF8) + /// LineOffset::new(0, LineOffsetEncoding::UTF8) /// )), /// TextSize::from(0) /// ); @@ -454,7 +454,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(2), - /// LineOffset::new(1, Encoding::UTF8) + /// LineOffset::new(1, LineOffsetEncoding::UTF8) /// )), /// TextSize::from(20) /// ); @@ -463,7 +463,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(2), - /// LineOffset::new(1, Encoding::UTF16) + /// LineOffset::new(1, LineOffsetEncoding::UTF16) /// )), /// TextSize::from(20) /// ); @@ -472,7 +472,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(1), - /// LineOffset::new(10, Encoding::UTF8) + /// LineOffset::new(10, LineOffsetEncoding::UTF8) /// )), /// TextSize::from(16) /// ); @@ -481,7 +481,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(1), - /// LineOffset::new(10, Encoding::UTF32) + /// LineOffset::new(10, LineOffsetEncoding::UTF32) /// )), /// TextSize::from(19) /// ); @@ -490,7 +490,7 @@ impl SourceFile { /// assert_eq!( /// source.offset(SourceLocation::new( /// LineNumber::from(3), - /// LineOffset::new(0, Encoding::UTF32) + /// LineOffset::new(0, LineOffsetEncoding::UTF32) /// )), /// TextSize::from(24) /// );