From 214f9d1f8cc0a9f9dd9fc543e69d16144c4b0214 Mon Sep 17 00:00:00 2001 From: Ashar Date: Sun, 2 Aug 2026 20:51:47 +0530 Subject: [PATCH 1/3] chore: pedantic clippy, cleanup and workspace sympbol fast impl Signed-off-by: Ashar --- Cargo.toml | 3 + build.rs | 3 +- src/cli.rs | 2 +- ...s__config__workspace__test__workspace.snap | 14 -- src/config/workspace.rs | 6 +- src/formatter/clang.rs | 10 +- src/log.rs | 16 +- src/lsp.rs | 76 +++++---- src/model/presentation.rs | 21 ++- src/parser/definition.rs | 46 +++--- src/parser/diagnostics.rs | 2 +- src/parser/docsymbol.rs | 32 +--- src/parser/hover.rs | 13 +- src/parser/rename.rs | 15 +- ...parser__rename__test__rename_fields-2.snap | 12 -- ...parser__rename__test__rename_fields-3.snap | 5 - ...__parser__rename__test__rename_fields.snap | 28 ---- src/parser/tree.rs | 16 +- src/protoc.rs | 148 ++++++++--------- src/server.rs | 36 ++--- src/state.rs | 152 +++++++++--------- src/transport.rs | 31 ++-- src/utils.rs | 12 +- src/workspace/definition.rs | 6 +- src/workspace/hover.rs | 8 +- src/workspace/rename.rs | 71 +++----- ..._workspace__rename__test__reference-3.snap | 13 -- ...rkspace_symbol__test__address_symbols.snap | 28 ---- ...__workspace_symbol__test__all_symbols.snap | 61 ------- ...orkspace_symbol__test__author_symbols.snap | 26 --- src/workspace/workspace_symbol.rs | 6 +- 31 files changed, 333 insertions(+), 585 deletions(-) delete mode 100644 src/config/snapshots/protols__config__workspace__test__workspace.snap delete mode 100644 src/parser/snapshots/protols__parser__rename__test__rename_fields-2.snap delete mode 100644 src/parser/snapshots/protols__parser__rename__test__rename_fields-3.snap delete mode 100644 src/parser/snapshots/protols__parser__rename__test__rename_fields.snap delete mode 100644 src/workspace/snapshots/protols__workspace__rename__test__reference-3.snap delete mode 100644 src/workspace/snapshots/protols__workspace__workspace_symbol__test__address_symbols.snap delete mode 100644 src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap delete mode 100644 src/workspace/snapshots/protols__workspace__workspace_symbol__test__author_symbols.snap diff --git a/Cargo.toml b/Cargo.toml index 21f61e6..f420b58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,3 +39,6 @@ insta = { version = "1.47", features = ["yaml", "redactions"] } strip = "debuginfo" # Removes heavy debug data but keeps function names for panic logs lto = true # Enables Link-Time Optimization for cross-crate improvements codegen-units = 1 # Maximizes LLVM optimization passes + +[lints.clippy] +pedantic = "warn" diff --git a/build.rs b/build.rs index ec80ab6..30447d9 100644 --- a/build.rs +++ b/build.rs @@ -5,7 +5,8 @@ fn main() { let path = Path::new(path); assert!( path.is_absolute(), - "Environment variable FALLBACK_INCLUDE_PATH must be absolute: {path:?}" + "Environment variable FALLBACK_INCLUDE_PATH must be absolute: {}", + path.display() ); } } diff --git a/src/cli.rs b/src/cli.rs index ef45914..3cf95f7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -71,7 +71,7 @@ impl Cli { /// /// This method collects all values from the `--include-paths` flags (including /// multiple occurrences and comma-separated values) and converts them into - /// a vector of [std::path::PathBuf]. Returns an empty vector if no paths are provided. + /// a vector of [`std::path::PathBuf`]. Returns an empty vector if no paths are provided. pub fn get_include_paths(&self) -> Vec { self.include_paths .as_ref() diff --git a/src/config/snapshots/protols__config__workspace__test__workspace.snap b/src/config/snapshots/protols__config__workspace__test__workspace.snap deleted file mode 100644 index 1438bb3..0000000 --- a/src/config/snapshots/protols__config__workspace__test__workspace.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: src/config/workspace.rs -expression: ws.get_config_for_uri(&inworkspace) ---- -config: - include_paths: - - foobar - - bazbaaz - single_file_mode: false - disable_parse_diagnostics: true - experimental: - use_protoc_diagnostics: true -formatter: - clang_format_path: /usr/bin/clang-format diff --git a/src/config/workspace.rs b/src/config/workspace.rs index f86aaed..5eb8f28 100644 --- a/src/config/workspace.rs +++ b/src/config/workspace.rs @@ -52,7 +52,7 @@ impl WorkspaceProtoConfigs { let p = Path::new(&wpath).join(file); match std::fs::exists(&p) { Ok(exists) if exists => return Some(p), - _ => continue, + _ => {} } } None @@ -128,7 +128,7 @@ impl WorkspaceProtoConfigs { } } - ipath.push(w.to_path_buf()); + ipath.push(w.clone()); ipath.extend_from_slice(&self.protoc_include_prefix); ipath.extend_from_slice(self.fallback_include_path.as_slice()); Some(ipath) @@ -145,7 +145,7 @@ impl WorkspaceProtoConfigs { if let Ok(cdir) = env::current_dir() && let Some(drive) = cdir.components().next() { - d = drive.as_os_str().to_string_lossy().to_string() + d = drive.as_os_str().to_string_lossy().to_string(); } format!("{d}://") } else { diff --git a/src/formatter/clang.rs b/src/formatter/clang.rs index 12911f8..df35b2f 100644 --- a/src/formatter/clang.rs +++ b/src/formatter/clang.rs @@ -59,8 +59,8 @@ impl Replacement<'_> { let character = text_after_newline.encode_utf16().count(); Some(Position { - line: line as u32, - character: character as u32, + line: u32::try_from(line).ok()?, + character: u32::try_from(character).ok()?, }) } @@ -104,7 +104,7 @@ impl ClangFormatter { Some(c) } - fn output_to_textedit(&self, output: &str, content: &str) -> Option> { + fn output_to_textedit(output: &str, content: &str) -> Option> { let r = Replacements::from_str(output).ok()?; let edits = r .replacements @@ -128,7 +128,7 @@ impl ProtoFormatter for ClangFormatter { ); return None; } - self.output_to_textedit(&String::from_utf8_lossy(&output.stdout), content) + Self::output_to_textedit(&String::from_utf8_lossy(&output.stdout), content) } fn format_document_range( @@ -153,7 +153,7 @@ impl ProtoFormatter for ClangFormatter { ); return None; } - self.output_to_textedit(&String::from_utf8_lossy(&output.stdout), content) + Self::output_to_textedit(&String::from_utf8_lossy(&output.stdout), content) } } diff --git a/src/log.rs b/src/log.rs index 379386a..610f384 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,4 +1,5 @@ use async_lsp::lsp_types::{LogMessageParams, MessageType, TraceValue}; +use std::fmt::Write; use tokio::sync::mpsc; use tracing::{ Level, Subscriber, @@ -26,19 +27,17 @@ impl Visit for MessageVisitor { if field.name() == "message" { self.message.push_str(value); } else { - self.fields - .push_str(&format!(" {}={}", field.name(), value)); + let _ = write!(self.fields, " {}={value}", field.name()); } } fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { if field.name() == "message" { if self.message.is_empty() { - self.message = format!("{:?}", value); + self.message = format!("{value:?}"); } } else { - self.fields - .push_str(&format!(" {}={:?}", field.name(), value)); + let _ = write!(self.fields, " {}={value:?}", field.name()); } } } @@ -75,13 +74,12 @@ impl Layer for ClientLogger { format!("{} | fields:{}", visitor.message, visitor.fields) }; - let message = format!("[{}] {}", target, full_text); + let message = format!("[{target}] {full_text}"); let typ = match *metadata.level() { tracing::Level::ERROR => MessageType::ERROR, tracing::Level::WARN => MessageType::WARNING, tracing::Level::INFO => MessageType::INFO, - tracing::Level::DEBUG => MessageType::LOG, _ => MessageType::LOG, }; @@ -108,7 +106,7 @@ pub fn install(tx: mpsc::Sender) -> (LogReloadHandle, WorkerGu let lsp_layer = ClientLogger { tx }; let dir = std::env::temp_dir(); - eprintln!("file logging at directory: {dir:?}"); + eprintln!("file logging at directory: {}", dir.display()); let file_appender = tracing_appender::rolling::daily(dir, "protols.log"); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); @@ -150,7 +148,7 @@ pub fn update_level(handle: &LogReloadHandle, value: TraceValue) { // Construct directives: "warn" for the whole world, "pkg=level" for us let global_directive = Level::WARN.into(); - let pkg_directive = format!("{}={}", pkg_name, level) + let pkg_directive = format!("{pkg_name}={level}") .parse::() .expect("Failed to parse log directive"); diff --git a/src/lsp.rs b/src/lsp.rs index 04180a0..e878040 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -12,8 +12,8 @@ use async_lsp::lsp_types::{ InitializeParams, InitializeResult, Location, MarkupContent, MarkupKind, OneOf, PrepareRenameResponse, ReferenceParams, RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities, ServerInfo, SetTraceParams, TextDocumentPositionParams, - TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit, - WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, + TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, WorkDoneProgressOptions, + WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, WorkspaceSymbolParams, WorkspaceSymbolResponse, }; use async_lsp::{Error, LanguageClient, ResponseError}; @@ -33,8 +33,9 @@ impl ProtoLanguageServer { let (cname, version) = params .client_info .as_ref() - .map(|c| (c.name.as_str(), c.version.as_deref())) - .unwrap_or(("", None)); + .map_or(("", None), |c| { + (c.name.as_str(), c.version.as_deref()) + }); let cversion = version.unwrap_or(""); @@ -83,10 +84,10 @@ impl ProtoLanguageServer { did_delete: Some(file_registration_option.clone()), ..Default::default() }), - }) + }); } else { tracing::info!("running in no workspace mode"); - self.configs.no_workspace_mode() + self.configs.no_workspace_mode(); } let mut rename_provider: OneOf = OneOf::Left(true); @@ -100,8 +101,8 @@ impl ProtoLanguageServer { { rename_provider = OneOf::Right(RenameOptions { prepare_provider: Some(true), - work_done_progress_options: Default::default(), - }) + work_done_progress_options: WorkDoneProgressOptions::default(), + }); } let response = InitializeResult { @@ -129,6 +130,20 @@ impl ProtoLanguageServer { }), }; + // Phase 2: Index all configured workspaces once at startup. This + // populates the in-memory metamodel pool that `workspace/symbol` + // queries against, keeping per-request symbol lookups free of + // on-the-fly workspace re-scans and re-parses. + let workspace_paths: Vec = self + .configs + .get_workspaces() + .into_iter() + .filter_map(|workspace| workspace.to_file_path().ok()) + .collect(); + for workspace_path in workspace_paths { + self.state.parse_all_from_workspace(&workspace_path, None); + } + Box::pin(async move { Ok(response) }) } @@ -200,14 +215,14 @@ impl ProtoLanguageServer { } if let Some(ipath) = self.configs.get_include_paths(&uri) { - for import in tree.get_import_paths(content.as_bytes()).iter() { + for import in &tree.get_import_paths(content.as_bytes()) { if let Some(p) = ipath.iter().map(|p| p.join(import)).find(|p| p.exists()) && let Ok(uri) = Url::from_file_path(p.clone()) { completions.extend(self.state.completion_items_for_tree(&uri)); } } - }; + } } Box::pin(async move { Ok(Some(CompletionResponse::Array(completions))) }) } @@ -283,8 +298,7 @@ impl ProtoLanguageServer { let chain_rpc_request_response = self .configs .get_config_for_uri(&uri) - .map(|c| c.config.rename.chain_rpc_request_response) - .unwrap_or_default(); + .is_some_and(|c| c.config.rename.chain_rpc_request_response); let ops = self.state.compute_rename_ops( &decl_uri, @@ -295,7 +309,7 @@ impl ProtoLanguageServer { ); let Some(all_edits) = self .state - .apply_rename_ops(&ops, workspace_path, progress_sender) + .apply_rename_ops(&ops, &workspace_path, progress_sender) else { error!(uri=%decl_uri, "failed to apply primary rename"); return Box::pin(async move { Ok(None) }); @@ -345,8 +359,8 @@ impl ProtoLanguageServer { if let Some(v) = self.state.reference_fields( current_package, &otext, - workspace.to_file_path().unwrap(), - progress_sender, + &workspace.to_file_path().unwrap(), + progress_sender.as_ref(), ) { refs.extend(v); } @@ -412,24 +426,15 @@ impl ProtoLanguageServer { Box::pin(async move { Ok(Some(response)) }) } + #[allow(clippy::needless_pass_by_value)] pub(super) fn workspace_symbol( &mut self, params: WorkspaceSymbolParams, ) -> BoxFuture<'static, Result, ResponseError>> { let query = params.query.to_lowercase(); - let work_done_token = params.work_done_progress_params.work_done_token; - - // Parse all files from all workspaces - let workspaces = self.configs.get_workspaces(); - let progress_sender = work_done_token.map(|token| self.with_report_progress(token)); - - for workspace in workspaces { - if let Ok(workspace_path) = workspace.to_file_path() { - self.state - .parse_all_from_workspace(workspace_path, progress_sender.clone()); - } - } + // Workspaces are indexed once at startup; symbol lookups are now a + // pure in-memory substring scan over the cached metamodel pool. let symbols = self.state.find_workspace_symbols(&query); Box::pin(async move { @@ -488,10 +493,10 @@ impl ProtoLanguageServer { if let Some(diagnostics) = self.state - .upsert_file(&uri, content, &ipath, 8, &pconf.config, true) + .upsert_file(&uri, &content, &ipath, 8, &pconf.config, true) && let Err(e) = self.client.publish_diagnostics(diagnostics) { - error!(error=%e, "failed to publish diagnostics") + error!(error=%e, "failed to publish diagnostics"); } ControlFlow::Continue(()) } @@ -513,10 +518,10 @@ impl ProtoLanguageServer { if let Some(diagnostics) = self.state - .upsert_file(&uri, content, &ipath, 8, &pconf.config, true) + .upsert_file(&uri, &content, &ipath, 8, &pconf.config, true) && let Err(e) = self.client.publish_diagnostics(diagnostics) { - error!(error=%e, "failed to publish diagnostics") + error!(error=%e, "failed to publish diagnostics"); } ControlFlow::Continue(()) } @@ -538,10 +543,10 @@ impl ProtoLanguageServer { if let Some(diagnostics) = self.state - .upsert_file(&uri, content, &ipath, 8, &pconf.config, false) + .upsert_file(&uri, &content, &ipath, 8, &pconf.config, false) && let Err(e) = self.client.publish_diagnostics(diagnostics) { - error!(error=%e, "failed to publish diagnostics") + error!(error=%e, "failed to publish diagnostics"); } ControlFlow::Continue(()) } @@ -556,7 +561,7 @@ impl ProtoLanguageServer { let content = read_to_string(uri.to_file_path().unwrap()).unwrap_or_default(); if let Some(ipath) = self.configs.get_include_paths(&uri) { - self.state.upsert_content(&uri, content, &ipath, 2); + self.state.upsert_content(&uri, &content, &ipath, 2); } } } @@ -598,6 +603,7 @@ impl ProtoLanguageServer { } /// Handles the `$/setTrace` notification to dynamically update log verbosity. + #[allow(clippy::needless_pass_by_value)] pub(super) fn set_trace(&mut self, params: SetTraceParams) -> ControlFlow> { log::update_level(&self.log_handle, params.value); @@ -615,7 +621,7 @@ impl ProtoLanguageServer { } } -/// Parse include_paths from initialization options +/// Parse `include_paths` from initialization options fn parse_init_include_paths(init_options: &Value) -> Option> { let mut result = vec![]; let paths = init_options["include_paths"].as_array()?; diff --git a/src/model/presentation.rs b/src/model/presentation.rs index 8ec6c2d..ca7f72d 100644 --- a/src/model/presentation.rs +++ b/src/model/presentation.rs @@ -7,12 +7,31 @@ use std::fmt; use std::fmt::Write; -use async_lsp::lsp_types::Position; +use async_lsp::lsp_types::{Position, SymbolKind}; use crate::docs; use super::types::{CardinalityKind, ElementKind, ModelElement, TypeReference}; +impl From<&ElementKind> for SymbolKind { + /// Maps an internal [`ElementKind`] variant directly to its closest + /// semantic LSP [`SymbolKind`]. + fn from(kind: &ElementKind) -> Self { + match kind { + ElementKind::Import { .. } => Self::MODULE, + ElementKind::Message { .. } => Self::STRUCT, + ElementKind::Oneof { .. } => Self::OBJECT, + ElementKind::Field { .. } + | ElementKind::MapField { .. } + | ElementKind::OneofField { .. } => Self::FIELD, + ElementKind::Enum { .. } => Self::ENUM, + ElementKind::EnumValue { .. } => Self::ENUM_MEMBER, + ElementKind::Service { .. } => Self::INTERFACE, + ElementKind::Rpc { .. } => Self::METHOD, + } + } +} + impl ModelElement { const DEPRECATED_BANNER: &'static str = "**`Deprecated`**\n"; const CODE_BLOCK_START: &'static str = "```protobuf\n"; diff --git a/src/parser/definition.rs b/src/parser/definition.rs index 4e251bd..8f992a3 100644 --- a/src/parser/definition.rs +++ b/src/parser/definition.rs @@ -23,36 +23,28 @@ impl ParsedTree { return; } - match identifier.split_once('.') { - Some((parent_identifier, remaining)) => { - let child_node = self - .find_all_nodes_from(n, NodeKind::is_userdefined) - .into_iter() - .find(|n| { - n.utf8_text(content.as_ref()).expect("utf8-parse error") - == parent_identifier - }) - .and_then(|n| n.parent()); + if let Some((parent_identifier, remaining)) = identifier.split_once('.') { + let child_node = Self::find_all_nodes_from(n, NodeKind::is_userdefined) + .into_iter() + .find(|n| { + n.utf8_text(content.as_ref()).expect("utf8-parse error") == parent_identifier + }) + .and_then(|n| n.parent()); - if let Some(inner) = child_node { - self.definition_impl(remaining, inner, v, content); - } + if let Some(inner) = child_node { + self.definition_impl(remaining, inner, v, content); } - None => { - let locations: Vec = self - .find_all_nodes_from(n, NodeKind::is_userdefined) - .into_iter() - .filter(|n| { - n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier - }) - .map(|n| Location { - uri: self.uri.clone(), - range: to_lsp_range(n), - }) - .collect(); + } else { + let locations: Vec = Self::find_all_nodes_from(n, NodeKind::is_userdefined) + .into_iter() + .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier) + .map(|n| Location { + uri: self.uri.clone(), + range: to_lsp_range(n), + }) + .collect(); - v.extend(locations); - } + v.extend(locations); } } } diff --git a/src/parser/diagnostics.rs b/src/parser/diagnostics.rs index 04f831d..481a310 100644 --- a/src/parser/diagnostics.rs +++ b/src/parser/diagnostics.rs @@ -21,7 +21,7 @@ impl ParsedTree { pub fn collect_import_diagnostics( &self, content: &[u8], - import: Vec, + import: &[&str], ) -> Vec { self.get_import_path_range(content, import) .into_iter() diff --git a/src/parser/docsymbol.rs b/src/parser/docsymbol.rs index 2c81ffb..373e202 100644 --- a/src/parser/docsymbol.rs +++ b/src/parser/docsymbol.rs @@ -11,25 +11,6 @@ use crate::model::{ElementKind, ElementMeta, ModelElement}; use super::ParsedTree; -impl From<&ElementKind> for SymbolKind { - /// Maps an internal [`ElementKind`] variant directly to its closest - /// semantic LSP [`SymbolKind`]. - fn from(kind: &ElementKind) -> Self { - match kind { - ElementKind::Import { .. } => Self::MODULE, - ElementKind::Message { .. } => Self::STRUCT, - ElementKind::Oneof { .. } => Self::OBJECT, - ElementKind::Field { .. } - | ElementKind::MapField { .. } - | ElementKind::OneofField { .. } => Self::FIELD, - ElementKind::Enum { .. } => Self::ENUM, - ElementKind::EnumValue { .. } => Self::ENUM_MEMBER, - ElementKind::Service { .. } => Self::INTERFACE, - ElementKind::Rpc { .. } => Self::METHOD, - } - } -} - impl ParsedTree { /// Compiles a fully resolved hierarchical tree of document symbols from the /// internal flat elements registry. @@ -188,14 +169,7 @@ mod test { let ipath = vec![]; let mut state = ProtoLanguageState::new(); - state.upsert_file( - &uri, - contents.to_string(), - &ipath, - 3, - &Config::default(), - false, - ); + state.upsert_file(&uri, contents, &ipath, 3, &Config::default(), false); state .get_tree(&uri) @@ -237,7 +211,7 @@ mod test { let ipath = vec![]; let mut state = ProtoLanguageState::new(); - state.upsert_file(&uri, String::new(), &ipath, 3, &Config::default(), false); + state.upsert_file(&uri, "", &ipath, 3, &Config::default(), false); let symbols = state .get_tree(&uri) @@ -249,7 +223,7 @@ mod test { let mut state_minimal = ProtoLanguageState::new(); state_minimal.upsert_file( &uri, - "syntax = \"proto3\";\npackage com.test;".to_string(), + "syntax = \"proto3\";\npackage com.test;", &ipath, 3, &Config::default(), diff --git a/src/parser/hover.rs b/src/parser/hover.rs index 1bf959b..ed36337 100644 --- a/src/parser/hover.rs +++ b/src/parser/hover.rs @@ -58,14 +58,7 @@ mod test { let ipath = vec![]; let mut state = ProtoLanguageState::new(); - state.upsert_file( - &uri, - contents.to_string(), - &ipath, - 3, - &Config::default(), - false, - ); + state.upsert_file(&uri, contents, &ipath, 3, &Config::default(), false); let mut hover_results = Vec::new(); @@ -158,7 +151,7 @@ mod test { let ipath = vec![]; let mut state = ProtoLanguageState::new(); - state.upsert_file(&uri, String::new(), &ipath, 3, &Config::default(), false); + state.upsert_file(&uri, "", &ipath, 3, &Config::default(), false); let pos = Position { line: 0, @@ -169,7 +162,7 @@ mod test { let mut state_minimal = ProtoLanguageState::new(); state_minimal.upsert_file( &uri, - "syntax = \"proto3\";\npackage com.test;".to_string(), + "syntax = \"proto3\";\npackage com.test;", &ipath, 3, &Config::default(), diff --git a/src/parser/rename.rs b/src/parser/rename.rs index 8c4def8..56d8148 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -134,13 +134,12 @@ impl ParsedTree { } fn nodes_within<'a>( - &self, n: Node<'a>, identifier: &str, content: impl AsRef<[u8]>, ) -> Option>> { n.parent().map(|p| { - self.find_all_nodes_from(p, NodeKind::is_field_name) + Self::find_all_nodes_from(p, NodeKind::is_field_name) .into_iter() .filter(|i| i.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier) .collect() @@ -164,14 +163,14 @@ impl ParsedTree { let mut otext = nodes.first()?.utf8_text(content.as_ref()).ok()?.to_owned(); while nodes.len() > i { let id = nodes[i].utf8_text(content.as_ref()).ok()?; - if let Some(inodes) = self.nodes_within(nodes[i], &otext, content.as_ref()) { + if let Some(inodes) = Self::nodes_within(nodes[i], &otext, content.as_ref()) { res.extend(inodes.into_iter().map(|n| Location { uri: self.uri.clone(), range: to_lsp_range(n), - })) + })); } otext = format!("{id}.{otext}"); - i += 1 + i += 1; } Some((res, otext)) } @@ -218,17 +217,17 @@ impl ParsedTree { while nodes.len() > i { let id = nodes[i].utf8_text(content.as_ref()).ok()?; - if let Some(inodes) = self.nodes_within(nodes[i], &otext, content.as_ref()) { + if let Some(inodes) = Self::nodes_within(nodes[i], &otext, content.as_ref()) { v.extend(inodes.into_iter().map(|n| TextEdit { range: to_lsp_range(n), - new_text: ntext.to_owned(), + new_text: ntext.clone(), })); } otext = format!("{id}.{otext}"); ntext = format!("{id}.{ntext}"); - i += 1 + i += 1; } Some((v, otext, ntext)) diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_fields-2.snap b/src/parser/snapshots/protols__parser__rename__test__rename_fields-2.snap deleted file mode 100644 index 3cf596e..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_fields-2.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "tree.rename_fields(\"Book.Author\", \"Writer\", contents)" ---- -- range: - start: - line: 21 - character: 4 - end: - line: 21 - character: 15 - newText: Book.Writer diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_fields-3.snap b/src/parser/snapshots/protols__parser__rename__test__rename_fields-3.snap deleted file mode 100644 index 02ded32..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_fields-3.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "tree.rename_fields(\"xyz.abc\", \"Doesn't matter\", contents)" ---- -[] diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_fields.snap b/src/parser/snapshots/protols__parser__rename__test__rename_fields.snap deleted file mode 100644 index 0d62b54..0000000 --- a/src/parser/snapshots/protols__parser__rename__test__rename_fields.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: src/parser/rename.rs -expression: "tree.rename_fields(\"Book\", \"Kitab\", contents)" ---- -- range: - start: - line: 20 - character: 13 - end: - line: 20 - character: 17 - newText: Kitab -- range: - start: - line: 21 - character: 4 - end: - line: 21 - character: 15 - newText: Kitab.Author -- range: - start: - line: 25 - character: 32 - end: - line: 25 - character: 36 - newText: Kitab diff --git a/src/parser/tree.rs b/src/parser/tree.rs index 0bd9fcc..5036547 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -53,7 +53,7 @@ impl ParsedTree { let n = self.get_node_at_position(pos)?; // If node is import path. return the whole path, removing the quotes - if n.parent().filter(NodeKind::is_import_path).is_some() { + if n.parent().as_ref().is_some_and(NodeKind::is_import_path) { return Some(Jumpable::Import( n.utf8_text(content) .expect("utf-8 parse error") @@ -79,7 +79,7 @@ impl ParsedTree { while let Some(p) = n.parent() { if NodeKind::is_message(&p) { for i in 0..p.child_count() { - let t = p.child(i as u32).unwrap(); + let t = p.child(u32::try_from(i).unwrap()).unwrap(); if NodeKind::is_message_name(&t) { nodes.push(t); } @@ -108,19 +108,19 @@ impl ParsedTree { } pub fn find_all_nodes(&self, f: fn(&Node) -> bool) -> Vec> { - self.find_all_nodes_from(self.tree.root_node(), f) + Self::find_all_nodes_from(self.tree.root_node(), f) } - pub fn find_all_nodes_from<'a>(&self, n: Node<'a>, f: fn(&Node) -> bool) -> Vec> { + pub fn find_all_nodes_from(n: Node<'_>, f: fn(&Node) -> bool) -> Vec> { let mut cursor = n.walk(); Self::walk_and_filter(&mut cursor, f, false) } pub fn find_first_node(&self, f: fn(&Node) -> bool) -> Vec> { - self.find_node_from(self.tree.root_node(), f) + Self::find_node_from(self.tree.root_node(), f) } - pub fn find_node_from<'a>(&self, n: Node<'a>, f: fn(&Node) -> bool) -> Vec> { + pub fn find_node_from(n: Node<'_>, f: fn(&Node) -> bool) -> Vec> { let mut cursor = n.walk(); Self::walk_and_filter(&mut cursor, f, true) } @@ -149,7 +149,7 @@ impl ParsedTree { .collect() } - pub fn get_import_path_range(&self, content: &[u8], import: Vec) -> Vec { + pub fn get_import_path_range(&self, content: &[u8], import: &[&str]) -> Vec { self.get_import_node() .into_iter() .filter(|n| { @@ -157,7 +157,7 @@ impl ParsedTree { .utf8_text(content) .expect("utf8-parse error") .trim_matches('"'); - import.iter().any(|i| i == t) + import.contains(&t) }) .map(to_lsp_range) .collect() diff --git a/src/protoc.rs b/src/protoc.rs index 9d2c74b..247bb74 100644 --- a/src/protoc.rs +++ b/src/protoc.rs @@ -5,85 +5,76 @@ use tree_sitter::Point; use crate::utils::to_lsp_position; -pub struct ProtocDiagnostics {} - -impl ProtocDiagnostics { - pub fn new() -> Self { - Self {} +pub fn collect_diagnostics( + protoc_path: &str, + file_path: &str, + include_paths: &[String], +) -> Vec { + let mut cmd = Command::new(protoc_path); + + // Add include paths + for path in include_paths { + cmd.arg("-I").arg(path); } - pub fn collect_diagnostics( - &self, - protoc_path: &str, - file_path: &str, - include_paths: &[String], - ) -> Vec { - let mut cmd = Command::new(protoc_path); - - // Add include paths - for path in include_paths { - cmd.arg("-I").arg(path); - } + // Generate descriptor but discard its output + cmd.arg("-o") + .arg(if cfg!(windows) { "NUL" } else { "/dev/null" }); - // Generate descriptor but discard its output - cmd.arg("-o") - .arg(if cfg!(windows) { "NUL" } else { "/dev/null" }); - - // Add the file to check - cmd.arg(file_path); - - // Run protoc and capture output - match cmd.output() { - Ok(output) => { - if !output.status.success() { - let error = String::from_utf8_lossy(&output.stderr); - self.parse_protoc_output(&error) - } else { - Vec::new() - } - } - Err(e) => { - tracing::error!(error=%e, "failed to run protoc"); + // Add the file to check + cmd.arg(file_path); + + // Run protoc and capture output + match cmd.output() { + Ok(output) => { + if output.status.success() { Vec::new() + } else { + let error = String::from_utf8_lossy(&output.stderr); + parse_protoc_output(&error) } } + Err(e) => { + tracing::error!(error=%e, "failed to run protoc"); + Vec::new() + } } +} - // Visible for testing - fn parse_protoc_output(&self, output: &str) -> Vec { - let mut diagnostics = Vec::new(); - - for line in output.lines() { - // Parse protoc error format: file:line:column: message - if let Some((file_info, message)) = line.split_once(": ") { - let parts: Vec<&str> = file_info.split(':').collect(); - if parts.len() >= 3 - && let (Ok(line), Ok(col)) = (parts[1].parse::(), parts[2].parse::()) - { - let point = Point { - row: (line - 1) as usize, - column: (col - 1) as usize, - }; - let diagnostic = Diagnostic { - range: Range { - start: to_lsp_position(point), - end: to_lsp_position(Point { - row: point.row, - column: point.column + 1, - }), - }, - severity: Some(DiagnosticSeverity::ERROR), - source: Some("protoc".to_string()), - message: message.to_string(), - ..Default::default() - }; - diagnostics.push(diagnostic); - } +// Visible for testing +fn parse_protoc_output(output: &str) -> Vec { + let mut diagnostics = Vec::new(); + + for line in output.lines() { + // Parse protoc error format: file:line:column: message + if let Some((file_info, message)) = line.split_once(": ") { + let parts: Vec<&str> = file_info.split(':').collect(); + if parts.len() >= 3 + && let (Ok(line), Ok(col)) = (parts[1].parse::(), parts[2].parse::()) + { + let point = Point { + row: (line - 1) as usize, + column: (col - 1) as usize, + }; + let diagnostic = Diagnostic { + range: Range { + start: to_lsp_position(point), + end: to_lsp_position(Point { + row: point.row, + column: point.column + 1, + }), + }, + severity: Some(DiagnosticSeverity::ERROR), + source: Some("protoc".to_string()), + message: message.to_string(), + ..Default::default() + }; + diagnostics.push(diagnostic); } } - - diagnostics } + + diagnostics } #[cfg(test)] @@ -92,9 +83,8 @@ mod test { #[test] fn test_parse_protoc_output_single_error() { - let d = ProtocDiagnostics::new(); let output = "foo.proto:5:3: Expected field name.\n"; - let diags = d.parse_protoc_output(output); + let diags = parse_protoc_output(output); assert_eq!(diags.len(), 1); assert_eq!(diags[0].message, "Expected field name."); assert_eq!(diags[0].source, Some("protoc".to_string())); @@ -107,9 +97,8 @@ mod test { #[test] fn test_parse_protoc_output_multiple_errors() { - let d = ProtocDiagnostics::new(); let output = "a.proto:1:1: Syntax error.\nb.proto:2:3: Unknown type.\n"; - let diags = d.parse_protoc_output(output); + let diags = parse_protoc_output(output); assert_eq!(diags.len(), 2); assert_eq!(diags[0].message, "Syntax error."); assert_eq!(diags[1].message, "Unknown type."); @@ -117,40 +106,35 @@ mod test { #[test] fn test_parse_protoc_output_empty() { - let d = ProtocDiagnostics::new(); - assert!(d.parse_protoc_output("").is_empty()); + assert!(parse_protoc_output("").is_empty()); } #[test] fn test_parse_protoc_output_malformed_line() { - let d = ProtocDiagnostics::new(); let output = "not a valid protoc error line\n"; - let diags = d.parse_protoc_output(output); + let diags = parse_protoc_output(output); assert!(diags.is_empty()); } #[test] fn test_parse_protoc_output_partial_format() { - let d = ProtocDiagnostics::new(); // Missing column number let output = "foo.proto:5: Expected field name.\n"; - let diags = d.parse_protoc_output(output); + let diags = parse_protoc_output(output); assert!(diags.is_empty()); } #[test] fn test_parse_protoc_output_non_numeric_line_col() { - let d = ProtocDiagnostics::new(); let output = "foo.proto:abc:def: some message\n"; - let diags = d.parse_protoc_output(output); + let diags = parse_protoc_output(output); assert!(diags.is_empty()); } #[test] fn test_parse_protoc_output_message_with_colons() { - let d = ProtocDiagnostics::new(); let output = "foo.proto:3:1: 'Foo' is not defined. It could be a typo for 'Bar'.\n"; - let diags = d.parse_protoc_output(output); + let diags = parse_protoc_output(output); assert_eq!(diags.len(), 1); assert_eq!( diags[0].message, diff --git a/src/server.rs b/src/server.rs index e2dc2ce..3115e24 100644 --- a/src/server.rs +++ b/src/server.rs @@ -61,28 +61,28 @@ impl ProtoLanguageServer { }); // Handling request - router.request::(|st, params| st.initialize(params)); - router.request::(|st, params| st.shutdown(params)); + router.request::(ProtoLanguageServer::initialize); + router.request::(ProtoLanguageServer::shutdown); router.request::(|st, params| st.hover(params)); - router.request::(|st, params| st.completion(params)); - router.request::(|st, params| st.prepare_rename(params)); - router.request::(|st, params| st.rename(params)); - router.request::(|st, params| st.references(params)); - router.request::(|st, params| st.definition(params)); + router.request::(ProtoLanguageServer::completion); + router.request::(ProtoLanguageServer::prepare_rename); + router.request::(ProtoLanguageServer::rename); + router.request::(ProtoLanguageServer::references); + router.request::(ProtoLanguageServer::definition); router.request::(|st, params| st.document_symbol(params)); - router.request::(|st, params| st.workspace_symbol(params)); - router.request::(|st, params| st.formatting(params)); - router.request::(|st, params| st.range_formatting(params)); + router.request::(ProtoLanguageServer::workspace_symbol); + router.request::(ProtoLanguageServer::formatting); + router.request::(ProtoLanguageServer::range_formatting); // Handling notification - router.notification::(|st, params| st.set_trace(params)); - router.notification::(|st, params| st.did_save(params)); - router.notification::(|st, params| st.did_open(params)); - router.notification::(|st, params| st.did_change(params)); - router.notification::(|st, params| st.did_create_files(params)); - router.notification::(|st, params| st.did_rename_files(params)); - router.notification::(|st, params| st.did_delete_files(params)); - router.notification::(|st, params| st.exit(params)); + router.notification::(ProtoLanguageServer::set_trace); + router.notification::(ProtoLanguageServer::did_save); + router.notification::(ProtoLanguageServer::did_open); + router.notification::(ProtoLanguageServer::did_change); + router.notification::(ProtoLanguageServer::did_create_files); + router.notification::(ProtoLanguageServer::did_rename_files); + router.notification::(ProtoLanguageServer::did_delete_files); + router.notification::(ProtoLanguageServer::exit); router } diff --git a/src/state.rs b/src/state.rs index b7b0e8b..d4ec053 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,23 +1,23 @@ use std::{ collections::{HashMap, HashSet}, - path::PathBuf, + path::{Path, PathBuf}, sync::{Arc, Mutex, RwLock, mpsc::Sender}, }; use tracing::info; use async_lsp::lsp_types::{ - CompletionItem, CompletionItemKind, DocumentSymbol, Location, OneOf, ProgressParamsValue, - PublishDiagnosticsParams, Url, WorkspaceSymbol, + CompletionItem, CompletionItemKind, Location, OneOf, ProgressParamsValue, + PublishDiagnosticsParams, Range, SymbolKind, SymbolTag, Url, WorkspaceSymbol, }; use tree_sitter::{Node, Query, QueryError}; use walkdir::WalkDir; use crate::{ config::Config, - model::generate_metamodel_query, + model::{ElementKind, generate_metamodel_query}, nodekind::NodeKind, parser::{ParsedTree, ProtoParser}, - protoc::ProtocDiagnostics, + protoc::collect_diagnostics, }; pub struct ProtoLanguageState { @@ -25,7 +25,6 @@ pub struct ProtoLanguageState { trees: Arc>>, parser: Arc>, parsed_workspaces: Arc>>, - protoc_diagnostics: Arc>, metamodel_query: Query, } @@ -48,7 +47,6 @@ impl ProtoLanguageState { trees: Arc::default(), parser: Arc::new(Mutex::new(ProtoParser::new())), parsed_workspaces: Arc::new(RwLock::new(HashSet::new())), - protoc_diagnostics: Arc::new(Mutex::new(ProtocDiagnostics::new())), metamodel_query, } } @@ -85,20 +83,57 @@ impl ProtoLanguageState { .collect() } + /// Runs a fast, pure-Rust substring match over the cached metamodel pool + /// populated during startup indexing. + /// + /// This deliberately avoids re-parsing the workspace or rebuilding the + /// hierarchical [`DocumentSymbol`] tree on every request. Instead it scans + /// the flat, already-indexed [`ModelElement`] registry and resolves each + /// candidate's container name by walking the in-memory parent links. pub fn find_workspace_symbols(&self, query: &str) -> Vec { + let query = query.to_lowercase(); let mut symbols = Vec::new(); for tree in self.get_trees() { - let doc_symbols = tree.document_symbols(); - - for doc_symbol in doc_symbols { - Self::find_workspace_symbols_impl( - &doc_symbol, - &tree.uri, - query, - None, - &mut symbols, - ); + for element in &tree.elements { + if matches!(element.kind, ElementKind::Import { .. }) { + continue; + } + + let name_lower = element.meta.name.to_lowercase(); + if !query.is_empty() && !name_lower.contains(&query) { + continue; + } + + let container_name = element + .parent_id + .and_then(|parent_id| tree.elements.get(parent_id)) + .map(|parent| parent.meta.name.clone()); + + let range = + element + .meta + .documentation + .first() + .map_or(element.meta.range, |comment| Range { + start: comment.range.start, + end: element.meta.range.end, + }); + + symbols.push(WorkspaceSymbol { + name: element.meta.name.clone(), + kind: SymbolKind::from(&element.kind), + tags: element + .kind + .is_deprecated() + .then(|| vec![SymbolTag::DEPRECATED]), + container_name, + location: OneOf::Left(Location { + uri: tree.uri.clone(), + range, + }), + data: None, + }); } } @@ -120,46 +155,10 @@ impl ProtoLanguageState { symbols } - fn find_workspace_symbols_impl( - doc_symbol: &DocumentSymbol, - uri: &Url, - query: &str, - container_name: Option, - symbols: &mut Vec, - ) { - let symbol_name_lower = doc_symbol.name.to_lowercase(); - - if query.is_empty() || symbol_name_lower.contains(query) { - symbols.push(WorkspaceSymbol { - name: doc_symbol.name.clone(), - kind: doc_symbol.kind, - tags: doc_symbol.tags.clone(), - container_name: container_name.clone(), - location: OneOf::Left(Location { - uri: uri.clone(), - range: doc_symbol.range, - }), - data: None, - }); - } - - if let Some(children) = &doc_symbol.children { - for child in children { - Self::find_workspace_symbols_impl( - child, - uri, - query, - Some(doc_symbol.name.clone()), - symbols, - ); - } - } - } - fn upsert_content_impl( &mut self, uri: &Url, - content: String, + content: &str, ipath: &[PathBuf], depth: usize, parse_session: &mut HashSet, @@ -190,17 +189,17 @@ impl ProtoLanguageState { self.documents .write() .expect("poison") - .insert(uri.clone(), content.clone()); + .insert(uri.clone(), content.to_string()); parse_session.insert(uri.clone()); - let imports = self.get_owned_imports(uri, content.as_str()); + let imports = self.get_owned_imports(uri, content); - for import in imports.iter() { + for import in &imports { if let Some(p) = ipath.iter().map(|p| p.join(import)).find(|p| p.exists()) && let Ok(uri) = Url::from_file_path(p.clone()) && let Ok(content) = std::fs::read_to_string(p) { - self.upsert_content_impl(&uri, content, ipath, depth - 1, parse_session); + self.upsert_content_impl(&uri, &content, ipath, depth - 1, parse_session); } } } @@ -217,12 +216,12 @@ impl ProtoLanguageState { pub fn upsert_content( &mut self, uri: &Url, - content: String, + content: &str, ipath: &[PathBuf], depth: usize, ) -> Vec { let mut session = HashSet::new(); - self.upsert_content_impl(uri, content.clone(), ipath, depth, &mut session); + self.upsert_content_impl(uri, content, ipath, depth, &mut session); // After content is upserted, those imports which couldn't be located // are flagged as import error @@ -237,8 +236,8 @@ impl ProtoLanguageState { pub fn parse_all_from_workspace( &mut self, - workspace: PathBuf, - progress_sender: Option>, + workspace: &Path, + progress_sender: Option<&Sender>, ) { if self .parsed_workspaces @@ -251,7 +250,7 @@ impl ProtoLanguageState { let files: Vec<_> = WalkDir::new(workspace.to_str().unwrap_or_default()) .into_iter() - .filter_map(|e| e.ok()) + .filter_map(std::result::Result::ok) .filter(|e| { if let Some(ext) = e.path().extension() { return ext == "proto"; @@ -272,10 +271,11 @@ impl ProtoLanguageState { if self.documents.read().expect("poison").contains_key(&uri) { continue; } - self.upsert_content(&uri, content, &[], 1); + self.upsert_content(&uri, &content, &[], 1); if let Some(sender) = &progress_sender { - let percentage = ((idx + 1) as f64 / total_files as f64 * 100.0) as u32; + let percentage = + u32::try_from((idx + 1 / total_files) * 100).unwrap_or_default(); let _ = sender.send(ProgressParamsValue::WorkDone( async_lsp::lsp_types::WorkDoneProgress::Report( async_lsp::lsp_types::WorkDoneProgressReport { @@ -302,25 +302,23 @@ impl ProtoLanguageState { pub fn upsert_file( &mut self, uri: &Url, - content: String, + content: &str, ipath: &[PathBuf], depth: usize, config: &Config, protoc_diagnostics: bool, ) -> Option { info!(%uri, %depth, "upserting file"); - let diag = self.upsert_content(uri, content.clone(), ipath, depth); + let diag = self.upsert_content(uri, content, ipath, depth); + let diag_slice: Vec<&str> = diag.iter().map(String::as_str).collect(); self.get_tree(uri).map(|tree| { let mut d = vec![]; d.extend(tree.collect_parse_diagnostics()); - d.extend(tree.collect_import_diagnostics(content.as_ref(), diag)); + d.extend(tree.collect_import_diagnostics(content.as_ref(), diag_slice.as_slice())); // Add protoc diagnostics if enabled - if protoc_diagnostics - && let Ok(protoc_diagnostics) = self.protoc_diagnostics.lock() - && let Ok(file_path) = uri.to_file_path() - { - let protoc_diags = protoc_diagnostics.collect_diagnostics( + if protoc_diagnostics && let Ok(file_path) = uri.to_file_path() { + let protoc_diags = collect_diagnostics( &config.path.protoc, file_path.to_str().unwrap_or_default(), &ipath @@ -651,7 +649,7 @@ mod test { let mut state0 = ProtoLanguageState::new(); state0.upsert_content( &uri("file:///a.proto"), - std::fs::read_to_string(&a_path).unwrap(), + std::fs::read_to_string(&a_path).unwrap().as_str(), &ipath, 0, ); @@ -661,7 +659,7 @@ mod test { let mut state1 = ProtoLanguageState::new(); state1.upsert_content( &uri("file:///a.proto"), - std::fs::read_to_string(&a_path).unwrap(), + std::fs::read_to_string(&a_path).unwrap().as_str(), &ipath, 1, ); @@ -686,11 +684,11 @@ mod test { // Non-proto file should be ignored std::fs::write(dir.path().join("notes.txt"), "hello").unwrap(); - state.parse_all_from_workspace(dir.path().to_path_buf(), None); + state.parse_all_from_workspace(&dir.path().to_path_buf(), None); assert_eq!(state.get_trees().len(), 2); // Second call should be idempotent - state.parse_all_from_workspace(dir.path().to_path_buf(), None); + state.parse_all_from_workspace(&dir.path().to_path_buf(), None); assert_eq!(state.get_trees().len(), 2); } diff --git a/src/transport.rs b/src/transport.rs index 7f6c996..7c52044 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -21,13 +21,13 @@ pub type LspWriter = Pin>; /// /// # Errors /// -/// Returns a [TransportError] if: +/// Returns a [`TransportError`] if: /// * The specified TCP port or socket address is already in use. /// * A Unix socket cannot be created (e.g., due to file permissions or path conflicts). /// * Windows Named Pipe creation fails due to access rights or naming violations. pub async fn create_transport(cli: &Cli) -> TransportResult<(LspReader, LspWriter)> { if let Some(port) = cli.port { - let addr = format!("127.0.0.1:{}", port); + let addr = format!("127.0.0.1:{port}"); return create_tcp_transport(&addr).await; } @@ -39,23 +39,20 @@ pub async fn create_transport(cli: &Cli) -> TransportResult<(LspReader, LspWrite return create_pipe_transport(path).await; } - create_stdio_transport().await + create_stdio_transport() } async fn create_tcp_transport(address: &str) -> TransportResult<(LspReader, LspWriter)> { let listener = tokio::net::TcpListener::bind(address) .await - .inspect_err(|e| eprintln!("Error: Could not bind to {}: {}", address, e))?; + .inspect_err(|e| eprintln!("Error: Could not bind to {address}: {e}"))?; - eprintln!( - "LSP server listening on TCP: {}. Waiting for client...", - address - ); + eprintln!("LSP server listening on TCP: {address}. Waiting for client..."); let (stream, _) = listener .accept() .await - .inspect_err(|e| eprintln!("Error: Failed to accept connection: {}", e))?; + .inspect_err(|e| eprintln!("Error: Failed to accept connection: {e}"))?; eprintln!("Client connected"); tracing::info!("Using TCP: {}", address); @@ -73,8 +70,7 @@ async fn create_pipe_transport(path: &str) -> TransportResult<(LspReader, LspWri if let Ok(metadata) = std::fs::metadata(path) { if !metadata.file_type().is_socket() { return Err(format!( - "Path '{}' exists and is not a socket. Refusing to overwrite.", - path + "Path '{path}' exists and is not a socket. Refusing to overwrite.", ) .into()); } @@ -85,17 +81,16 @@ async fn create_pipe_transport(path: &str) -> TransportResult<(LspReader, LspWri } let listener = tokio::net::UnixListener::bind(path) - .inspect_err(|e| eprintln!("Failed to bind Unix domain socket {}: {}", path, e))?; + .inspect_err(|e| eprintln!("Failed to bind Unix domain socket {path}: {e}"))?; eprintln!( - "Listening on Unix domain socket: {}. Waiting for client...", - path + "Listening on Unix domain socket: {path}. Waiting for client..." ); let (stream, _) = listener .accept() .await - .inspect_err(|e| eprintln!("Error: Failed to accept connection: {}", e))?; + .inspect_err(|e| eprintln!("Error: Failed to accept connection: {e}"))?; eprintln!("Client connected"); tracing::info!("Using Unix domain socket: {}", path); @@ -173,14 +168,14 @@ fn normalize_windows_pipe(path: &str) -> TransportResult { Ok(full_path) } -async fn create_stdio_transport() -> TransportResult<(LspReader, LspWriter)> { +fn create_stdio_transport() -> TransportResult<(LspReader, LspWriter)> { // Prefer truly asynchronous piped stdin/stdout without blocking tasks. #[cfg(unix)] { let stdin = async_lsp::stdio::PipeStdin::lock_tokio() - .map_err(|e| format!("Failed to lock stdin: {}", e))?; + .map_err(|e| format!("Failed to lock stdin: {e}"))?; let stdout = async_lsp::stdio::PipeStdout::lock_tokio() - .map_err(|e| format!("Failed to lock stdout: {}", e))?; + .map_err(|e| format!("Failed to lock stdout: {e}"))?; eprintln!("Using Stdio"); tracing::info!("Using Stdio"); diff --git a/src/utils.rs b/src/utils.rs index 916592b..c97e9c0 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -58,17 +58,11 @@ pub fn is_position_inside_range(position: Position, range: Range) -> bool { } fn is_title_case(s: &str) -> bool { - s.chars() - .next() - .map(|x| x.is_uppercase()) - .unwrap_or_default() + s.chars().next().is_some_and(char::is_uppercase) } fn is_first_lower_case(s: &&str) -> bool { - s.chars() - .next() - .map(|x| x.is_lowercase()) - .unwrap_or_default() + s.chars().next().is_some_and(char::is_lowercase) } pub fn is_inner_identifier(s: &str) -> bool { @@ -85,7 +79,7 @@ pub fn trailing_segment(qualified: &str) -> &str { } pub fn split_identifier_package(s: &str) -> (&str, &str) { - let s = s.trim_start_matches("."); + let s = s.trim_start_matches('.'); if is_inner_identifier(s) || !s.contains('.') { return ("", s); } diff --git a/src/workspace/definition.rs b/src/workspace/definition.rs index c541d75..1e5a908 100644 --- a/src/workspace/definition.rs +++ b/src/workspace/definition.rs @@ -76,9 +76,9 @@ mod test { let c = include_str!("input/c.proto"); let mut state: ProtoLanguageState = ProtoLanguageState::new(); - state.upsert_file(&a_uri, a.to_owned(), &ipath, 2, &Config::default(), false); - state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false); - state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false); + state.upsert_file(&a_uri, a, &ipath, 2, &Config::default(), false); + state.upsert_file(&b_uri, b, &ipath, 2, &Config::default(), false); + state.upsert_file(&c_uri, c, &ipath, 2, &Config::default(), false); assert_yaml_snapshot!(state.definition( &ipath, diff --git a/src/workspace/hover.rs b/src/workspace/hover.rs index e95fd97..43c84d2 100644 --- a/src/workspace/hover.rs +++ b/src/workspace/hover.rs @@ -140,10 +140,10 @@ mod test { let x = include_str!("input/inner/x.proto"); let mut state: ProtoLanguageState = ProtoLanguageState::new(); - state.upsert_file(&a_uri, a.to_owned(), &ipath, 3, &Config::default(), false); - state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false); - state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false); - state.upsert_file(&x_uri, x.to_owned(), &ipath, 2, &Config::default(), false); + state.upsert_file(&a_uri, a, &ipath, 3, &Config::default(), false); + state.upsert_file(&b_uri, b, &ipath, 2, &Config::default(), false); + state.upsert_file(&c_uri, c, &ipath, 2, &Config::default(), false); + state.upsert_file(&x_uri, x, &ipath, 2, &Config::default(), false); assert_yaml_snapshot!(state.hover( &a_uri, diff --git a/src/workspace/rename.rs b/src/workspace/rename.rs index 1d8a4ef..7aad962 100644 --- a/src/workspace/rename.rs +++ b/src/workspace/rename.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::mpsc::Sender; use async_lsp::lsp_types::{Location, Position, ProgressParamsValue, TextEdit, Url}; @@ -28,8 +28,8 @@ impl ProtoLanguageState { current_package: &str, identifier: &str, new_text: &str, - workspace: PathBuf, - progress_sender: Option>, + workspace: &Path, + progress_sender: Option<&Sender>, ) -> HashMap> { self.parse_all_from_workspace(workspace, progress_sender); let (_, identifier) = split_identifier_package(identifier); @@ -94,8 +94,8 @@ impl ProtoLanguageState { &mut self, current_package: &str, identifier: &str, - workspace: PathBuf, - progress_sender: Option>, + workspace: &Path, + progress_sender: Option<&Sender>, ) -> Option> { self.parse_all_from_workspace(workspace, progress_sender); let (_, identifier) = split_identifier_package(identifier); @@ -222,7 +222,7 @@ impl ProtoLanguageState { pub fn apply_rename_ops( &mut self, ops: &[RenameOp], - workspace: PathBuf, + workspace: &Path, progress_sender: Option>, ) -> Option>> { let mut all: HashMap> = HashMap::new(); @@ -231,14 +231,14 @@ impl ProtoLanguageState { // Only the first op gets the progress sender; subsequent ops would // double-report. let sender = progress.take(); - match self.run_single_rename(op, workspace.clone(), sender) { + match self.run_single_rename(op, workspace, sender.as_ref()) { Some(edits) => { for (u, e) in edits { all.entry(u).or_default().extend(e); } } None if i == 0 => return None, - None => continue, + None => {} } } Some(all) @@ -247,8 +247,8 @@ impl ProtoLanguageState { fn run_single_rename( &mut self, op: &RenameOp, - workspace: PathBuf, - progress_sender: Option>, + workspace: &Path, + progress_sender: Option<&Sender>, ) -> Option>> { let tree = self.get_tree(&op.uri)?; let content = self.get_content(&op.uri); @@ -484,14 +484,7 @@ mod test { let mut state = ProtoLanguageState::new(); for (uri, content) in files { let parsed_uri = uri.parse().unwrap(); - state.upsert_file( - &parsed_uri, - (*content).to_owned(), - ipath, - 2, - &Config::default(), - false, - ); + state.upsert_file(&parsed_uri, content, ipath, 2, &Config::default(), false); } state } @@ -516,29 +509,29 @@ mod test { let c = include_str!("input/c.proto"); let mut state: ProtoLanguageState = ProtoLanguageState::new(); - state.upsert_file(&a_uri, a.to_owned(), &ipath, 2, &Config::default(), false); - state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false); - state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false); + state.upsert_file(&a_uri, a, &ipath, 2, &Config::default(), false); + state.upsert_file(&b_uri, b, &ipath, 2, &Config::default(), false); + state.upsert_file(&c_uri, c, &ipath, 2, &Config::default(), false); assert_yaml_snapshot!(state.rename_fields( "com.workspace", "Author", "Writer", - PathBuf::from("src/workspace/input"), + &PathBuf::from("src/workspace/input"), None )); assert_yaml_snapshot!(state.rename_fields( "com.workspace", "Author.Address", "Author.Location", - PathBuf::from("src/workspace/input"), + &PathBuf::from("src/workspace/input"), None )); assert_yaml_snapshot!(state.rename_fields( "com.utility", "Foobar.Baz", "Foobar.Baaz", - PathBuf::from("src/workspace/input"), + &PathBuf::from("src/workspace/input"), None )); } @@ -555,20 +548,20 @@ mod test { let c = include_str!("input/c.proto"); let mut state: ProtoLanguageState = ProtoLanguageState::new(); - state.upsert_file(&a_uri, a.to_owned(), &ipath, 2, &Config::default(), false); - state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false); - state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false); + state.upsert_file(&a_uri, a, &ipath, 2, &Config::default(), false); + state.upsert_file(&b_uri, b, &ipath, 2, &Config::default(), false); + state.upsert_file(&c_uri, c, &ipath, 2, &Config::default(), false); assert_yaml_snapshot!(state.reference_fields( "com.workspace", "Author", - PathBuf::from("src/workspace/input"), + &PathBuf::from("src/workspace/input"), None )); assert_yaml_snapshot!(state.reference_fields( "com.workspace", "Author.Address", - PathBuf::from("src/workspace/input"), + &PathBuf::from("src/workspace/input"), None )); } @@ -582,22 +575,8 @@ mod test { let msg = include_str!("input/messages.proto"); let mut state: ProtoLanguageState = ProtoLanguageState::new(); - state.upsert_file( - &svc_uri, - svc.to_owned(), - &ipath, - 2, - &Config::default(), - false, - ); - state.upsert_file( - &msg_uri, - msg.to_owned(), - &ipath, - 2, - &Config::default(), - false, - ); + state.upsert_file(&svc_uri, svc, &ipath, 2, &Config::default(), false); + state.upsert_file(&msg_uri, msg, &ipath, 2, &Config::default(), false); // Lookup hits the rpc_name node in service.proto. let mut locs = state.find_rpc_decls("GetBook"); @@ -920,7 +899,7 @@ mod test { }; let ops = state.compute_rename_ops(&svc_uri, pos, "FetchBook", &ipath, true); let edits = state - .apply_rename_ops(&ops, PathBuf::from("src/workspace/input"), None) + .apply_rename_ops(&ops, &PathBuf::from("src/workspace/input"), None) .expect("primary rename should not fail"); // Sort within each file so the snapshot is order-independent across diff --git a/src/workspace/snapshots/protols__workspace__rename__test__reference-3.snap b/src/workspace/snapshots/protols__workspace__rename__test__reference-3.snap deleted file mode 100644 index 8f1f9eb..0000000 --- a/src/workspace/snapshots/protols__workspace__rename__test__reference-3.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: src/workspace/rename.rs -expression: "state.reference_fields(\"com.utility\", \"Foobar.Baz\")" -snapshot_kind: text ---- -- uri: "file://input/a.proto" - range: - start: - line: 11 - character: 3 - end: - line: 11 - character: 25 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__address_symbols.snap b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__address_symbols.snap deleted file mode 100644 index 06f3207..0000000 --- a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__address_symbols.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: src/workspace/workspace_symbol.rs -expression: address_symbols ---- -- name: Address - kind: 23 - containerName: Author - location: - uri: "file:///home/runner/work/protols/protols/src/workspace/input/b.proto" - range: - start: - line: 9 - character: 3 - end: - line: 11 - character: 4 -- name: Address - kind: 23 - containerName: Author - location: - uri: "file://input/b.proto" - range: - start: - line: 9 - character: 3 - end: - line: 11 - character: 4 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap deleted file mode 100644 index f40ba5b..0000000 --- a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap +++ /dev/null @@ -1,61 +0,0 @@ ---- -source: src/workspace/workspace_symbol.rs -expression: all_symbols ---- -- name: Address - kind: 23 - containerName: Author - location: - uri: "file://input/b.proto" - range: - start: - line: 9 - character: 3 - end: - line: 11 - character: 4 -- name: Author - kind: 23 - location: - uri: "file://input/b.proto" - range: - start: - line: 5 - character: 0 - end: - line: 14 - character: 1 -- name: Baz - kind: 23 - containerName: Foobar - location: - uri: "file://input/c.proto" - range: - start: - line: 8 - character: 3 - end: - line: 10 - character: 4 -- name: Book - kind: 23 - location: - uri: "file://input/a.proto" - range: - start: - line: 9 - character: 0 - end: - line: 14 - character: 1 -- name: Foobar - kind: 23 - location: - uri: "file://input/c.proto" - range: - start: - line: 5 - character: 0 - end: - line: 13 - character: 1 diff --git a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__author_symbols.snap b/src/workspace/snapshots/protols__workspace__workspace_symbol__test__author_symbols.snap deleted file mode 100644 index 229fdd0..0000000 --- a/src/workspace/snapshots/protols__workspace__workspace_symbol__test__author_symbols.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: src/workspace/workspace_symbol.rs -expression: author_symbols ---- -- name: Author - kind: 23 - location: - uri: "file:///home/runner/work/protols/protols/src/workspace/input/b.proto" - range: - start: - line: 5 - character: 0 - end: - line: 14 - character: 1 -- name: Author - kind: 23 - location: - uri: "file://input/b.proto" - range: - start: - line: 5 - character: 0 - end: - line: 14 - character: 1 diff --git a/src/workspace/workspace_symbol.rs b/src/workspace/workspace_symbol.rs index 7e91b43..dcc80d0 100644 --- a/src/workspace/workspace_symbol.rs +++ b/src/workspace/workspace_symbol.rs @@ -24,9 +24,9 @@ mod test { let c = include_str!("input/c.proto"); let mut state: ProtoLanguageState = ProtoLanguageState::new(); - state.upsert_file(&a_uri, a.to_owned(), &ipath, 3, &Config::default(), false); - state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false); - state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false); + state.upsert_file(&a_uri, a, &ipath, 3, &Config::default(), false); + state.upsert_file(&b_uri, b, &ipath, 2, &Config::default(), false); + state.upsert_file(&c_uri, c, &ipath, 2, &Config::default(), false); // Test empty query - should return all symbols let all_symbols = state.find_workspace_symbols(""); From 98b112ab76b1236c239439bc360c513339e96666 Mon Sep 17 00:00:00 2001 From: Ashar Date: Sun, 2 Aug 2026 21:04:57 +0530 Subject: [PATCH 2/3] ci: format code Signed-off-by: Ashar --- src/parser/diagnostics.rs | 6 +----- src/transport.rs | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/parser/diagnostics.rs b/src/parser/diagnostics.rs index 481a310..88d2bd8 100644 --- a/src/parser/diagnostics.rs +++ b/src/parser/diagnostics.rs @@ -18,11 +18,7 @@ impl ParsedTree { .collect() } - pub fn collect_import_diagnostics( - &self, - content: &[u8], - import: &[&str], - ) -> Vec { + pub fn collect_import_diagnostics(&self, content: &[u8], import: &[&str]) -> Vec { self.get_import_path_range(content, import) .into_iter() .map(|r| Diagnostic { diff --git a/src/transport.rs b/src/transport.rs index 7c52044..3a66f7e 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -83,9 +83,7 @@ async fn create_pipe_transport(path: &str) -> TransportResult<(LspReader, LspWri let listener = tokio::net::UnixListener::bind(path) .inspect_err(|e| eprintln!("Failed to bind Unix domain socket {path}: {e}"))?; - eprintln!( - "Listening on Unix domain socket: {path}. Waiting for client..." - ); + eprintln!("Listening on Unix domain socket: {path}. Waiting for client..."); let (stream, _) = listener .accept() From ae8f69e73ec9d42f725f79317bb8e8a15f4121ca Mon Sep 17 00:00:00 2001 From: Ashar Date: Sun, 2 Aug 2026 21:26:25 +0530 Subject: [PATCH 3/3] ci: fix lint for all targets Signed-off-by: Ashar --- src/formatter/clang.rs | 11 +++++------ src/log.rs | 4 ++-- src/state.rs | 19 +++++++++---------- src/transport.rs | 7 +++---- src/utils.rs | 8 ++++---- src/workspace/workspace_symbol.rs | 12 ++++++------ 6 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/formatter/clang.rs b/src/formatter/clang.rs index df35b2f..f5e90dc 100644 --- a/src/formatter/clang.rs +++ b/src/formatter/clang.rs @@ -185,7 +185,7 @@ mod test { for i in pos { with_settings!({description => c, info => &i}, { assert_yaml_snapshot!(Replacement::offset_to_position(i, c)); - }) + }); } } @@ -199,7 +199,7 @@ mod test { for i in pos { with_settings!({description => c, info => &i}, { assert_yaml_snapshot!(Replacement::offset_to_position(i, c)); - }) + }); } } @@ -220,12 +220,11 @@ mod test { .find(target) .expect("Could not find target in content"); let xml_output = format!( - r#" + r" - + // -"#, - offset +" ); let r = Replacements::from_str(&xml_output).unwrap(); diff --git a/src/log.rs b/src/log.rs index 610f384..3debc31 100644 --- a/src/log.rs +++ b/src/log.rs @@ -188,10 +188,10 @@ mod tests { assert!(msg.message.contains(MESSAGE)); - let expected_field = format!("{}={}", field_name, field_value); + let expected_field = format!("{field_name}={field_value}"); assert!(msg.message.contains(&expected_field)); - assert!(msg.message.contains(&format!("[{}]", TARGET))); + assert!(msg.message.contains(&format!("[{TARGET}]"))); } #[test] diff --git a/src/state.rs b/src/state.rs index d4ec053..48c7e94 100644 --- a/src/state.rs +++ b/src/state.rs @@ -443,19 +443,19 @@ mod test { state.upsert_content( &uri("file:///test.proto"), - "syntax = \"proto3\";\npackage com.test;\nmessage Book { string title = 1; }\nenum Color { RED = 0; }\n".into(), + "syntax = \"proto3\";\npackage com.test;\nmessage Book { string title = 1; }\nenum Color { RED = 0; }\n", ipath, 1, ); state.upsert_content( &uri("file:///other.proto"), - "syntax = \"proto3\";\npackage com.test;\nmessage Author { string name = 1; }\n".into(), + "syntax = \"proto3\";\npackage com.test;\nmessage Author { string name = 1; }\n", ipath, 1, ); state.upsert_content( &uri("file:///diff.proto"), - "syntax = \"proto3\";\npackage com.other;\nmessage Foo { int32 bar = 1; }\n".into(), + "syntax = \"proto3\";\npackage com.other;\nmessage Foo { int32 bar = 1; }\n", ipath, 1, ); @@ -603,7 +603,7 @@ mod test { let ipath: &[PathBuf] = &[]; let unresolved = state.upsert_content( &uri("file:///importing.proto"), - "syntax = \"proto3\";\nimport \"nonexistent.proto\";\npackage com.test;\n".into(), + "syntax = \"proto3\";\nimport \"nonexistent.proto\";\npackage com.test;\n", ipath, 1, ); @@ -620,7 +620,7 @@ mod test { let unresolved = state.upsert_content( &uri("file:///main.proto"), - "syntax = \"proto3\";\nimport \"dep.proto\";\npackage com.main;\n".into(), + "syntax = \"proto3\";\nimport \"dep.proto\";\npackage com.main;\n", &ipath, 1, ); @@ -684,11 +684,11 @@ mod test { // Non-proto file should be ignored std::fs::write(dir.path().join("notes.txt"), "hello").unwrap(); - state.parse_all_from_workspace(&dir.path().to_path_buf(), None); + state.parse_all_from_workspace(dir.path(), None); assert_eq!(state.get_trees().len(), 2); // Second call should be idempotent - state.parse_all_from_workspace(&dir.path().to_path_buf(), None); + state.parse_all_from_workspace(dir.path(), None); assert_eq!(state.get_trees().len(), 2); } @@ -698,7 +698,7 @@ mod test { let ipath: &[PathBuf] = &[]; let result = state.upsert_file( &uri("file:///test.proto"), - "syntax = \"proto3\";\npackage com.test;\nmessage Book {}\n".into(), + "syntax = \"proto3\";\npackage com.test;\nmessage Book {}\n", ipath, 1, &Config::default(), @@ -717,8 +717,7 @@ mod test { let ipath: &[PathBuf] = &[]; let result = state.upsert_file( &uri("file:///bad.proto"), - "syntax = \"proto3\";\npackage com.test;\nmessage Book { invalid syntax here }\n" - .into(), + "syntax = \"proto3\";\npackage com.test;\nmessage Book { invalid syntax here }\n", ipath, 1, &Config::default(), diff --git a/src/transport.rs b/src/transport.rs index 3a66f7e..8bdbe00 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -149,18 +149,17 @@ fn normalize_windows_pipe(path: &str) -> TransportResult { unc.to_string() } else { return Err(format!( - "Invalid UNC pipe path: '{}'. Missing or misplaced '\\pipe\\'.", - unc + "Invalid UNC pipe path: '{unc}'. Missing or misplaced '\\pipe\\'." ) .into()); } } file if file.contains(':') => { - return Err(format!("Named pipes cannot be files (like '{}').", file).into()); + return Err(format!("Named pipes cannot be files (like '{file}').").into()); } - suffix => format!(r"\\.\pipe\{}", suffix), + suffix => format!(r"\\.\pipe\{suffix}"), }; Ok(full_path) diff --git a/src/utils.rs b/src/utils.rs index c97e9c0..cf69e3e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -188,12 +188,12 @@ mod test { #[test] fn test_position_large_values() { let p = Point { - row: 999999, - column: 999999, + row: 999_999, + column: 999_999, }; let pos = to_lsp_position(p); - assert_eq!(pos.line, 999999); - assert_eq!(pos.character, 999999); + assert_eq!(pos.line, 999_999); + assert_eq!(pos.character, 999_999); } #[test] diff --git a/src/workspace/workspace_symbol.rs b/src/workspace/workspace_symbol.rs index dcc80d0..9d7bb13 100644 --- a/src/workspace/workspace_symbol.rs +++ b/src/workspace/workspace_symbol.rs @@ -36,11 +36,11 @@ mod test { assert!( uri_str.contains(&base_uri_1), - "URI {} should contain {}", uri_str, base_uri_1 + "URI {uri_str} should contain {base_uri_1}" ); let file_name = uri_str.split('/').next_back().unwrap(); - format!("file:///src/workspace/input/{}", file_name) + format!("file:///src/workspace/input/{file_name}") })}); @@ -52,11 +52,11 @@ mod test { assert!( uri_str.contains(&base_uri_2), - "URI {} should contain {}", uri_str, base_uri_2 + "URI {uri_str} should contain {base_uri_2}" ); let file_name = uri_str.split('/').next_back().unwrap(); - format!("file:///src/workspace/input/{}", file_name) + format!("file:///src/workspace/input/{file_name}") })}); // Test query for "address" - should match Address @@ -67,12 +67,12 @@ mod test { assert!( uri_str.contains(&base_uri_3), - "URI {} should contain {}", uri_str, base_uri_3 + "URI {uri_str} should contain {base_uri_3}" ); let file_name = uri_str.split('/').next_back().unwrap(); - format!("file:///src/workspace/input/{}", file_name) + format!("file:///src/workspace/input/{file_name}") })}); // Test query that should not match anything