From e7f777a01dd45f48f3a5f0f4abaaee55fe3e4d2e Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Tue, 21 Jul 2026 08:24:03 +0500 Subject: [PATCH 01/18] Fix: Move to a better code coverage crate (cargo llvm-cov). --- builder/Cargo.toml | 1 - builder/src/main.rs | 20 +++++++++----------- server/Cargo.toml | 8 ++++++-- server/src/ide/filewatcher.rs | 8 ++++---- server/src/main.rs | 2 +- server/src/webserver.rs | 16 ++++++++++------ 6 files changed, 30 insertions(+), 25 deletions(-) diff --git a/builder/Cargo.toml b/builder/Cargo.toml index 28de841c..adeb81a2 100644 --- a/builder/Cargo.toml +++ b/builder/Cargo.toml @@ -29,6 +29,5 @@ clap = { version = "4", features = ["derive"] } cmd_lib = "2.0.0" current_platform = "0.2.0" dunce = "1.0.5" -open = "5" path-slash = "0.2.1" regex = "1.11.1" diff --git a/builder/src/main.rs b/builder/src/main.rs index 7f1a489d..f4cdd67c 100644 --- a/builder/src/main.rs +++ b/builder/src/main.rs @@ -421,8 +421,12 @@ fn run_install(dev: bool) -> io::Result<()> { cargo binstall cargo-sort --no-confirm; info "cargo binstall cargo-audit"; cargo binstall cargo-audit --no-confirm; - info "cargo binstall cargo-tarpaulin"; - cargo binstall cargo-tarpaulin --no-confirm; + info "cargo binstall cargo-llvm-cov"; + cargo binstall cargo-llvm-cov --no-confirm; + // Install the required llvm-tools component used by cargo-llvm-cov + // without prompting. + info "rustup component add llvm-tools-preview"; + rustup component add llvm-tools-preview; )?; } Ok(()) @@ -835,15 +839,9 @@ fn run_postrelease(target: &str, tag: &str) -> io::Result<()> { fn run_coverage() -> io::Result<()> { run_cmd!( - info "cargo tarpaulin --skip-clean --out=html --target-dir=tarpaulin"; - cargo tarpaulin --skip-clean --out=html --out=json --target-dir=tarpaulin; - )?; - - // Open the resulting coverage report in the default web browser. The current - // working directory is `server/` (see `main`), so the report lives at - // `server/tarpaulin-report.html`. - let report = Path::new("tarpaulin-report.html"); - open::that(report) + info "cargo llvm-cov --open"; + cargo llvm-cov --open; + ) } // CLI implementation diff --git a/server/Cargo.toml b/server/Cargo.toml index 23f29691..6f0e169a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -144,6 +144,10 @@ strip = "symbols" [profile.dist] inherits = "release" +# Code coverage +# ------------- +# [lints.rust] -# Avoid a lint about tarpaulin. -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } +# Avoid a lint about the `coverage` cfg set by `cargo llvm-cov` (see `bt +# cover`). +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] } diff --git a/server/src/ide/filewatcher.rs b/server/src/ide/filewatcher.rs index 3a524c8a..01c3588b 100644 --- a/server/src/ide/filewatcher.rs +++ b/server/src/ide/filewatcher.rs @@ -89,7 +89,7 @@ pub const FILEWATCHER_PATH_PREFIX: &[&str] = &["fw", "fsc"]; /// replaced by something better. /// /// Redirect from the root of the filesystem to the actual root path on this OS. -#[cfg(not(tarpaulin_include))] +#[cfg_attr(coverage, coverage(off))] pub async fn filewatcher_root_fs_redirect() -> impl Responder { HttpResponse::TemporaryRedirect() .insert_header((header::LOCATION, "/fw/fsb/")) @@ -104,7 +104,7 @@ pub async fn filewatcher_root_fs_redirect() -> impl Responder { /// /// Omit code coverage -- this is a temporary interface, until IDE integration /// replaces this. -#[cfg(not(tarpaulin_include))] +#[cfg_attr(coverage, coverage(off))] #[get("/fw/fsb/{path:.*}")] async fn filewatcher_browser_endpoint( req: HttpRequest, @@ -164,7 +164,7 @@ async fn filewatcher_browser_endpoint( /// /// Omit code coverage -- this is a temporary interface, until IDE integration /// replaces this. -#[cfg(not(tarpaulin_include))] +#[cfg_attr(coverage, coverage(off))] async fn dir_listing(web_path: &str, dir_path: &Path) -> HttpResponse { // Special case on Windows: list drive letters. #[cfg(target_os = "windows")] @@ -311,7 +311,7 @@ async fn dir_listing(web_path: &str, dir_path: &Path) -> HttpResponse { /// Copied almost verbatim from the /// [win\_partitions crate](https://docs.rs/crate/win_partitions/0.3.0/source/src/win_api.rs#144) /// when compilation errors broke the crate. -#[cfg(not(tarpaulin_include))] +#[cfg_attr(coverage, coverage(off))] #[cfg(target_os = "windows")] pub fn get_logical_drive() -> Result, std::io::Error> { let bitmask = unsafe { GetLogicalDrives() }; diff --git a/server/src/main.rs b/server/src/main.rs index 99c06299..8882ef5f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -360,7 +360,7 @@ fn fix_addr(addr: &SocketAddr) -> SocketAddr { } } -#[cfg(not(tarpaulin_include))] +#[cfg_attr(coverage, coverage(off))] fn main() -> Result<(), Box> { let cli = Cli::parse(); let addr = SocketAddr::new(cli.host, cli.port); diff --git a/server/src/webserver.rs b/server/src/webserver.rs index 2831d061..d285e84c 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -538,17 +538,21 @@ pub fn set_root_path( }; #[cfg(not(any(test, debug_assertions)))] let root_path = PathBuf::from(exe_dir); - #[cfg(any(test, debug_assertions))] + #[cfg(any(test, debug_assertions, coverage))] let mut root_path = PathBuf::from(exe_dir); - // When in debug or running tests, use the layout of the Git repo to find - // client files. In release mode, we assume the static folder is a - // subdirectory of the directory containing the executable. + // When running tests, the executable lives in an extra `deps` directory + // (e.g. `target/debug/deps/`) compared to a plain `cargo build` + // (`target/debug/`). In development, this extra directory level for the + // extension isn't needed. #[cfg(test)] - // In development, this extra directory level for the extension isn't - // needed. if extension_base_path.is_none() { root_path.push(".."); } + // `cargo llvm-cov` builds into `target/llvm-cov-target/` instead of + // `target/`, which adds one extra directory level compared to a plain + // `cargo test`/`cargo build`; account for it here. + #[cfg(coverage)] + root_path.push(".."); // Note that `debug_assertions` is also enabled for testing, so this adds to // the previous line when running tests. #[cfg(debug_assertions)] From 32cc85357fe9adc0eeeaf2c984c53c7546845d91 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Tue, 21 Jul 2026 09:04:46 +0500 Subject: [PATCH 02/18] Add: stricter linting for Rust. --- server/Cargo.toml | 19 +++++++++++++++++++ server/src/lib.rs | 32 ++++++++++++++++---------------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/server/Cargo.toml b/server/Cargo.toml index 6f0e169a..08d06b3b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -151,3 +151,22 @@ inherits = "release" # Avoid a lint about the `coverage` cfg set by `cargo llvm-cov` (see `bt # cover`). unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] } + +# Clippy config +# ------------- +# +# A good article: https://emschwartz.me/your-clippy-config-should-be-stricter. +# +# Docs on clippy: see `cargo clippy --help`, `cargo check --help` (the target +# selection and feature selection flags seems to apply to clippy). +[lints.clippy] +# https://github.com/rust-lang/rust-clippy shows which categories are enabled by +# default; https://doc.rust-lang.org/stable/clippy/lints.html for a description +# of each of these categories. +pedantic = "warn" +# Disable specific pedantic lints that don't apply. Use LP docs instead of +# rustdoc for errors and panics. +missing_errors_doc = "allow" +missing_panics_doc = "allow" +# This warns at the phrase "CodeChat Editor", so it's disabled. +doc_markdown = "allow" diff --git a/server/src/lib.rs b/server/src/lib.rs index 16e71f18..7d3a0787 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,19 +1,19 @@ -/// Copyright (C) 2025 Bryan A. Jones. -/// -/// This file is part of the CodeChat Editor. The CodeChat Editor is free -/// software: you can redistribute it and/or modify it under the terms of the -/// GNU General Public License as published by the Free Software Foundation, -/// either version 3 of the License, or (at your option) any later version. -/// -/// The CodeChat Editor is distributed in the hope that it will be useful, but -/// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -/// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -/// more details. -/// -/// You should have received a copy of the GNU General Public License along with -/// the CodeChat Editor. If not, see -/// [http://www.gnu.org/licenses](http://www.gnu.org/licenses). -/// +// Copyright (C) 2025 Bryan A. Jones. +// +// This file is part of the CodeChat Editor. The CodeChat Editor is free +// software: you can redistribute it and/or modify it under the terms of the +// GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// The CodeChat Editor is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along with +// the CodeChat Editor. If not, see +// [http://www.gnu.org/licenses](http://www.gnu.org/licenses). +// /// `lib.rs` -- Define library modules for the CodeChat Editor Server /// ================================================================= /// From fbb7b22b2bb06ff0ae5cf98ffbdd36cae60760ba Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Tue, 21 Jul 2026 10:06:47 +0500 Subject: [PATCH 03/18] Fix: new lints. --- builder/Cargo.toml | 4 + builder/src/main.rs | 14 +- extensions/VSCode/Cargo.toml | 5 + extensions/VSCode/src/lib.rs | 8 +- server/Cargo.toml | 19 ++- server/src/capture.rs | 111 +++++++------ server/src/ide.rs | 14 +- server/src/ide/filewatcher.rs | 94 ++++++----- server/src/ide/vscode.rs | 206 ++++++++++++------------ server/src/ide/vscode/tests.rs | 59 ++++--- server/src/lexer.rs | 99 ++++++------ server/src/lexer/pest_parser.rs | 28 ++-- server/src/lexer/supported_languages.rs | 3 +- server/src/lexer/tests.rs | 51 +++--- server/src/main.rs | 17 +- server/src/processing.rs | 98 +++++------ server/src/processing/tests.rs | 36 ++--- server/src/translation.rs | 66 ++++---- server/src/webserver.rs | 184 +++++++++++---------- server/src/webserver/tests.rs | 2 +- server/tests/overall_1.rs | 4 +- server/tests/overall_4.rs | 77 +++------ server/tests/overall_5.rs | 8 +- server/tests/overall_common/mod.rs | 23 +-- test_utils/Cargo.toml | 13 ++ test_utils/src/test_utils.rs | 2 + 26 files changed, 641 insertions(+), 604 deletions(-) diff --git a/builder/Cargo.toml b/builder/Cargo.toml index adeb81a2..df465ba0 100644 --- a/builder/Cargo.toml +++ b/builder/Cargo.toml @@ -31,3 +31,7 @@ current_platform = "0.2.0" dunce = "1.0.5" path-slash = "0.2.1" regex = "1.11.1" + +[lints.clippy] +pedantic = { level = "warn", priority = 0 } +doc_markdown = { level = "allow", priority = 1 } diff --git a/builder/src/main.rs b/builder/src/main.rs index f4cdd67c..4c6998e6 100644 --- a/builder/src/main.rs +++ b/builder/src/main.rs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License along with // the CodeChat Editor. If not, see // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). -/// `main.rs` -- Entrypoint for the CodeChat Editor Builder +/// `main.rs` -- Entrypoint for the `CodeChat` Editor Builder /// ======================================================= /// /// This code uses [dist](https://opensource.axo.dev/cargo-dist/book/) as a part @@ -163,7 +163,7 @@ fn run_script, A: AsRef, P: AsRef + std::fmt::Displa process.arg("/c").arg(script); } else { process = Command::new(script); - }; + } process.args(args).current_dir(&dir); // A bit crude, but displays the command being run. println!("{dir}: {process:#?}"); @@ -181,7 +181,7 @@ fn run_script, A: AsRef, P: AsRef + std::fmt::Displa /// programs (`robocopy`/`rsync`) to accomplish this. Very important: the `src` /// **must** end with a `/`, otherwise the Windows and Linux copies aren't /// identical. -fn quick_copy_dir>(src: P, dest: P, files: Option

) -> io::Result<()> { +fn quick_copy_dir>(src: P, dest: P, files: Option<&P>) -> io::Result<()> { assert!(src.as_ref().to_string_lossy().ends_with('/')); let mut copy_process; let src = OsStr::new(src.as_ref()); @@ -248,7 +248,7 @@ fn quick_copy_dir>(src: P, dest: P, files: Option

) -> io::Resu } // Print the command, in case this produces and error or takes a while. - println!("{:#?}", copy_process); + println!("{copy_process:#?}"); // Check for errors. let exit_code = copy_process @@ -355,7 +355,7 @@ fn patch_client_libs() -> io::Result<()> { quick_copy_dir( format!("{CLIENT_PATH}/node_modules/mathjax/"), format!("{CLIENT_PATH}/static/mathjax"), - Some("tex-chtml.js".to_string()), + Some(&"tex-chtml.js".to_string()), )?; quick_copy_dir( format!("{CLIENT_PATH}/node_modules/@mathjax/mathjax-newcm-font/chtml/"), @@ -508,7 +508,7 @@ fn run_format_and_lint(check_only: bool) -> io::Result<()> { )?; let mut eslint_args = vec!["eslint", "src"]; if !eslint_check.is_empty() { - eslint_args.push(eslint_check) + eslint_args.push(eslint_check); } run_script("npx", &eslint_args, CLIENT_PATH, true)?; run_script("npx", &eslint_args, VSCODE_PATH, true)?; @@ -770,7 +770,7 @@ fn run_change_version(new_version: &String) -> io::Result<()> { )?; search_and_replace_file( format!("{CLIENT_PATH}/package.json5"), - r#"(\r?\n version: ')[\d.]+(?:-[a-z\d]*)?(',\r?\n)"#, + r"(\r?\n version: ')[\d.]+(?:-[a-z\d]*)?(',\r?\n)", &replacement_string, )?; Ok(()) diff --git a/extensions/VSCode/Cargo.toml b/extensions/VSCode/Cargo.toml index 22ab5fd7..7a85961f 100644 --- a/extensions/VSCode/Cargo.toml +++ b/extensions/VSCode/Cargo.toml @@ -52,3 +52,8 @@ codegen-units = 1 lto = true panic = "abort" strip = "symbols" + +[lints.clippy] +pedantic = { level = "warn", priority = 0 } +missing_errors_doc = { level = "allow", priority = 1 } +doc_markdown = { level = "allow", priority = 1 } diff --git a/extensions/VSCode/src/lib.rs b/extensions/VSCode/src/lib.rs index a828aef2..8722736b 100644 --- a/extensions/VSCode/src/lib.rs +++ b/extensions/VSCode/src/lib.rs @@ -56,7 +56,7 @@ impl CodeChatEditorServer { #[napi(constructor)] pub fn new(capture_spool_path: String) -> Result { Ok(CodeChatEditorServer( - ide::CodeChatEditorServer::new_with_capture_spool(PathBuf::from(capture_spool_path))?, + ide::CodeChatEditorServer::new_with_capture_spool(&PathBuf::from(capture_spool_path))?, )) } @@ -96,13 +96,15 @@ impl CodeChatEditorServer { } #[napi] + // We must pass `base_url` as a `String` per NAPI requirements. + #[allow(clippy::needless_pass_by_value)] pub fn configure_capture_service( &self, base_url: String, token: Option, ) -> Result<(), Error> { self.0 - .configure_capture_service(base_url, token) + .configure_capture_service(&base_url, token) .map_err(|err| Error::new(Status::GenericFailure, err)) } @@ -174,6 +176,6 @@ impl CodeChatEditorServer { // This returns after the server shuts down. #[napi] pub async fn stop_server(&self) { - self.0.stop_server().await + self.0.stop_server().await; } } diff --git a/server/Cargo.toml b/server/Cargo.toml index 08d06b3b..b682bcc5 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -63,7 +63,6 @@ html5ever = "0.39" htmlize = "1.0.6" imara-diff = { version = "0.2", features = [] } indoc = "2.0.5" -lazy_static = "1" # Ideally, we'd set this in a non-library place, but this also builds a CLI. # TODO: separate the CLI into a different crate. log = { version = "0.4", features = ["release_max_level_warn"] } @@ -163,10 +162,20 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)'] } # https://github.com/rust-lang/rust-clippy shows which categories are enabled by # default; https://doc.rust-lang.org/stable/clippy/lints.html for a description # of each of these categories. -pedantic = "warn" +# +# Individual lints below need an explicit `priority` higher than this group's +# implicit 0, or Cargo errors with "lint group `pedantic` has the same +# priority (0) as a lint" (see +# https://doc.rust-lang.org/cargo/reference/manifest.html#the-lints-section). +pedantic = { level = "warn", priority = 0 } # Disable specific pedantic lints that don't apply. Use LP docs instead of # rustdoc for errors and panics. -missing_errors_doc = "allow" -missing_panics_doc = "allow" +missing_errors_doc = { level = "allow", priority = 1 } +missing_panics_doc = { level = "allow", priority = 1 } # This warns at the phrase "CodeChat Editor", so it's disabled. -doc_markdown = "allow" +doc_markdown = { level = "allow", priority = 1 } +# TODO: allow this in tests, but be more careful in the main codebase. +too_many_lines = { level = "allow", priority = 1 } +float_cmp = { level = "allow", priority = 1 } +# TODO: check/fix later. +cast_possible_truncation = { level = "allow", priority = 1 } diff --git a/server/src/capture.rs b/server/src/capture.rs index 21e1bb26..22bfb9e5 100644 --- a/server/src/capture.rs +++ b/server/src/capture.rs @@ -105,6 +105,7 @@ pub enum CaptureEventType { } impl CaptureEventType { + #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::WriteDoc => "write_doc", @@ -138,6 +139,7 @@ impl std::fmt::Display for CaptureEventType { /// Hash a local file path before it enters capture storage. The hash is stable /// enough to group edits to the same file while avoiding raw path collection. +#[must_use] pub fn hash_capture_path(path: &str) -> String { hash_capture_text(path) } @@ -147,10 +149,13 @@ fn hash_capture_token(token: &str) -> String { } fn hash_capture_text(value: &str) -> String { + use std::fmt::Write; Sha256::digest(value.as_bytes()) .iter() - .map(|byte| format!("{byte:02x}")) - .collect() + .fold(String::new(), |mut acc, byte| { + let _ = write!(acc, "{byte:02x}"); + acc + }) } /// JSON payload received from local clients for capture events. @@ -317,8 +322,8 @@ pub struct CaptureServiceConfig { } impl CaptureServiceConfig { - fn configured(base_url: String, token: Option) -> Result { - let base_url = normalize_service_base_url(&base_url)?; + fn configured(base_url: &str, token: Option) -> Result { + let base_url = normalize_service_base_url(base_url)?; Ok(Self { base_url: Some(base_url), token: token.filter(|token| !token.trim().is_empty()), @@ -462,6 +467,7 @@ pub struct CaptureStatus { } impl CaptureStatus { + #[must_use] pub fn disabled() -> Self { Self { enabled: false, @@ -517,6 +523,7 @@ pub struct CaptureEvent { } impl CaptureEvent { + #[must_use] pub fn new( user_id: String, file_hash: Option, @@ -541,6 +548,7 @@ impl CaptureEvent { } #[allow(clippy::too_many_arguments)] + #[must_use] pub fn with_columns( event_id: Option, sequence_number: Option, @@ -571,6 +579,7 @@ impl CaptureEvent { } } + #[must_use] pub fn now( user_id: String, file_hash: Option, @@ -732,7 +741,7 @@ impl EventCapture { thread::Builder::new() .name("codechat-capture-upload".to_string()) - .spawn(move || upload_worker(rx, config_worker, status_worker, spool_worker)) + .spawn(move || upload_worker(&rx, &config_worker, &status_worker, &spool_worker)) .map_err(|err| { io::Error::other(format!("Capture: failed to start upload worker: {err}")) })?; @@ -745,7 +754,7 @@ impl EventCapture { }) } - pub fn configure_service(&self, base_url: String, token: Option) -> Result<(), String> { + pub fn configure_service(&self, base_url: &str, token: Option) -> Result<(), String> { let mut new_config = CaptureServiceConfig::configured(base_url, token)?; let token_status = if new_config.token().is_some() { CaptureTokenStatus::Unverified @@ -764,7 +773,7 @@ impl EventCapture { status.enabled = true; status.state = CaptureState::Spooling; status.token_status = token_status; - status.service_base_url = new_config.base_url.clone(); + status.service_base_url.clone_from(&new_config.base_url); status.participant_id = None; status.instance_id = None; status.capture_enabled = None; @@ -826,7 +835,7 @@ impl EventCapture { /// Durably append an event to the local FIFO spool, then ask the worker to /// upload as soon as service access permits. - pub fn log(&self, event: CaptureEvent) { + pub fn log(&self, event: &CaptureEvent) { debug!( "Capture: spooling event: type={}, user_id={}, file_hash={:?}", event.event_type, event.user_id, event.file_hash @@ -846,7 +855,7 @@ impl EventCapture { } }; - match append_spool_event(&self.spool_path, &event, spool_identity) { + match append_spool_event(&self.spool_path, event, spool_identity) { Ok(()) => { update_status(&self.status, |status| { if matches!( @@ -881,15 +890,16 @@ impl EventCapture { } } + #[must_use] pub fn status(&self) -> CaptureStatus { - self.status - .lock() - .map(|status| status.clone()) - .unwrap_or_else(|_| { + self.status.lock().map_or_else( + |_| { let mut status = CaptureStatus::disabled(); status.last_error = Some("Capture status lock is poisoned".to_string()); status - }) + }, + |status| status.clone(), + ) } } @@ -901,15 +911,18 @@ fn update_status(status: &Arc>, f: impl FnOnce(&mut Capture } fn upload_worker( - rx: mpsc::Receiver, - config: Arc>, - status: Arc>, - spool_path: PathBuf, + rx: &mpsc::Receiver, + config: &Arc>, + status: &Arc>, + spool_path: &Path, ) { - info!("Capture: upload worker started with spool at {spool_path:?}."); + info!( + "Capture: upload worker started with spool at {}.", + spool_path.display() + ); let mut retry_delay = Duration::from_millis(INITIAL_RETRY_DELAY_MS); loop { - match upload_next_batch(&spool_path, &config, &status) { + match upload_next_batch(spool_path, config, status) { UploadOutcome::UploadedBatch => { retry_delay = Duration::from_millis(INITIAL_RETRY_DELAY_MS); continue; @@ -927,7 +940,7 @@ fn upload_worker( } let wait_for = if matches!( - capture_status_state(&status), + capture_status_state(status), CaptureState::ServiceUnavailable ) { retry_delay @@ -936,8 +949,7 @@ fn upload_worker( }; match rx.recv_timeout(wait_for) { - Ok(WorkerMsg::Flush) => {} - Err(RecvTimeoutError::Timeout) => {} + Ok(WorkerMsg::Flush) | Err(RecvTimeoutError::Timeout) => {} Err(RecvTimeoutError::Disconnected) => { warn!("Capture: upload worker channel closed; worker exiting."); break; @@ -945,7 +957,7 @@ fn upload_worker( } if matches!( - capture_status_state(&status), + capture_status_state(status), CaptureState::ServiceUnavailable ) { retry_delay = retry_delay @@ -958,8 +970,7 @@ fn upload_worker( fn capture_status_state(status: &Arc>) -> CaptureState { status .lock() - .map(|status| status.state) - .unwrap_or(CaptureState::Disabled) + .map_or(CaptureState::Disabled, |status| status.state) } #[derive(Debug, PartialEq, Eq)] @@ -1078,7 +1089,10 @@ fn upload_next_batch( if !run_if_capture_config_snapshot_is_current(config, &cfg, || { for file in &batch.files { if let Err(err) = fs::remove_file(file) { - warn!("Capture: unable to remove uploaded spool file {file:?}: {err}"); + warn!( + "Capture: unable to remove uploaded spool file {}: {err}", + file.display() + ); } } update_status(status, |status| { @@ -1099,11 +1113,7 @@ fn upload_next_batch( UploadOutcome::UploadedBatch } Err(err) => match err.status_code { - Some(401) => { - apply_http_error_if_current(config, status, &cfg, &err); - UploadOutcome::Paused - } - Some(403) => { + Some(401 | 403) => { apply_http_error_if_current(config, status, &cfg, &err); UploadOutcome::Paused } @@ -1333,8 +1343,7 @@ fn spool_record_matches_current_identity(record: &SpoolRecord, cfg: &CaptureServ cfg.participant_id .as_deref() - .map(|participant_id| record.event.user_id == participant_id) - .unwrap_or(false) + .is_some_and(|participant_id| record.event.user_id == participant_id) } fn read_spool_record(path: &Path) -> Result { @@ -1593,10 +1602,10 @@ fn quarantine_files( } for file in files { - let file_name = file - .file_name() - .map(|name| name.to_owned()) - .unwrap_or_else(|| "capture-event.json".into()); + let file_name = file.file_name().map_or_else( + || "capture-event.json".into(), + std::borrow::ToOwned::to_owned, + ); let target = quarantine.join(file_name); if let Err(err) = fs::rename(file, &target).or_else(|_| { fs::copy(file, &target)?; @@ -1604,13 +1613,16 @@ fn quarantine_files( }) { update_status(status, |status| { status.failed_events += 1; - status.last_error = Some(format!("Unable to quarantine {file:?}: {err}")); + status.last_error = Some(format!("Unable to quarantine {}: {err}", file.display())); }); continue; } let reason_path = target.with_extension("reason.txt"); if let Err(err) = fs::write(&reason_path, reason) { - warn!("Capture: unable to write quarantine reason {reason_path:?}: {err}"); + warn!( + "Capture: unable to write quarantine reason \"{}\": {err}", + reason_path.display() + ); } update_status(status, |status| { status.quarantined_events += 1; @@ -1658,7 +1670,7 @@ mod tests { fn capture_service_config(token: &str, generation: u64) -> CaptureServiceConfig { let mut cfg = CaptureServiceConfig::configured( - "https://capture.example/dev".to_string(), + "https://capture.example/dev", Some(token.to_string()), ) .expect("capture service config should parse"); @@ -1672,9 +1684,8 @@ mod tests { generation: u64, participant_id: &str, ) -> CaptureServiceConfig { - let mut cfg = - CaptureServiceConfig::configured(base_url.to_string(), Some(token.to_string())) - .expect("capture service config should parse"); + let mut cfg = CaptureServiceConfig::configured(base_url, Some(token.to_string())) + .expect("capture service config should parse"); cfg.generation = generation; cfg.participant_id = Some(participant_id.to_string()); cfg.instance_id = Some(format!("{participant_id}-instance")); @@ -1827,7 +1838,7 @@ mod tests { let _ = fs::remove_dir_all(&spool_path); let capture = EventCapture::new(spool_path.clone()).expect("capture worker should start"); - capture.log(CaptureEvent::with_columns( + capture.log(&CaptureEvent::with_columns( Some("event-1".to_string()), Some(1), Some(2), @@ -2151,14 +2162,14 @@ mod tests { let _ = fs::remove_dir_all(&spool_path); let mut old_cfg = CaptureServiceConfig::configured( - "https://capture.example/dev".to_string(), + "https://capture.example/dev", Some("old-token".to_string()), ) .expect("old config should parse"); old_cfg.participant_id = Some("old-user".to_string()); old_cfg.instance_id = Some("old-instance".to_string()); let mut new_cfg = CaptureServiceConfig::configured( - "https://capture.example/dev".to_string(), + "https://capture.example/dev", Some("new-token".to_string()), ) .expect("new config should parse"); @@ -2208,12 +2219,12 @@ mod tests { let _ = fs::remove_dir_all(&spool_path); let old_cfg = CaptureServiceConfig::configured( - "https://capture.example/dev".to_string(), + "https://capture.example/dev", Some("old-token".to_string()), ) .expect("old config should parse"); let mut new_cfg = CaptureServiceConfig::configured( - "https://capture.example/dev".to_string(), + "https://capture.example/dev", Some("new-token".to_string()), ) .expect("new config should parse"); @@ -2246,7 +2257,7 @@ mod tests { let _ = fs::remove_dir_all(&spool_path); let cfg = CaptureServiceConfig::configured( - "https://capture.example/dev".to_string(), + "https://capture.example/dev", Some("token".to_string()), ) .expect("config should parse"); diff --git a/server/src/ide.rs b/server/src/ide.rs index e7d8cede..69bb43fb 100644 --- a/server/src/ide.rs +++ b/server/src/ide.rs @@ -41,6 +41,7 @@ pub mod vscode; use std::{ collections::HashMap, net::{IpAddr, Ipv4Addr, SocketAddr}, + path::Path, sync::Arc, thread, time::Duration, @@ -109,16 +110,16 @@ impl CodeChatEditorServer { // Creating the server could fail, so this must return an `io::Result`. pub fn new() -> std::io::Result { let capture_spool_path = webserver::ROOT_PATH.lock().unwrap().join("capture-spool"); - Self::new_with_capture_spool(capture_spool_path) + Self::new_with_capture_spool(&capture_spool_path) } pub fn new_with_capture_spool( - capture_spool_path: std::path::PathBuf, + capture_spool_path: &Path, ) -> std::io::Result { // Start the server. let (server, app_state) = webserver::setup_server_with_capture_spool( // A port of 0 requests the OS to assign an open port. - &SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0), + &SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), None, capture_spool_path, )?; @@ -197,7 +198,7 @@ impl CodeChatEditorServer { // Like `get_message`, but with a timeout. pub async fn get_message_timeout(&self, timeout: Duration) -> Option { select! { - _ = sleep(timeout) => None, + () = sleep(timeout) => None, v = self.get_message() => v } } @@ -271,13 +272,14 @@ impl CodeChatEditorServer { .await } + #[must_use] pub fn capture_status(&self) -> crate::capture::CaptureStatus { webserver::capture_status(&self.app_state) } pub fn configure_capture_service( &self, - base_url: String, + base_url: &str, token: Option, ) -> Result<(), String> { self.app_state @@ -333,7 +335,7 @@ impl CodeChatEditorServer { metadata: SourceFileMetadata { // The IDE doesn't need to provide the `mode`; this will // determined by the server. - mode: "".to_string(), + mode: String::new(), }, source: CodeMirrorDiffable::Plain(CodeMirror { doc: contents.0, diff --git a/server/src/ide/filewatcher.rs b/server/src/ide/filewatcher.rs index 01c3588b..40cb6c12 100644 --- a/server/src/ide/filewatcher.rs +++ b/server/src/ide/filewatcher.rs @@ -20,7 +20,9 @@ // // ### Standard library use std::{ + fmt::Write, path::{Path, PathBuf}, + sync::LazyLock, time::Duration, }; @@ -34,7 +36,6 @@ use actix_web::{ }; use dunce::simplified; use indoc::formatdoc; -use lazy_static::lazy_static; use log::{error, info, warn}; use notify_debouncer_full::{ DebounceEventResult, new_debouncer, @@ -74,10 +75,8 @@ use crate::{ // Globals // ------- -lazy_static! { - /// Matches a bare drive letter. Only needed on Windows. - static ref DRIVE_LETTER_REGEX: Regex = Regex::new("^[a-zA-Z]:$").unwrap(); -} +/// Matches a bare drive letter. Only needed on Windows. +static DRIVE_LETTER_REGEX: LazyLock = LazyLock::new(|| Regex::new("^[a-zA-Z]:$").unwrap()); pub const FILEWATCHER_PATH_PREFIX: &[&str] = &["fw", "fsc"]; @@ -146,7 +145,7 @@ async fn filewatcher_browser_endpoint( } else if canon_path.is_file() { // Get an ID for this connection. let connection_id_raw = get_connection_id_raw(&app_state); - return processing_task(&canon_path, req, app_state, connection_id_raw).await; + return processing_task(&canon_path, &req, app_state, connection_id_raw); } // It's not a directory or a file...we give up. For simplicity, don't handle @@ -176,9 +175,10 @@ async fn dir_listing(web_path: &str, dir_path: &Path) -> HttpResponse { Err(err) => return http_not_found(&format!("Unable to list drive letters: {err}.")), }; for drive_letter in logical_drives { - drive_html.push_str(&format!( - "

  • {drive_letter}:
  • \n" - )); + let _ = writeln!( + drive_html, + "
  • {drive_letter}:
  • " + ); } return HttpResponse::Ok() @@ -233,7 +233,7 @@ async fn dir_listing(web_path: &str, dir_path: &Path) -> HttpResponse { Err(err) => { return http_not_found(&format!("Unable to read file in directory: {err}.")); } - }; + } } // Sort them -- case-insensitive on Windows, normally on Linux/OS X. #[cfg(target_os = "windows")] @@ -258,13 +258,15 @@ async fn dir_listing(web_path: &str, dir_path: &Path) -> HttpResponse { Ok(v) => v, Err(err) => { return http_not_found(&format!( - "Unable to decode directory name '{err:?}' as UTF-8." + "Unable to decode directory name '{}' as UTF-8.", + Path::new(&err).display() )); } }; let encoded_dir = urlencoding::encode(&dir_name); - dir_html += &format!( - "
  • {dir_name}
  • \n", + let _ = writeln!( + dir_html, + "
  • {dir_name}
  • " ); } @@ -274,7 +276,10 @@ async fn dir_listing(web_path: &str, dir_path: &Path) -> HttpResponse { let file_name = match file.file_name().into_string() { Ok(v) => v, Err(err) => { - return http_not_found(&format!("Unable to decode file name {err:?} as UTF-8.",)); + return http_not_found(&format!( + "Unable to decode file name {} as UTF-8.", + Path::new(&err).display() + )); } }; let encoded_file = urlencoding::encode(&file_name); @@ -354,9 +359,9 @@ async fn filewatcher_client_endpoint( .await } -async fn processing_task( +fn processing_task( file_path: &Path, - req: HttpRequest, + req: &HttpRequest, app_state: WebAppState, connection_id_raw: u32, ) -> Result { @@ -373,7 +378,7 @@ async fn processing_task( // // The path to the currently open CodeChat Editor file. let Ok(current_filepath) = file_path.to_path_buf().canonicalize() else { - let msg = format!("Unable to canonicalize path {file_path:?}."); + let msg = format!("Unable to canonicalize path {}.", file_path.display()); error!("{msg}"); return Err(error::ErrorBadRequest(msg)); }; @@ -400,13 +405,12 @@ async fn processing_task( // Transfer the queues from the global state to this task. let (from_ide_tx, mut to_ide_rx) = - match app_state.ide_queues.lock().unwrap().remove(&connection_id) { - Some(queues) => (queues.from_websocket_tx.clone(), queues.to_websocket_rx), - None => { - let err = format!("No websocket queues for connection id {connection_id}."); - error!("{err}"); - return Err(error::ErrorBadRequest(err)); - } + if let Some(queues) = app_state.ide_queues.lock().unwrap().remove(&connection_id) { + (queues.from_websocket_tx.clone(), queues.to_websocket_rx) + } else { + let err = format!("No websocket queues for connection id {connection_id}."); + error!("{err}"); + return Err(error::ErrorBadRequest(err)); }; // #### The filewatcher task. @@ -451,7 +455,10 @@ async fn processing_task( // Provide it a file to open. if let Some(cfp) = ¤t_filepath { let Some(cfp_str) = cfp.to_str() else { - let err = format!("Unable to convert file path {cfp:?} to string."); + let err = format!( + "Unable to convert file path \"{}\" to string.", + cfp.display() + ); error!("{err}"); break 'task; }; @@ -463,7 +470,7 @@ async fn processing_task( // `queue_send` exits before this runs, the message didn't get // sent, so the ID wasn't used. id += MESSAGE_ID_INCREMENT; - }; + } shutdown_only = false; } @@ -526,7 +533,7 @@ async fn processing_task( } else { let cfp = current_filepath.as_ref().unwrap(); let Some(current_filepath_str) = cfp.to_str() else { - error!("Unable to convert path {cfp:?} to string."); + error!("Unable to convert path \"{}\" to string.", cfp.display()); break 'task; }; @@ -562,7 +569,7 @@ async fn processing_task( contents: Some(CodeChatForWeb { metadata: SourceFileMetadata { // The IDE doesn't need to provide this. - mode: "".to_string(), + mode: String::new(), }, source: crate::processing::CodeMirrorDiffable::Plain(CodeMirror { doc: file_contents, @@ -574,6 +581,7 @@ async fn processing_task( // stays in sync with any diffs. Produce a // whole number to avoid encoding // difference with fractional values. + #[allow(clippy::cast_precision_loss)] version: random::() as f64, }), }), @@ -598,9 +606,8 @@ async fn processing_task( break 'process Err(ResultErrTypes::WrongFileUpdate(update_message_contents.file_path, current_filepath.clone())); } // Without code, there's nothing to save. - let codechat_for_web = match update_message_contents.contents { - None => break 'process Ok(ResultOkTypes::Void), - Some(cfw) => cfw, + let Some(codechat_for_web) = update_message_contents.contents else { + break 'process Ok(ResultOkTypes::Void) }; // Translate from the CodeChatForWeb format to @@ -614,14 +621,14 @@ async fn processing_task( // it, in order to avoid a watch notification // from this write. if let Err(err) = debounced_watcher.unwatch(cfp) { - break 'process Err(ResultErrTypes::FileUnwatchError(cfp.to_path_buf(), err.to_string())); + break 'process Err(ResultErrTypes::FileUnwatchError(cfp.clone(), err.to_string())); } // Save this string to a file. if let Err(err) = fs::write(cfp.as_path(), plain.doc).await { - break 'process Err(ResultErrTypes::SaveFileError(cfp.to_path_buf(), err.to_string())); + break 'process Err(ResultErrTypes::SaveFileError(cfp.clone(), err.to_string())); } if let Err(err) = debounced_watcher.watch(cfp, RecursiveMode::NonRecursive) { - break 'process Err(ResultErrTypes::FileWatchError(cfp.to_path_buf(), err.to_string())); + break 'process Err(ResultErrTypes::FileWatchError(cfp.clone(), err.to_string())); } Ok(ResultOkTypes::Void) }; @@ -635,15 +642,15 @@ async fn processing_task( if let Some(cfp) = ¤t_filepath && let Err(err) = debounced_watcher.unwatch(cfp) { - break 'err_exit Err(ResultErrTypes::FileUnwatchError(cfp.to_path_buf(), err.to_string())); + break 'err_exit Err(ResultErrTypes::FileUnwatchError(cfp.clone(), err.to_string())); } // Update to the new path, which is already // canonicalized. - current_filepath = Some(file_path.to_path_buf()); + current_filepath = Some(file_path.clone()); // Watch the new file. if let Err(err) = debounced_watcher.watch(&file_path, RecursiveMode::NonRecursive) { - break 'err_exit Err(ResultErrTypes::FileWatchError(file_path.to_path_buf(), err.to_string())); + break 'err_exit Err(ResultErrTypes::FileWatchError(file_path.clone(), err.to_string())); } // Indicate there was no error in the `Result` // message. @@ -701,7 +708,7 @@ async fn processing_task( info!("Watcher closed."); }); - match get_client_framework(get_test_mode(&req), "fw/ws", &connection_id_raw.to_string()) { + match get_client_framework(get_test_mode(req), "fw/ws", &connection_id_raw.clone()) { Ok(s) => Ok(HttpResponse::Ok().content_type(ContentType::html()).body(s)), Err(err) => Err(error::ErrorBadRequest(err)), } @@ -717,13 +724,14 @@ pub async fn filewatcher_websocket( ) -> Result { client_websocket( format!("{FW}{connection_id_raw}"), - req, + &req, body, app_state.client_queues.clone(), ) } /// Return a unique ID for an IDE websocket connection. +#[must_use] pub fn get_connection_id_raw(app_state: &WebAppState) -> u32 { let mut connection_id_raw = app_state.filewatcher_next_connection_id.lock().unwrap(); *connection_id_raw += 1; @@ -815,7 +823,7 @@ mod tests { println!("{} - {:?}", m.id, m.message); m } - _ = sleep(Duration::from_secs(3)) => { + () = sleep(Duration::from_secs(3)) => { // The backtrace shows what message the code was waiting for; // otherwise, it's an unhelpful error message. panic!("Timeout waiting for message:\n{}", Backtrace::force_capture()); @@ -985,7 +993,7 @@ mod tests { ), ( INITIAL_CLIENT_MESSAGE_ID + 2.0 * MESSAGE_ID_INCREMENT, - EditorMessageContents::ClientHtml("".to_string()), + EditorMessageContents::ClientHtml(String::new()), ), ( INITIAL_CLIENT_MESSAGE_ID + 3.0 * MESSAGE_ID_INCREMENT, @@ -1011,7 +1019,7 @@ mod tests { .send(EditorMessage { id: INITIAL_CLIENT_MESSAGE_ID + 4.0 * MESSAGE_ID_INCREMENT, message: EditorMessageContents::Update(UpdateMessageContents { - file_path: "".to_string(), + file_path: String::new(), cursor_position: None, scroll_position: None, is_re_translation: false, @@ -1020,7 +1028,7 @@ mod tests { mode: "cpp".to_string(), }, source: CodeMirrorDiffable::Plain(CodeMirror { - doc: "".to_string(), + doc: String::new(), doc_blocks: vec![], }), version: 0.0, diff --git a/server/src/ide/vscode.rs b/server/src/ide/vscode.rs index b6fa791e..a6a7f2d6 100644 --- a/server/src/ide/vscode.rs +++ b/server/src/ide/vscode.rs @@ -78,7 +78,7 @@ pub async fn vscode_ide_websocket( CreateTranslationQueuesError::IdeInUse(connection_id_str) => { return client_websocket( connection_id_str, - req, + &req, body, app_state.ide_queues.clone(), ); @@ -89,11 +89,12 @@ pub async fn vscode_ide_websocket( // Move data between the IDE and the processing task via queues. The // websocket connection between the client and the IDE will run in the // endpoint for that connection. - client_websocket(connection_id, req, body, app_state.ide_queues.clone()) + client_websocket(connection_id, &req, body, app_state.ide_queues.clone()) } // Given a (random) connection ID produced by the IDE, return a string that // gives a VSCode-specific ID. +#[must_use] pub fn connection_id_raw_to_str(connection_id_raw: &str) -> String { format!("{VSC}{connection_id_raw}") } @@ -126,7 +127,7 @@ pub fn vscode_ide_core( // Make sure it's the `Opened` message. let EditorMessageContents::Opened(ide_type) = first_message.message else { - let err = ResultErrTypes::UnexpectedMessage(format!("{:#?}", first_message)); + let err = ResultErrTypes::UnexpectedMessage(format!("{first_message:#?}")); error!("{err}"); send_response(&to_ide_tx, first_message.id, Err(err)).await; @@ -137,113 +138,110 @@ pub fn vscode_ide_core( debug!("Received IDE Opened message."); // Ensure the IDE type (VSCode) is correct. - match ide_type { - IdeType::VSCode(is_self_hosted) => { - // Get the address for the server. - let port = *app_state_task.port.lock().unwrap(); - let address = match get_server_url(port).await { - Ok(address) => address, - Err(err) => { - error!("{err:?}"); - break 'task; - } - }; - if is_self_hosted { - // Send a response (successful) to the `Opened` message. - debug!( - "Sending response = OK to IDE Opened message, id {}.", - first_message.id - ); - send_response(&to_ide_tx, first_message.id, Ok(ResultOkTypes::Void)).await; - // Send the HTML for the internal browser. The ID of - // this message is `RESERVED_MESSAGE_ID`. - let client_html = formatdoc!( - r#" - - - - - - - - "# - ); - debug!("Sending ClientHtml message to IDE: {client_html}"); - queue_send!(to_ide_tx.send(EditorMessage { - id: RESERVED_MESSAGE_ID, - message: EditorMessageContents::ClientHtml(client_html) - }), 'task); + if let IdeType::VSCode(is_self_hosted) = ide_type { + // Get the address for the server. + let port = *app_state_task.port.lock().unwrap(); + let address = match get_server_url(port).await { + Ok(address) => address, + Err(err) => { + error!("{err:?}"); + break 'task; + } + }; + if is_self_hosted { + // Send a response (successful) to the `Opened` message. + debug!( + "Sending response = OK to IDE Opened message, id {}.", + first_message.id + ); + send_response(&to_ide_tx, first_message.id, Ok(ResultOkTypes::Void)).await; + // Send the HTML for the internal browser. The ID of + // this message is `RESERVED_MESSAGE_ID`. + let client_html = formatdoc!( + r#" + + + + + + + + "# + ); + debug!("Sending ClientHtml message to IDE: {client_html}"); + queue_send!(to_ide_tx.send(EditorMessage { + id: RESERVED_MESSAGE_ID, + message: EditorMessageContents::ClientHtml(client_html) + }), 'task); - // Wait for the response. - let Some(message) = from_ide_rx.recv().await else { - error!("{}", "IDE websocket received no data."); - break 'task; - }; + // Wait for the response. + let Some(message) = from_ide_rx.recv().await else { + error!("{}", "IDE websocket received no data."); + break 'task; + }; - // Make sure it's the `Result` message with no errors. - let res = - // First, make sure the ID matches. - if message.id != RESERVED_MESSAGE_ID { - Err(format!("Unexpected message ID {}.", message.id)) - } else { - match message.message { - EditorMessageContents::Result(message_result) => match message_result { - Err(err) => Err(format!("Error in ClientHtml: {err}")), - Ok(result_ok) => - if let ResultOkTypes::Void = result_ok { - Ok(()) - } else { - Err(format!( - "Unexpected Result message with LoadFile contents {result_ok:?}." - )) - } - }, - _ => Err(format!("Unexpected message {message:?}")), - } - }; - if let Err(err) = res { - error!("{err}"); - // Send a `Closed` message using the next available - // ID (`RESERVED_MESSAGE_ID + 1`). - queue_send!(to_ide_tx.send(EditorMessage { - id: RESERVED_MESSAGE_ID + 1.0, - message: EditorMessageContents::Closed - }), 'task); - break 'task; + // Make sure it's the `Result` message with no errors. + let res = + // First, make sure the ID matches. + if message.id == RESERVED_MESSAGE_ID { + match message.message { + EditorMessageContents::Result(message_result) => match message_result { + Err(err) => Err(format!("Error in ClientHtml: {err}")), + Ok(result_ok) => + if let ResultOkTypes::Void = result_ok { + Ok(()) + } else { + Err(format!( + "Unexpected Result message with LoadFile contents {result_ok:?}." + )) + } + }, + _ => Err(format!("Unexpected message {message:?}")), + } + } else { + Err(format!("Unexpected message ID {}.", message.id)) }; - } else { - // Open the Client in an external browser. - if let Err(err) = - webbrowser::open(&format!("{address}/vsc/cf/{connection_id_raw}")) - { - let err = ResultErrTypes::WebBrowserOpenFailed(err.to_string()); - error!("{err:?}"); - send_response(&to_ide_tx, first_message.id, Err(err)).await; + if let Err(err) = res { + error!("{err}"); + // Send a `Closed` message using the next available + // ID (`RESERVED_MESSAGE_ID + 1`). + queue_send!(to_ide_tx.send(EditorMessage { + id: RESERVED_MESSAGE_ID + 1.0, + message: EditorMessageContents::Closed + }), 'task); + break 'task; + } + } else { + // Open the Client in an external browser. + if let Err(err) = + webbrowser::open(&format!("{address}/vsc/cf/{connection_id_raw}")) + { + let err = ResultErrTypes::WebBrowserOpenFailed(err.to_string()); + error!("{err:?}"); + send_response(&to_ide_tx, first_message.id, Err(err)).await; - // Send a `Closed` message; use an ID of - // `RESERVED_MESSAGE_ID`, since this is the first - // message from the IDE. - queue_send!(to_ide_tx.send(EditorMessage{ - id: RESERVED_MESSAGE_ID, - message: EditorMessageContents::Closed - }), 'task); - break 'task; - } - // Send a response (successful) to the `Opened` message. - send_response(&to_ide_tx, first_message.id, Ok(ResultOkTypes::Void)).await; + // Send a `Closed` message; use an ID of + // `RESERVED_MESSAGE_ID`, since this is the first + // message from the IDE. + queue_send!(to_ide_tx.send(EditorMessage{ + id: RESERVED_MESSAGE_ID, + message: EditorMessageContents::Closed + }), 'task); + break 'task; } + // Send a response (successful) to the `Opened` message. + send_response(&to_ide_tx, first_message.id, Ok(ResultOkTypes::Void)).await; } - _ => { - // This is the wrong IDE type. Report the error. - let err = ResultErrTypes::InvalidIdeType(ide_type); - error!("{err:?}"); - send_response(&to_ide_tx, first_message.id, Err(err)).await; + } else { + // This is the wrong IDE type. Report the error. + let err = ResultErrTypes::InvalidIdeType(ide_type); + error!("{err:?}"); + send_response(&to_ide_tx, first_message.id, Err(err)).await; - // Close the connection, again using the first available ID - // of `RESERVED_MESSAGE_ID`. - queue_send!(to_ide_tx.send(EditorMessage { id: RESERVED_MESSAGE_ID, message: EditorMessageContents::Closed}), 'task); - break 'task; - } + // Close the connection, again using the first available ID + // of `RESERVED_MESSAGE_ID`. + queue_send!(to_ide_tx.send(EditorMessage { id: RESERVED_MESSAGE_ID, message: EditorMessageContents::Closed}), 'task); + break 'task; } shutdown_only = false; } @@ -288,7 +286,7 @@ pub async fn vscode_client_websocket( ) -> Result { client_websocket( format!("{VSC}{connection_id}"), - req, + &req, body, app_state.client_queues.clone(), ) diff --git a/server/src/ide/vscode/tests.rs b/server/src/ide/vscode/tests.rs index 226ee88a..daba557c 100644 --- a/server/src/ide/vscode/tests.rs +++ b/server/src/ide/vscode/tests.rs @@ -23,6 +23,7 @@ use std::{ io::{Error, Read}, net::SocketAddr, path::{self, Path, PathBuf}, + sync::LazyLock, thread::{self, JoinHandle}, time::{Duration, SystemTime}, }; @@ -32,7 +33,6 @@ use assertables::{assert_contains, assert_ends_with, assert_starts_with}; use dunce::simplified; use futures_util::{SinkExt, StreamExt}; use indoc::indoc; -use lazy_static::lazy_static; use minreq; use path_slash::PathExt; use pretty_assertions::assert_eq; @@ -69,18 +69,17 @@ use test_utils::{ // Globals // ------- -lazy_static! { - // Run a single webserver for all tests. - static ref WEBSERVER_HANDLE: JoinHandle> = - thread::spawn(|| { - main( - None, - &SocketAddr::new("127.0.0.1".parse().unwrap(), IP_PORT), - None, - log::LevelFilter::Debug - ) - }); -} +// Run a single webserver for all tests. +static WEBSERVER_HANDLE: LazyLock>> = LazyLock::new(|| { + thread::spawn(|| { + main( + None, + &SocketAddr::new("127.0.0.1".parse().unwrap(), IP_PORT), + None, + log::LevelFilter::Debug, + ) + }) +}); // Constants // --------- @@ -112,7 +111,7 @@ async fn read_message( let msg_txt = loop { let msg = select! { data = ws_stream.next() => data.unwrap().unwrap(), - _ = sleep(Duration::from_secs(6) - now.elapsed().unwrap()) => panic!("Timeout waiting for message") + () = sleep(Duration::from_secs(6).checked_sub(now.elapsed().unwrap()).unwrap()) => panic!("Timeout waiting for message") }; match msg { Message::Close(_) => panic!("Unexpected close message."), @@ -130,7 +129,7 @@ async fn read_message( type WebSocketStreamTcp = WebSocketStream>; async fn connect_async_server(prefix: &str, connection_id: &str) -> WebSocketStreamTcp { - connect_async(format!("ws://127.0.0.1:{IP_PORT}{prefix}/{connection_id}",)) + connect_async(format!("ws://127.0.0.1:{IP_PORT}{prefix}/{connection_id}")) .await .expect("Failed to connect") .0 @@ -194,7 +193,7 @@ async fn open_client(ws_ide: &mut WebSocketSt /// Perform all the setup for testing the Server via IDE and Client websockets. /// This should be invoked by the `prep_test!` macro; otherwise, test files /// won't be found. -async fn _prep_test( +async fn prep_test_core( connection_id: &str, test_full_name: &str, ) -> (TempDir, PathBuf, WebSocketStreamTcp, WebSocketStreamTcp) { @@ -205,7 +204,7 @@ async fn _prep_test( let now = SystemTime::now(); let mut started = false; while now.elapsed().unwrap() < WEBSERVER_START_TIMEOUT { - if minreq::get(format!("http://127.0.0.1:{IP_PORT}/ping",)) + if minreq::get(format!("http://127.0.0.1:{IP_PORT}/ping")) .send() .is_ok_and(|response| response.as_bytes() == b"pong") { @@ -224,13 +223,13 @@ async fn _prep_test( (temp_dir, test_dir, ws_ide, ws_client) } -/// This calls `_prep_test` with the current function name. It must be a macro, +/// This calls `prep_test_core` with the current function name. It must be a macro, /// so that it's called with the test function's name; calling it inside -/// `_prep_test` would give the wrong name. +/// `prep_test_core` would give the wrong name. macro_rules! prep_test { ($connection_id: ident) => {{ use test_utils::function_name; - _prep_test($connection_id, function_name!()) + prep_test_core($connection_id, function_name!()) }}; } @@ -263,7 +262,7 @@ async fn test_vscode_ide_websocket1() { &EditorMessage { id: INITIAL_IDE_MESSAGE_ID, message: EditorMessageContents::Update(UpdateMessageContents { - file_path: "".to_string(), + file_path: String::new(), cursor_position: None, scroll_position: None, is_re_translation: false, @@ -349,7 +348,7 @@ async fn test_vscode_ide_websocket3() { .unwrap() .status_code, 404 - ) + ); }); // The HTTP request produces a `LoadFile` message. @@ -393,7 +392,7 @@ async fn test_vscode_ide_websocket3a() { let file_path = test_dir.join("test.py"); // Force the path separator to be Window-style for this test, even on // non-Windows platforms. - let file_path_str = file_path.to_str().unwrap().to_string().replace("/", "\\"); + let file_path_str = file_path.to_str().unwrap().to_string().replace('/', "\\"); // Since we expect a 404 error, wait some to ensure the 404 results from not // finding the requested file, instead of the web server not being fully @@ -412,7 +411,7 @@ async fn test_vscode_ide_websocket3a() { .unwrap() .status_code, 404 - ) + ); }); // The HTTP request produces a `LoadFile` message. @@ -507,7 +506,7 @@ async fn test_vscode_ide_websocket8() { .unwrap() .status_code, 200 - ) + ); }); // This should produce a `LoadFile` message. @@ -557,7 +556,7 @@ async fn test_vscode_ide_websocket8() { doc_blocks: vec![CodeMirrorDocBlock { from: 0, to: 1, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "

    testing".to_string() }], @@ -689,7 +688,7 @@ async fn test_vscode_ide_websocket7() { doc_blocks: vec![CodeMirrorDocBlock { from: 0, to: 1, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "

    more".to_string() }] @@ -769,7 +768,7 @@ async fn test_vscode_ide_websocket7() { doc_blocks: vec![CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { from: 6, to: 7, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "

    most".to_string(), },),], @@ -828,7 +827,7 @@ async fn test_vscode_ide_websocket6() { doc_blocks: vec![CodeMirrorDocBlock { from: 0, to: 1, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "less\n".to_string(), }], @@ -996,7 +995,7 @@ async fn test_vscode_ide_websocket4() { doc_blocks: vec![CodeMirrorDocBlock { from: 0, to: 1, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "

    test.py".to_string() }], diff --git a/server/src/lexer.rs b/server/src/lexer.rs index faa7aa1f..e8de7662 100644 --- a/server/src/lexer.rs +++ b/server/src/lexer.rs @@ -24,10 +24,13 @@ pub mod supported_languages; // ------- // // ### Standard library -use std::{cmp::min, collections::HashMap, sync::Arc}; +use std::{ + cmp::min, + collections::HashMap, + sync::{Arc, LazyLock}, +}; // ### Third-party -use lazy_static::lazy_static; use log::trace; use regex::Regex; @@ -263,12 +266,13 @@ pub enum CodeDocBlock { // // Create constant regexes needed by the lexer, following the // [Regex docs recommendation](https://docs.rs/regex/1.6.0/regex/index.html#example-avoid-compiling-the-same-regex-in-a-loop). -lazy_static! { - static ref WHITESPACE_ONLY_REGEX: Regex = Regex::new("^[[:space:]]*$").unwrap(); - /// TODO: This regex should also allow termination on an unescaped `${` - /// sequence, which then must count matching braces to find the end of the - /// expression. - static ref TEMPLATE_LITERAL_CLOSING_REGEX: Regex = Regex::new( +static WHITESPACE_ONLY_REGEX: LazyLock = + LazyLock::new(|| Regex::new("^[[:space:]]*$").unwrap()); +/// TODO: This regex should also allow termination on an unescaped `${` +/// sequence, which then must count matching braces to find the end of the +/// expression. +static TEMPLATE_LITERAL_CLOSING_REGEX: LazyLock = LazyLock::new(|| { + Regex::new( // Allow `.` to match *any* character, including a newline. See the // [regex docs](https://docs.rs/regex/1.6.0/regex/index.html#grouping-and-flags). &("(?s)".to_string() + @@ -285,11 +289,13 @@ lazy_static! { ")*" + // Now, find the end of the string: the string delimiter. "`"), - ).unwrap(); + ) + .unwrap() +}); - /// A vector of all supported languages. - pub static ref LEXERS: LanguageLexersCompiled = compile_lexers(get_language_lexer_vec()); -} +/// A vector of all supported languages. +pub static LEXERS: LazyLock = + LazyLock::new(|| compile_lexers(get_language_lexer_vec())); // Support C# verbatim string literals, which end with a `"`; a `""` inserts a // single " in the string. @@ -375,7 +381,7 @@ fn build_lexer_regex( // If this is a single-character string delimiter, then we're done. if delimiter.chars().count() < 2 { return String::new(); - }; + } // Otherwise, build a vector of substrings of the delimiter: for a // delimiter of `'''`, we want `["", "'"", "''"]`. @@ -487,7 +493,7 @@ fn build_lexer_regex( // Look for either the delimiter or a newline to terminate the // string. - (false, NewlineSupport::None) => Regex::new(&format!("{}|\n", escaped_delimiter)), + (false, NewlineSupport::None) => Regex::new(&format!("{escaped_delimiter}|\n")), } .unwrap(); regex_builder( @@ -560,7 +566,7 @@ fn build_lexer_regex( ), ); } - }; + } // This must be last, since it includes one group (so the index of all // future items will be off by 1). Build a regex for a heredoc start. @@ -594,6 +600,7 @@ fn build_lexer_regex( // Compile lexers // -------------- +#[must_use] pub fn compile_lexers(language_lexer_arr: Vec) -> LanguageLexersCompiled { let mut language_lexers_compiled = LanguageLexersCompiled { language_lexer_compiled_vec: Vec::new(), @@ -637,6 +644,7 @@ pub fn compile_lexers(language_lexer_arr: Vec) -> LanguageLexersC /// /// These linter warnings would IMHO make the code less readable. #[allow(clippy::bool_to_int_with_if)] +#[must_use] pub fn source_lexer( // The source code to lex. source_code: &str, @@ -725,12 +733,13 @@ pub fn source_lexer( } } CodeDocBlock::CodeBlock(ref mut _last_code_block) => { - if indent.is_empty() && delimiter.is_empty() { - // Code blocks should never need to be appended to a - // previous entry. - panic!("Attempted to append code block contents to a previous entry.") - //_last_code_block.push_str(contents); - } + // Code blocks should never need to be appended to a + // previous entry. + //_last_code_block.push_str(contents); + assert!( + !(indent.is_empty() && delimiter.is_empty()), + "Attempted to append code block contents to a previous entry." + ); } } } @@ -808,7 +817,7 @@ pub fn source_lexer( // match will be appended to the current code // block. |closing_regex: &Regex| { - trace!("Searching for the end of this token using the pattern '{:?}'.", closing_regex); + trace!("Searching for the end of this token using the pattern '{closing_regex:?}'."); // Add the opening delimiter to the code. source_code_unlexed_index += matching_group_str.len(); @@ -875,10 +884,9 @@ pub fn source_lexer( &source_code[full_comment_start_index..source_code_unlexed_index]; trace!( - "This is an inline comment. Source code before the line containing this comment is:\n'{}'\n\ - The text preceding this comment is: '{}'.\n\ - The comment is: '{}'\n", - code_lines_before_comment, comment_line_prefix, full_comment + "This is an inline comment. Source code before the line containing this comment is:\n'{code_lines_before_comment}'\n\ + The text preceding this comment is: '{comment_line_prefix}'.\n\ + The comment is: '{full_comment}'\n" ); // **Next**, determine if this comment is a doc block. @@ -933,13 +941,9 @@ pub fn source_lexer( trace!( "This is a doc block. Possibly added the preceding code block\n\ - '{}'.\n\ - Added a doc block with indent = '{}', delimiter = '{}', and contents =\n\ - '{}'.\n", - current_code_block, - comment_line_prefix, - matching_group_str, - contents + '{current_code_block}'.\n\ + Added a doc block with indent = '{comment_line_prefix}', delimiter = '{matching_group_str}', and contents =\n\ + '{contents}'.\n" ); // We've now stored the current code block (which @@ -964,8 +968,7 @@ pub fn source_lexer( source_code_unlexed_index + matching_group_str.len(); trace!( - "The opening delimiter is '{}', and the closing delimiter regex is '{}'.", - matching_group_str, comment_delim_regex + "The opening delimiter is '{matching_group_str}', and the closing delimiter regex is '{comment_delim_regex}'." ); // For nested comments, only treat the innermost comment @@ -1070,10 +1073,8 @@ pub fn source_lexer( comment_start_index = source_code_unlexed_index + opening_delimiter.len(); trace!( - "Found a nested opening block comment delimiter. Nesting depth: {}", - nesting_depth + "Found a nested opening block comment delimiter. Nesting depth: {nesting_depth}" ); - continue; } else { // This is a closing comment delimiter. assert!(nesting_depth > 0); @@ -1092,8 +1093,7 @@ pub fn source_lexer( + closing_delimiter_match.start() + closing_delimiter_match.len(); trace!( - "Found a non-innermost closing block comment delimiter. Nesting depth: {}", - nesting_depth + "Found a non-innermost closing block comment delimiter. Nesting depth: {nesting_depth}" ); continue; } @@ -1146,10 +1146,7 @@ pub fn source_lexer( [closing_delimiter_end_index ..newline_or_eof_after_closing_delimiter_index]; - trace!( - "The post-comment line is '{}'.", - post_closing_delimiter_line - ); + trace!("The post-comment line is '{post_closing_delimiter_line}'."); // Set the `current_code_block` to contain // preceding code (which might be multiple @@ -1178,12 +1175,9 @@ pub fn source_lexer( newline_or_eof_after_closing_delimiter_index; trace!( - "current_code_block is '{}'\n\ - comment_line_prefix is '{}'\n\ - code_lines_before_comment is '{}'", - current_code_block, - comment_line_prefix, - code_lines_before_comment + "current_code_block is '{current_code_block}'\n\ + comment_line_prefix is '{comment_line_prefix}'\n\ + code_lines_before_comment is '{code_lines_before_comment}'" ); // Next, determine if this is a doc block. @@ -1355,8 +1349,7 @@ pub fn source_lexer( // print the doc block trace!( - "Appending a doc block with indent '{}', delimiter '{}', and contents '{}'.", - comment_line_prefix, matching_group_str, contents + "Appending a doc block with indent '{comment_line_prefix}', delimiter '{matching_group_str}', and contents '{contents}'." ); // advance `current_code_block_index` to @@ -1380,7 +1373,7 @@ pub fn source_lexer( // #### String-like syntax RegexDelimType::String(closing_regex) => { trace!("This is a string. "); - append_code(closing_regex) + append_code(closing_regex); } RegexDelimType::TemplateLiteral => { diff --git a/server/src/lexer/pest_parser.rs b/server/src/lexer/pest_parser.rs index 5924706f..e0b21690 100644 --- a/server/src/lexer/pest_parser.rs +++ b/server/src/lexer/pest_parser.rs @@ -283,30 +283,30 @@ mod test { fn test_pest_c_1() { assert_eq!( c::parse_to_code_doc_blocks(indoc!( - r#" - //"# + r" + //" )), vec![CodeDocBlock::DocBlock(DocBlock { - indent: "".to_string(), + indent: String::new(), delimiter: "//".to_string(), - contents: "".to_string(), + contents: String::new(), lines: 0, })], ); assert_eq!( c::parse_to_code_doc_blocks(indoc!( - r#" + r" code; /* Testing 1, 2, 3 - */"# + */" )), vec![ CodeDocBlock::CodeBlock("code;\n".to_string()), CodeDocBlock::DocBlock(DocBlock { - indent: "".to_string(), + indent: String::new(), delimiter: "/*".to_string(), contents: "Testing\n1,\n\n2, 3\n".to_string(), lines: 4, @@ -315,18 +315,18 @@ mod test { ); assert_eq!( c::parse_to_code_doc_blocks(indoc!( - r#" + r" code; /* Testing * 1, * * 2, 3 - */"# + */" )), vec![ CodeDocBlock::CodeBlock("code;\n".to_string()), CodeDocBlock::DocBlock(DocBlock { - indent: "".to_string(), + indent: String::new(), delimiter: "/*".to_string(), contents: "Testing\n1,\n\n2, 3\n".to_string(), lines: 4, @@ -362,7 +362,7 @@ mod test { .to_string() ), CodeDocBlock::DocBlock(DocBlock { - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "A comment.\n".to_string(), lines: 1, @@ -398,7 +398,7 @@ mod test { .to_string() ), CodeDocBlock::DocBlock(DocBlock { - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "A comment.".to_string(), lines: 1, @@ -425,7 +425,7 @@ mod test { .to_string() ), CodeDocBlock::DocBlock(DocBlock { - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "A comment.".to_string(), lines: 1, @@ -450,7 +450,7 @@ mod test { .to_string() ), CodeDocBlock::DocBlock(DocBlock { - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "A comment.".to_string(), lines: 1, diff --git a/server/src/lexer/supported_languages.rs b/server/src/lexer/supported_languages.rs index 1097ee10..3e629f35 100644 --- a/server/src/lexer/supported_languages.rs +++ b/server/src/lexer/supported_languages.rs @@ -79,7 +79,7 @@ fn make_language_lexer( ext_arr: ext_arr.iter().map(|x| Arc::new(x.to_string())).collect(), inline_comment_delim_arr: inline_comment_delim_arr .iter() - .map(|x| x.to_string()) + .map(std::string::ToString::to_string) .collect(), block_comment_delim_arr: block_comment_delim_arr.to_vec(), string_delim_spec_arr: string_delim_spec_arr.to_vec(), @@ -101,6 +101,7 @@ fn make_string_delimiter_spec( } } +#[allow(clippy::unnecessary_wraps)] fn make_heredoc_delim( start_prefix: &str, delim_ident_regex: &str, diff --git a/server/src/lexer/tests.rs b/server/src/lexer/tests.rs index 266f3e7d..3e7d30c5 100644 --- a/server/src/lexer/tests.rs +++ b/server/src/lexer/tests.rs @@ -33,12 +33,8 @@ fn build_doc_block(indent: &str, delimiter: &str, contents: &str) -> CodeDocBloc indent: indent.to_string(), delimiter: delimiter.to_string(), contents: contents.to_string(), - lines: contents.matches("\n").count() - + (if contents.chars().last().unwrap_or('\n') == '\n' { - 0 - } else { - 1 - }), + lines: contents.matches('\n').count() + + usize::from(contents.chars().last().unwrap_or('\n') != '\n'), }) } @@ -335,9 +331,9 @@ fn test_js() { // Indented block comments. assert_eq!( source_lexer( - r#"test_1(); + r"test_1(); /* Test - 2 */"#, + 2 */", js ), [ @@ -348,9 +344,9 @@ fn test_js() { assert_eq!( source_lexer( - r#"test_1(); + r"test_1(); /* Test - 2 */"#, + 2 */", js ), [ @@ -361,10 +357,10 @@ fn test_js() { assert_eq!( source_lexer( - r#"test_1(); + r"test_1(); /* Test 2 - */"#, + */", js ), [ @@ -375,10 +371,10 @@ fn test_js() { assert_eq!( source_lexer( - r#"test_1(); + r"test_1(); /* Test 2 - */"#, + */", js ), [ @@ -389,12 +385,12 @@ fn test_js() { assert_eq!( source_lexer( - r#"test_1(); + r"test_1(); /* Test 2 3 - */"#, + */", js ), [ @@ -406,9 +402,9 @@ fn test_js() { // Mis-indented block comments. assert_eq!( source_lexer( - r#"test_1(); + r"test_1(); /* Test - 2 */"#, + 2 */", js ), [ @@ -419,9 +415,9 @@ fn test_js() { assert_eq!( source_lexer( - r#"test_1(); + r"test_1(); /* Test - 2 */"#, + 2 */", js ), [ @@ -459,8 +455,7 @@ fn test_js() { ); } -// TODO: re-enable this test when heredocs are supported. -#[ignore] +#[ignore = "Re-enable this test where heredocs are supported."] #[test] fn test_cpp() { let llc = compile_lexers(get_language_lexer_vec()); @@ -590,29 +585,29 @@ fn test_rust() { assert_eq!( source_lexer( - r#" /* Depth 1 + r" /* Depth 1 /* Depth 2 comment */ /* Depth 2 /* Depth 3 */ */ /* Depth 2 /* Depth 3 comment */ */ -More depth 1 */"#, +More depth 1 */", rust ), [ build_code_block(" /* Depth 1\n"), build_doc_block(" ", "/*", "Depth 2 comment\n"), build_code_block( - r#" /* Depth 2 + r" /* Depth 2 /* Depth 3 */ */ /* Depth 2 -"# +" ), build_doc_block(" ", "/*", "Depth 3 comment\n"), build_code_block( - r#" */ -More depth 1 */"# + r" */ +More depth 1 */" ), ] ); diff --git a/server/src/main.rs b/server/src/main.rs index 8882ef5f..8d2d60e3 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -158,11 +158,10 @@ impl Cli { } return Ok(()); - } else { - eprintln!( - "Unexpected response from server: {body}, status code = {status_code}" - ); } + eprintln!( + "Unexpected response from server: {body}, status code = {status_code}" + ); } Err(err) => { // Use this to skip the print from a nested if @@ -202,7 +201,7 @@ impl Cli { match self.test_mode { None => cmd = Command::new(¤t_exe), Some(TestMode::NotFound) => { - cmd = Command::new("nonexistent-command") + cmd = Command::new("nonexistent-command"); } Some(TestMode::Sleep) => { cmd = Command::new(¤t_exe); @@ -320,6 +319,8 @@ fn port_in_range(s: &str) -> Result { .parse() .map_err(|_| format!("`{s}` isn't a port number"))?; if PORT_RANGE.contains(&port) { + // Code above guarantees this is safe. + #[allow(clippy::cast_possible_truncation)] Ok(port as u16) } else { Err(format!( @@ -347,7 +348,7 @@ fn parse_credentials(s: &str) -> Result { /// This is used by `ping` to transform the "access connections from any /// address" address into localhost, a valid destination address for a ping. fn fix_addr(addr: &SocketAddr) -> SocketAddr { - if addr.ip() == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) { + if addr.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED) { let mut addr = *addr; addr.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); addr @@ -432,7 +433,7 @@ mod test { // ### `fix_addr` #[test] fn test_fix_addr_ipv4_unspecified() { - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8080); + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080); let fixed = fix_addr(&addr); assert_eq!(fixed.ip(), IpAddr::V4(Ipv4Addr::LOCALHOST)); assert_eq!(fixed.port(), 8080); @@ -449,7 +450,7 @@ mod test { #[test] fn test_fix_addr_specific_unchanged() { // A specific (non-unspecified) address is returned unchanged. - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080); assert_eq!(fix_addr(&addr), addr); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 5)), 1234); assert_eq!(fix_addr(&addr), addr); diff --git a/server/src/processing.rs b/server/src/processing.rs index 60fa7cf2..0c48fead 100644 --- a/server/src/processing.rs +++ b/server/src/processing.rs @@ -57,7 +57,6 @@ use html5ever::{ tree_builder::TreeBuilderOpts, }; use imara_diff::{Algorithm, Diff, Hunk, InternedInput, TokenSource}; -use lazy_static::lazy_static; use markup5ever_rcdom::{Node, NodeData, RcDom, SerializableHandle}; use minify_html; use phf::phf_map; @@ -247,12 +246,13 @@ pub enum TranslationResultsString { // // Globals // ------- -lazy_static! { - /// Match the lexer directive in a source file. - static ref LEXER_DIRECTIVE: Regex = Regex::new(r"CodeChat Editor lexer: (\w+)").unwrap(); - /// If this matches, it means an unterminated fenced code block. This should - /// be replaced with the `` terminator. - static ref DOC_BLOCK_SEPARATOR_BROKEN_FENCE: Regex = Regex::new(concat!( +/// Match the lexer directive in a source file. +static LEXER_DIRECTIVE: LazyLock = + LazyLock::new(|| Regex::new(r"CodeChat Editor lexer: (\w+)").unwrap()); +/// If this matches, it means an unterminated fenced code block. This should +/// be replaced with the `` terminator. +static DOC_BLOCK_SEPARATOR_BROKEN_FENCE: LazyLock = LazyLock::new(|| { + Regex::new(concat!( // Allow the `.` wildcard to match newlines. "(?s)", // The first `` will be munged when a fenced code @@ -261,8 +261,10 @@ lazy_static! { // Non-greedy wildcard -- match the first separator, so we don't munch // multiple `DOC_BLOCK_SEPARATOR_STRING`s in one replacement. ".*?", - "\n")).unwrap(); -} + "\n" + )) + .unwrap() +}); // Use this as a way to end unterminated fenced code blocks and specific types // of HTML blocks. (The remaining types of HTML blocks are terminated by a blank @@ -304,14 +306,14 @@ const DOC_BLOCK_SEPARATOR_SPLIT_STRING: &str = ""; // Correctly terminated fenced code blocks produce this, which can be removed // from the HTML produced by Markdown conversion. -const DOC_BLOCK_SEPARATOR_REMOVE_FENCE: &str = r#" +const DOC_BLOCK_SEPARATOR_REMOVE_FENCE: &str = r" -->?>]]> ``````````````````````` ~~~~~~~~~~~~~~~~~~~~~~~ -"#; +"; // The replacement string for the `DOC_BLOCK_SEPARATOR_BROKEN_FENCE` regex. const DOC_BLOCK_SEPARATOR_MENDED_FENCE: &str = "\n\n"; @@ -376,6 +378,7 @@ impl<'de> Deserialize<'de> for CodeMirrorDocBlock { // Determine if the provided file is part of a project // --------------------------------------------------- +#[must_use] pub fn find_path_to_toc(file_path: &Path) -> Option { // To determine if this source code is part of a project, look for a project // file by searching the current directory, then all its parents, for a file @@ -424,13 +427,10 @@ pub fn codechat_for_web_to_source( ) -> Result { let lexer_name = &codechat_for_web.metadata.mode; // Given the mode, find the lexer. - let lexer = match LEXERS.map_mode_to_lexer.get(lexer_name) { - Some(v) => v, - None => { - return Err(CodechatForWebToSourceError::InvalidLexer( - lexer_name.clone(), - )); - } + let Some(lexer) = LEXERS.map_mode_to_lexer.get(lexer_name) else { + return Err(CodechatForWebToSourceError::InvalidLexer( + lexer_name.clone(), + )); }; // Extract the plain (not diffed) CodeMirror contents. @@ -446,14 +446,14 @@ pub fn codechat_for_web_to_source( } // Translate the HTML document to Markdown. let converter = HtmlToMarkdownWrapped::new(); - let tree = html_to_tree(&code_mirror.doc, &None)?; + let tree = html_to_tree(&code_mirror.doc, None)?; dehydrating_walk_node(&tree); return converter .convert(&tree) .map_err(CodechatForWebToSourceError::HtmlToMarkdownFailed); } let code_doc_block_vec_html = code_mirror_to_code_doc_blocks(code_mirror); - let code_doc_block_vec = doc_block_html_to_markdown(code_doc_block_vec_html, &None) + let code_doc_block_vec = doc_block_html_to_markdown(code_doc_block_vec_html, None) .map_err(CodechatForWebToSourceError::HtmlToMarkdownFailed)?; code_doc_block_vec_to_source(&code_doc_block_vec, lexer) .map_err(CodechatForWebToSourceError::CannotTranslateCodeChat) @@ -461,6 +461,7 @@ pub fn codechat_for_web_to_source( /// Return the byte index of `s[utf_16_index]`, where the indexing operation is /// in UTF-16 code units. +#[must_use] pub fn byte_index_of(s: &str, utf_16_index: usize) -> usize { let mut byte_index = 0; let mut current_index = 0; @@ -498,13 +499,13 @@ fn code_mirror_to_code_doc_blocks(code_mirror: &CodeMirror) -> Vec // Append the code block, unless it's empty. let code_contents = &code_mirror.doc[byte_index_prev..byte_index]; if !code_contents.is_empty() { - code_doc_block_arr.push(CodeDocBlock::CodeBlock(code_contents.to_string())) + code_doc_block_arr.push(CodeDocBlock::CodeBlock(code_contents.to_string())); } // Append the doc block. code_doc_block_arr.push(CodeDocBlock::DocBlock(DocBlock { - indent: codemirror_doc_block.indent.to_string(), - delimiter: codemirror_doc_block.delimiter.to_string(), - contents: codemirror_doc_block.contents.to_string(), + indent: codemirror_doc_block.indent.clone(), + delimiter: codemirror_doc_block.delimiter.clone(), + contents: codemirror_doc_block.contents.clone(), lines: 0, })); let byte_index_prev = byte_index; @@ -610,7 +611,7 @@ pub fn doc_block_html_to_markdown( // // For this reason, when provided, this function must called with a vec // containing only one doc block. - dom_location: &Option<(Vec, usize)>, + dom_location: Option<&(Vec, usize)>, ) -> Result, HtmlToMarkdownWrappedError> { let mut converter = HtmlToMarkdownWrapped::new(); let mut last_doc_block_index = None; @@ -809,7 +810,7 @@ fn code_doc_block_vec_to_source( // This is code. Simply append it (by definition, indent and // delimiter are empty). { - file_contents += contents + file_contents += contents; } } } @@ -890,7 +891,7 @@ pub fn source_to_codechat_for_web( // Create an initially-empty struct; the source code will be // translated to this. let mut code_mirror = CodeMirror { - doc: "".to_string(), + doc: String::new(), doc_blocks: Vec::new(), }; @@ -945,7 +946,7 @@ pub fn source_to_codechat_for_web( match code_or_doc_block { CodeDocBlock::CodeBlock(code_string) => { source.push_str(&code_string); - len += len_utf16(&code_string) + len += len_utf16(&code_string); } CodeDocBlock::DocBlock(doc_block) => { // Create the doc block. @@ -954,8 +955,8 @@ pub fn source_to_codechat_for_web( // To. Note that the last doc block could be zero // length, so handle this case. to: len + max(doc_block.lines, 1), - indent: doc_block.indent.to_string(), - delimiter: doc_block.delimiter.to_string(), + indent: doc_block.indent.clone(), + delimiter: doc_block.delimiter.clone(), // Used the markdown-translated replacement for this // doc block, rather than the original string. contents: minify(doc_block_contents_iter.next().unwrap())?, @@ -1034,7 +1035,7 @@ pub fn minify(html: &str) -> Result { // Compute the length of the provided string in UTF16 characters. fn len_utf16(s: &str) -> usize { - s.chars().map(|c| c.len_utf16()).sum() + s.chars().map(char::len_utf16).sum() } // Like `source_to_codechat_for_web`, translate a source file to the CodeChat @@ -1123,7 +1124,7 @@ pub const UNICODE_CURSOR_MARKER: char = '\u{E83B}'; fn html_to_tree( html: &str, // See the same parameter from `doc_block_html_to_markdown`. - dom_location: &Option<(Vec, usize)>, + dom_location: Option<&(Vec, usize)>, ) -> io::Result> { let dom = parse_document( RcDom::default(), @@ -1176,7 +1177,7 @@ fn html_to_tree( // A framework to transform HTML by parsing it to a DOM tree, walking the tree, // then serializing the tree back to an HTML string. pub fn transform_html)>(html: &str, transform: T) -> io::Result { - let tree = html_to_tree(html, &None)?; + let tree = html_to_tree(html, None)?; transform(&tree); // Serialize the transformed DOM back to a string. @@ -1422,14 +1423,13 @@ pub fn remove_tinymce_data( // since this will re-borrow it. remove_tinymce_data(parent, index) }; - } else { - // If we didn't remove this element, then filter out unwanted - // attributes. - attrs.borrow_mut().retain(|attr| { - !(attr.name.local.starts_with("data-mce-") - || (attr.name.local == *"class" && attr.value.starts_with("mce-"))) - }); } + // If we didn't remove this element, then filter out unwanted + // attributes. + attrs.borrow_mut().retain(|attr| { + !(attr.name.local.starts_with("data-mce-") + || (attr.name.local == *"class" && attr.value.starts_with("mce-"))) + }); } Some(node.clone()) } @@ -1675,6 +1675,7 @@ static CUSTOM_ELEMENT_TO_CODE_BLOCK_LANGUAGE: phf::Map<&'static str, &'static st // // #### String diff /// Given two strings, return a list of changes between them. +#[must_use] pub fn diff_str(before: &str, after: &str) -> Vec { let mut change_spec: Vec = Vec::new(); // The previous value of `before.start` and the character index @@ -1719,11 +1720,11 @@ pub fn diff_str(before: &str, after: &str) -> Vec { None }, insert: if hunk_after.is_empty() { - "".to_string() + String::new() } else { hunk_after.into_iter().collect() }, - }) + }); } change_spec @@ -1752,8 +1753,8 @@ impl<'a> TokenSource for CodeMirrorDocBlocksStruct<'a> { } } -fn none_if_eq(before: T, after: T) -> Option { - if before == after { None } else { Some(after) } +fn none_if_eq(before: &T, after: T) -> Option { + if before == &after { None } else { Some(after) } } fn none_if_eq_ref(before: &T, after: &T) -> Option { @@ -1765,6 +1766,7 @@ fn none_if_eq_ref(before: &T, after: &T) -> Option { } /// Given two `CodeMirrorDocBlocks`, return a list of changes between them. +#[must_use] pub fn diff_code_mirror_doc_blocks( before: &CodeMirrorDocBlockVec, after: &CodeMirrorDocBlockVec, @@ -1801,11 +1803,11 @@ pub fn diff_code_mirror_doc_blocks( CodeMirrorDocBlockUpdate { from: prev_before_range_start_val.from, from_new: none_if_eq( - prev_before_range_start_val.from, + &prev_before_range_start_val.from, prev_after_range_start_val.from, ), to: none_if_eq( - prev_before_range_start_val.to, + &prev_before_range_start_val.to, prev_after_range_start_val.to, ), indent: none_if_eq_ref( @@ -1852,8 +1854,8 @@ pub fn diff_code_mirror_doc_blocks( change_specs.push(CodeMirrorDocBlockTransaction::Update( CodeMirrorDocBlockUpdate { from: before_val.from, - from_new: none_if_eq(before_val.from, after_val.from), - to: none_if_eq(before_val.to, after_val.to), + from_new: none_if_eq(&before_val.from, after_val.from), + to: none_if_eq(&before_val.to, after_val.to), indent: none_if_eq_ref(&before_val.indent, &after_val.indent), delimiter: none_if_eq_ref(&before_val.delimiter, &after_val.delimiter), contents: diff_str(&before_val.contents, &after_val.contents), diff --git a/server/src/processing/tests.rs b/server/src/processing/tests.rs index 7086cad3..8c24b233 100644 --- a/server/src/processing/tests.rs +++ b/server/src/processing/tests.rs @@ -118,7 +118,7 @@ fn test_codechat_for_web_to_source() { let codechat_for_web = build_codechat_for_web("python", "", vec![]); assert_eq!( cast!(codechat_for_web_to_source(&codechat_for_web), Ok), - "".to_string() + String::new() ); let codechat_for_web = build_codechat_for_web("undefined", "", vec![]); @@ -228,7 +228,7 @@ fn test_codemirror_to_code_doc_blocks_py() { } #[test] -#[should_panic] +#[should_panic(expected = "assertion `left == right` failed")] fn test_codemirror_to_code_doc_blocks_error() { run_test( "python", @@ -388,11 +388,11 @@ fn test_code_doc_blocks_to_source_css() { css_lexer ) .unwrap(), - r#"Test_0 + r"Test_0 /* Test 1 Test 2 */ -"# +" ); // Repeat the above tests with an indent. @@ -409,11 +409,11 @@ fn test_code_doc_blocks_to_source_css() { css_lexer ) .unwrap(), - r#"Test_0 + r"Test_0 /* Test 1 Test 2 */ -"# +" ); // Basic code. @@ -806,7 +806,7 @@ fn test_source_to_codechat_for_web_1() { 1, "", "//", - r#"

    1"# + r"

    1" ),] ))) ); @@ -847,7 +847,7 @@ fn apply_str_diff(before: &str, diffs: &[StringDiff]) -> String { before.replace_range(from_index..to_index, &diff.insert); } else { before.insert_str(from_index, &diff.insert); - }; + } } before } @@ -902,7 +902,7 @@ fn test_diff_1() { &[StringDiff { from: 0, to: Some(2), - insert: "".to_string(), + insert: String::new(), }], ); @@ -931,7 +931,7 @@ fn test_diff_1() { &[StringDiff { from: 2, to: Some(6), - insert: "".to_string(), + insert: String::new(), }], ); @@ -960,7 +960,7 @@ fn test_diff_1() { &[StringDiff { from: 6, to: Some(8), - insert: "".to_string(), + insert: String::new(), }], ); @@ -1085,7 +1085,7 @@ fn test_diff_2() { vec![CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { from: 10, to: 11, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "test1".to_string() })] @@ -1114,7 +1114,7 @@ fn test_diff_2() { CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { from: 11, to: 12, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "test".to_string() }), @@ -1137,7 +1137,7 @@ fn test_diff_2() { vec![CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { from: 11, to: 12, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "test2".to_string() })] @@ -1154,7 +1154,7 @@ fn test_diff_2() { vec![CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { from: 11, to: 12, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "test2".to_string() })] @@ -1272,7 +1272,7 @@ fn test_diff_2() { CodeMirrorDocBlockTransaction::Add(CodeMirrorDocBlock { from: 14, to: 15, - indent: "".to_string(), + indent: String::new(), delimiter: "#".to_string(), contents: "test4a".to_string() }), @@ -1297,7 +1297,7 @@ fn test_doc_block_html_to_markdown_1() { "", "

    Index 0

    Index 1.0Index 1.1012345

    " )], - &Some((vec![1, 2], 3)), + Some(&(vec![1, 2], 3)), ) .unwrap(), vec![build_doc_block( @@ -1397,7 +1397,7 @@ fn test_hydrate_html_1() { } fn dehydrate_html(html: &str) -> io::Result> { - let tree = html_to_tree(html, &None)?; + let tree = html_to_tree(html, None)?; dehydrating_walk_node(&tree); Ok(tree) } diff --git a/server/src/translation.rs b/server/src/translation.rs index 393a22ed..8b04e796 100644 --- a/server/src/translation.rs +++ b/server/src/translation.rs @@ -212,11 +212,11 @@ use std::{ fmt::Debug, path::{Path, PathBuf}, rc::Rc, + sync::LazyLock, }; use htmd::Node; // ### Third-party -use lazy_static::lazy_static; use log::{debug, error, warn}; use rand::random; use regex::Regex; @@ -253,10 +253,8 @@ use crate::{ // The max length of a message to show in the console. const MAX_MESSAGE_LENGTH: usize = 500; -lazy_static! { - /// A regex to determine the type of the first EOL. See 'PROCESSINGS\`. - pub static ref EOL_FINDER: Regex = Regex::new("[^\r\n]*(\r?\n)").unwrap(); -} +/// A regex to determine the type of the first EOL. See 'PROCESSINGS\`. +pub static EOL_FINDER: LazyLock = LazyLock::new(|| Regex::new("[^\r\n]*(\r?\n)").unwrap()); // Data structures // --------------- @@ -268,6 +266,7 @@ pub enum EolType { // Code // ---- +#[must_use] pub fn find_eol_type(s: &str) -> EolType { match EOL_FINDER.captures(s) { // Assume a line type for strings with no newlines. @@ -474,7 +473,7 @@ pub async fn translation_task( .processing_task_queue_tx .lock() .unwrap() - .insert(connection_id.to_string(), from_http_tx); + .insert(connection_id.clone(), from_http_tx); let mut continue_loop = true; let mut tt = TranslationTask { @@ -524,7 +523,7 @@ pub async fn translation_task( EditorMessageContents::Closed | EditorMessageContents::RequestClose => { debug!("Forwarding it to the Client."); - queue_send!(tt.to_client_tx.send(ide_message)) + queue_send!(tt.to_client_tx.send(ide_message)); }, EditorMessageContents::Result(_) => continue_loop = tt.ide_result(ide_message).await, @@ -571,7 +570,7 @@ pub async fn translation_task( // Handle HTTP requests. Some(http_request) = tt.from_http_rx.recv() => { - debug!("Received HTTP request for {:?} and sending LoadFile to IDE, id = {}.", http_request.file_path, tt.id); + debug!("Received HTTP request for {} and sending LoadFile to IDE, id = {}.", http_request.file_path.display(), tt.id); // Convert the request into a `LoadFile` message. queue_send!(tt.to_ide_tx.send(EditorMessage { id: tt.id, @@ -609,7 +608,7 @@ pub async fn translation_task( // Handle messages that are simply passed through. EditorMessageContents::Closed => { debug!("Forwarding it to the IDE."); - queue_send!(tt.to_ide_tx.send(client_message)) + queue_send!(tt.to_ide_tx.send(client_message)); }, EditorMessageContents::Result(ref result) => { @@ -619,7 +618,7 @@ pub async fn translation_task( if matches!(result, Err(ResultErrTypes::OutOfSync(..))) { tt.sent_full = false; } - queue_send!(tt.to_ide_tx.send(client_message)) + queue_send!(tt.to_ide_tx.send(client_message)); }, // Open a web browser when requested. @@ -655,7 +654,7 @@ pub async fn translation_task( EditorMessageContents::CurrentFile(url_string, _is_text) => { debug!("Forwarding translated path to IDE."); let result = match url_to_path(&url_string, tt.prefix) { - Err(err) => Err(ResultErrTypes::UrlToPathError(url_string.to_string(), err.to_string())), + Err(err) => Err(ResultErrTypes::UrlToPathError(url_string.clone(), err.to_string())), Ok(file_path) => { match file_path.to_str() { None => Err(ResultErrTypes::NoPathToString(file_path)), @@ -947,7 +946,7 @@ impl TranslationTask { ), // There's no file, so return empty contents, which will // be ignored. - "".to_string(), + String::new(), ), Ok(mut fc) => { let option_file_contents = try_read_as_text(&mut fc).await; @@ -962,7 +961,7 @@ impl TranslationTask { .await, // If the file is binary, return empty contents, // which will be ignored. - option_file_contents.unwrap_or("".to_string()), + option_file_contents.unwrap_or(String::new()), ) } } @@ -978,7 +977,7 @@ impl TranslationTask { self.source_code = file_contents; self.eol = find_eol_type(&self.source_code); // We must clone here, since the original is placed in the TX queue. - self.code_mirror_doc = plain.doc.clone(); + self.code_mirror_doc.clone_from(&plain.doc); self.code_mirror_doc_blocks = Some(plain.doc_blocks.clone()); debug!("Sending Update from LoadFile to Client, id = {}.", self.id); @@ -1123,7 +1122,7 @@ impl TranslationTask { metadata: SourceFileMetadata { // Since this is raw data, `mode` doesn't // matter. - mode: "".to_string(), + mode: String::new(), }, source: CodeMirrorDiffable::Plain(CodeMirror { doc: code_mirror.doc.clone(), @@ -1134,7 +1133,7 @@ impl TranslationTask { }), })); self.source_code = code_mirror.doc; - self.code_mirror_doc = self.source_code.clone(); + self.code_mirror_doc.clone_from(&self.source_code); self.code_mirror_doc_blocks = Some(vec![]); Ok(ResultOkTypes::Void) } @@ -1231,7 +1230,7 @@ impl TranslationTask { "client", ); } - self.code_mirror_doc = code_mirror.doc.clone(); + self.code_mirror_doc.clone_from(&code_mirror.doc); self.code_mirror_doc_blocks = Some(code_mirror.doc_blocks.clone()); // We may need to change this version if we send a // diff back to the Client. @@ -1289,8 +1288,13 @@ impl TranslationTask { ))) { // Use a whole number to avoid encoding - // differences with fractional values. - cfw_version = random::() as f64; + // differences with fractional values. Precision loss from the + // u64 -> f64 cast is fine, since we just need a unique-ish version number. + cfw_version = { + #[allow(clippy::cast_precision_loss)] + let v = random::() as f64; + v + }; // The Client needs an update. let client_contents = self.diff_code_mirror( cfw.metadata.clone(), @@ -1324,10 +1328,10 @@ impl TranslationTask { self.code_mirror_doc_blocks = Some(code_mirror_translated.doc_blocks); } - }; + } // Correct EOL endings for use with the IDE. let new_source_code_eol = eol_convert(new_source_code, &self.eol); - let ccfw = if self.sent_full && self.allow_source_diffs { + let new_cfw = if self.sent_full && self.allow_source_diffs { Some(CodeChatForWeb { metadata: cfw.metadata, source: CodeMirrorDiffable::Diff(CodeMirrorDiff { @@ -1354,7 +1358,7 @@ impl TranslationTask { }; self.version = cfw_version; self.source_code = new_source_code_eol; - ccfw + new_cfw } Err(message) => { let err = ResultErrTypes::CannotTranslateCodeChat(message.to_string()); @@ -1389,7 +1393,7 @@ impl TranslationTask { let is_markdown = self .code_mirror_doc_blocks .as_ref() - .is_none_or(|v| v.is_empty()); + .is_none_or(std::vec::Vec::is_empty); // 1. Find the HTML (for a Markdown document) or the doc // block the cursor is in. Create a temporary @@ -1440,7 +1444,7 @@ impl TranslationTask { // the HTML to Markdown. let translated = doc_block_html_to_markdown( vec![CodeDocBlock::DocBlock(doc_block)], - &Some((dom_path, dom_offset)), + Some(&(dom_path, dom_offset)), ) .ok()?; let CodeDocBlock::DocBlock(db) = &translated[0] else { @@ -1491,7 +1495,7 @@ fn eol_convert(s: String, eol_type: &EolType) -> String { if eol_type == &EolType::Crlf { // There shouldn't be any Windows-style CRLFs -- but if there are, // handle this nicely. - s.replace("\r\n", "\n").replace("\n", "\r\n") + s.replace("\r\n", "\n").replace('\n', "\r\n") } else { s } @@ -1568,7 +1572,7 @@ fn doc_blocks_compare(a: &CodeMirrorDocBlockVec, b: &CodeMirrorDocBlockVec) -> b // `MAX_MESSAGE_LENGTH` characters of the provided value. fn debug_shorten(val: T) -> String { if cfg!(debug_assertions) { - let msg = format!("{:?}", val); + let msg = format!("{val:?}"); let max_index = msg .char_indices() .nth(MAX_MESSAGE_LENGTH) @@ -1576,7 +1580,7 @@ fn debug_shorten(val: T) -> String { .0; msg[..max_index].to_string() } else { - "".to_string() + String::new() } } @@ -1692,14 +1696,14 @@ mod tests { let ide = vec![CodeMirrorDocBlock { from: 0, to: 20, - indent: "".to_string(), + indent: String::new(), delimiter: "//".to_string(), contents: "
    • Task list

    Line
    break

    Non-breaking\u{a0} space.

    ".to_string(), }]; let client = vec![CodeMirrorDocBlock { from: 0, to: 20, - indent: "".to_string(), + indent: String::new(), delimiter: "//".to_string(), contents: "
    • Task list

    Line
    break

    Non-breaking  space.

    ".to_string(), }]; @@ -1711,14 +1715,14 @@ mod tests { let ide = vec![CodeMirrorDocBlock { from: 0, to: 20, - indent: "".to_string(), + indent: String::new(), delimiter: "//".to_string(), contents: "
      1. 1
    ".to_string(), }]; let client = vec![CodeMirrorDocBlock { from: 0, to: 20, - indent: "".to_string(), + indent: String::new(), delimiter: "//".to_string(), contents: "
      1. 1
    ".to_string(), }]; diff --git a/server/src/webserver.rs b/server/src/webserver.rs index d285e84c..8592dc46 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -27,12 +27,14 @@ pub mod tests; use std::{ borrow::Cow, collections::{HashMap, HashSet}, - env, fs, io, + env, fs, + hash::BuildHasher, + io, net::SocketAddr, path::{self, MAIN_SEPARATOR_STR, Path, PathBuf}, str::FromStr, string::FromUtf8Error, - sync::{Arc, Mutex}, + sync::{Arc, LazyLock, Mutex}, time::Duration, }; @@ -56,9 +58,11 @@ use dunce::simplified; use futures_util::StreamExt; use htmlize::{escape_attribute, escape_text}; use indoc::formatdoc; -use lazy_static::lazy_static; use log::{LevelFilter, error, info, warn}; -use log4rs::{self, config::load_config_file}; +use log4rs::{ + self, + config::{Deserializers, load_config_file}, +}; use mime::Mime; use mime_guess; use path_slash::{PathBufExt, PathExt}; @@ -464,7 +468,7 @@ macro_rules! queue_send_func { pub const REPLY_TIMEOUT_MS: Duration = if cfg!(test) { Duration::from_millis(2500) } else { - Duration::from_millis(15000) + Duration::from_secs(15) }; /// The time to wait for a pong from the websocket in response to a ping sent by @@ -486,36 +490,44 @@ pub const INITIAL_IDE_MESSAGE_ID: f64 = INITIAL_CLIENT_MESSAGE_ID + 1.0; /// assuming an average of 1 message/second.) pub const MESSAGE_ID_INCREMENT: f64 = 3.0; -lazy_static! { - pub static ref ROOT_PATH: Arc> = Arc::new(Mutex::new(PathBuf::new())); - - // Define the location of static files. - static ref CLIENT_STATIC_PATH: PathBuf = { - let mut client_static_path = ROOT_PATH.lock().unwrap().clone(); - #[cfg(debug_assertions)] - client_static_path.push("client"); +pub static ROOT_PATH: LazyLock>> = + LazyLock::new(|| Arc::new(Mutex::new(PathBuf::new()))); - client_static_path.push("static"); - client_static_path - }; +// Define the location of static files. +static CLIENT_STATIC_PATH: LazyLock = LazyLock::new(|| { + let mut client_static_path = ROOT_PATH.lock().unwrap().clone(); + #[cfg(debug_assertions)] + client_static_path.push("client"); - // Read in the hashed names of files bundled by esbuild. - static ref BUNDLED_FILES_MAP: HashMap = { - let mut hl = ROOT_PATH.lock().unwrap().clone(); - #[cfg(debug_assertions)] - hl.push("server"); - hl.push("hashLocations.json"); - let json = fs::read_to_string(hl.clone()).unwrap_or_else( - |err| panic!("Error: Unable to read {:#?}: {err}", hl.to_string_lossy()) - ); - let hmm: HashMap = - serde_json::from_str(&json).unwrap_or_else(|_| panic!("Unable to parse JSON in {:#?}", hl.to_string_lossy())); - hmm - }; + client_static_path.push("static"); + client_static_path +}); - static ref CODECHAT_EDITOR_FRAMEWORK_JS: String = BUNDLED_FILES_MAP.get("CodeChatEditorFramework.js").cloned().expect("Unable to find framework JS in bundled files map."); - static ref CODECHAT_EDITOR_PROJECT_CSS: String = BUNDLED_FILES_MAP.get("CodeChatEditorProject.css").cloned().expect("Unable to find project CSS in bundled files map."); -} +// Read in the hashed names of files bundled by esbuild. +static BUNDLED_FILES_MAP: LazyLock> = LazyLock::new(|| { + let mut hl = ROOT_PATH.lock().unwrap().clone(); + #[cfg(debug_assertions)] + hl.push("server"); + hl.push("hashLocations.json"); + let json = fs::read_to_string(hl.clone()) + .unwrap_or_else(|err| panic!("Error: Unable to read {:#?}: {err}", hl.to_string_lossy())); + let hmm: HashMap = serde_json::from_str(&json) + .unwrap_or_else(|_| panic!("Unable to parse JSON in {:#?}", hl.to_string_lossy())); + hmm +}); + +static CODECHAT_EDITOR_FRAMEWORK_JS: LazyLock = LazyLock::new(|| { + BUNDLED_FILES_MAP + .get("CodeChatEditorFramework.js") + .cloned() + .expect("Unable to find framework JS in bundled files map.") +}); +static CODECHAT_EDITOR_PROJECT_CSS: LazyLock = LazyLock::new(|| { + BUNDLED_FILES_MAP + .get("CodeChatEditorProject.css") + .cloned() + .expect("Unable to find project CSS in bundled files map.") +}); // Define the location of the root path, which contains `static/`, `log4rs.yml`, // and `hashLocations.json` in a production build, or `client/` and `server/` in @@ -624,7 +636,7 @@ pub fn log_capture_event(app_state: &WebAppState, wire: CaptureEventWire) -> Cap data, ); - capture.log(event); + capture.log(&event); capture.status() } else { CaptureStatus::disabled() @@ -635,12 +647,12 @@ pub fn capture_status(app_state: &WebAppState) -> CaptureStatus { app_state .capture .as_ref() - .map(EventCapture::status) - .unwrap_or_else(CaptureStatus::disabled) + .map_or_else(CaptureStatus::disabled, EventCapture::status) } // Get the `mode` query parameter to determine `is_test_mode`; default to // `false`. +#[must_use] pub fn get_test_mode(req: &HttpRequest) -> bool { let query_params = web::Query::>::from_query(req.query_string()); if let Ok(query) = query_params { @@ -726,7 +738,7 @@ pub async fn filesystem_endpoint( // with a leading slash, which gets absorbed into the URL to prevent a URL // such as "/fw/fsc/1//foo/bar/...". Restore it here. #[cfg(target_os = "windows")] - let fixed_file_path = request_file_path.replace("\\", "%5C"); + let fixed_file_path = request_file_path.replace('\\', "%5C"); // On OS X/Linux, the path starts with a leading slash, which gets absorbed // into the URL to prevent a URL such as "/fw/fsc/1//foo/bar/...". Restore // it here. @@ -773,10 +785,7 @@ pub async fn filesystem_endpoint( // Get the processing queue; only keep the lock during this block. let processing_queue_tx = app_state.processing_task_queue_tx.lock().unwrap(); let Some(processing_tx) = processing_queue_tx.get(&connection_id) else { - let msg = format!( - "Error: no processing task queue for connection id {}.", - connection_id - ); + let msg = format!("Error: no processing task queue for connection id {connection_id}."); error!("{msg}"); return http_not_found(&msg); }; @@ -819,7 +828,10 @@ pub async fn filesystem_endpoint( } v.into_response(req) } - Err(err) => http_not_found(&format!("Error opening file {path:?}: {err}.",)), + Err(err) => http_not_found(&format!( + "Error opening file \"{}\": {err}.", + path.display() + )), } } }, @@ -872,9 +884,7 @@ pub async fn file_to_response( let file_path = &http_request.file_path; let Some(file_name) = file_path.file_name() else { return ( - SimpleHttpResponse::Err(SimpleHttpResponseError::ProjectPathShort( - file_path.to_path_buf(), - )), + SimpleHttpResponse::Err(SimpleHttpResponseError::ProjectPathShort(file_path.clone())), None, ); }; @@ -954,7 +964,7 @@ pub async fn file_to_response( ), ) } else { - ("".to_string(), "".to_string()) + (String::new(), String::new()) }; // Do we need to respond with a [simple viewer](#Client-simple-viewer)? @@ -996,13 +1006,13 @@ pub async fn file_to_response( let codechat_for_web = match translation_results_string { // The file type is binary. Ask the HTTP server to serve it raw. TranslationResultsString::Binary => return - (SimpleHttpResponse::Bin(file_path.to_path_buf()), None) + (SimpleHttpResponse::Bin(file_path.clone()), None) , // The file type is unknown. Serve it raw. TranslationResultsString::Unknown => { return ( SimpleHttpResponse::Raw( - file_contents.unwrap().to_string(), + file_contents.unwrap().clone(), mime_guess::from_path(file_path).first_or_text_plain(), ), None, @@ -1040,18 +1050,14 @@ pub async fn file_to_response( // Provided info from the HTTP request, determine the following parameters. let Some(raw_dir) = file_path.parent() else { return ( - SimpleHttpResponse::Err(SimpleHttpResponseError::ProjectPathShort( - file_path.to_path_buf(), - )), + SimpleHttpResponse::Err(SimpleHttpResponseError::ProjectPathShort(file_path.clone())), None, ); }; let dir = path_display(raw_dir); let Some(file_path) = file_path.to_str() else { return ( - SimpleHttpResponse::Err(SimpleHttpResponseError::PathNotString( - file_path.to_path_buf(), - )), + SimpleHttpResponse::Err(SimpleHttpResponseError::PathNotString(file_path.clone())), None, ); }; @@ -1112,24 +1118,20 @@ fn make_simple_viewer(http_request: &ProcessingTaskHttpRequest, html: &str) -> S let file_path = &http_request.file_path; let Some(file_name) = file_path.file_name() else { return SimpleHttpResponse::Err(SimpleHttpResponseError::ProjectPathShort( - file_path.to_path_buf(), + file_path.clone(), )); }; let Some(file_name) = file_name.to_str() else { - return SimpleHttpResponse::Err(SimpleHttpResponseError::PathNotString( - file_path.to_path_buf(), - )); + return SimpleHttpResponse::Err(SimpleHttpResponseError::PathNotString(file_path.clone())); }; let file_name = escape_text(file_name); let Some(path_to_toc) = find_path_to_toc(file_path) else { - return SimpleHttpResponse::Err(SimpleHttpResponseError::PathNotProject( - file_path.to_path_buf(), - )); + return SimpleHttpResponse::Err(SimpleHttpResponseError::PathNotProject(file_path.clone())); }; let Some(path_to_toc) = path_to_toc.to_str() else { return SimpleHttpResponse::Err(SimpleHttpResponseError::PathNotString( - path_to_toc.to_path_buf(), + path_to_toc.clone(), )); }; let path_to_toc = escape_text(path_to_toc); @@ -1188,13 +1190,13 @@ fn make_simple_viewer(http_request: &ProcessingTaskHttpRequest, html: &str) -> S /// allowing the user to edit the plain text of the source code in the IDE, or /// make GUI-enhanced edits of the source code rendered by the CodeChat Editor /// Client. -pub fn client_websocket( +pub fn client_websocket( connection_id: String, - req: HttpRequest, + req: &HttpRequest, body: web::Payload, - websocket_queues: Arc>>, + websocket_queues: Arc>>, ) -> Result { - let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?; + let (response, mut session, mut msg_stream) = actix_ws::handle(req, body)?; // Websocket task: start a task to handle receiving `JointMessage` websocket // data from the CodeChat Editor Client and forwarding it to the IDE and @@ -1207,16 +1209,15 @@ pub fn client_websocket( // Transfer the queues from the global state to this task. let (from_websocket_tx, mut to_websocket_rx, mut pending_messages) = - match websocket_queues.lock().unwrap().remove(&connection_id) { - Some(queues) => ( + if let Some(queues) = websocket_queues.lock().unwrap().remove(&connection_id) { + ( queues.from_websocket_tx.clone(), queues.to_websocket_rx, queues.pending_messages, - ), - None => { - error!("No websocket queues for connection id {connection_id}."); - return; - } + ) + } else { + error!("No websocket queues for connection id {connection_id}."); + return; }; // Shutdown may occur in a controlled process or an immediate websocket @@ -1253,7 +1254,7 @@ pub fn client_websocket( loop { select! { // Send pings on a regular basis. - _ = sleep(WEBSOCKET_PING_DELAY) => { + () = sleep(WEBSOCKET_PING_DELAY) => { if sent_ping { // If we haven't received the answering pong, the // websocket must be broken. @@ -1274,6 +1275,7 @@ pub fn client_websocket( Some(msg_wrapped) = aggregated_msg_stream.next() => { match msg_wrapped { Ok(msg) => { + #[allow(clippy::match_wildcard_for_single_variants)] match msg { // Send a pong in response to a ping. AggregatedMessage::Ping(bytes) => { @@ -1347,6 +1349,7 @@ pub fn client_websocket( break; } + // Lint allow on `match` above allows this: it's a catch-all for anything not know here. other => { warn!("Unexpected message {other:?}"); break; @@ -1432,7 +1435,7 @@ pub fn client_websocket( // Don't stop timers; the re-connection may handle them. info!("Websocket re-enqueued."); websocket_queues.lock().unwrap().insert( - connection_id.to_string(), + connection_id.clone(), WebsocketQueues { from_websocket_tx, to_websocket_rx, @@ -1487,13 +1490,13 @@ pub fn setup_server( credentials: Option, ) -> std::io::Result<(Server, Data)> { let capture_spool_path = ROOT_PATH.lock().unwrap().join("capture-spool"); - setup_server_with_capture_spool(addr, credentials, capture_spool_path) + setup_server_with_capture_spool(addr, credentials, &capture_spool_path) } pub fn setup_server_with_capture_spool( addr: &SocketAddr, credentials: Option, - capture_spool_path: PathBuf, + capture_spool_path: &Path, ) -> std::io::Result<(Server, Data)> { // Pre-load the bundled files before starting the webserver. let _ = &*BUNDLED_FILES_MAP; @@ -1558,7 +1561,7 @@ pub fn configure_logger(level: LevelFilter) -> Result<(), Box Result<(), Box) -> WebAppState { let capture_spool_path = ROOT_PATH.lock().unwrap().join("capture-spool"); - make_app_data_with_capture_spool(credentials, capture_spool_path) + make_app_data_with_capture_spool(credentials, &capture_spool_path) } fn make_app_data_with_capture_spool( credentials: Option, - capture_spool_path: PathBuf, + capture_spool_path: &Path, ) -> WebAppState { // Initialize capture with a durable local upload spool. The VS Code // extension supplies the CaptureWebService endpoint and bearer token at // runtime; this server never reads database credentials from disk or env. - let capture: Option = match EventCapture::new(capture_spool_path.clone()) { + let capture: Option = match EventCapture::new(capture_spool_path.to_path_buf()) { Ok(ec) => { - info!("Capture: enabled with local upload spool at {capture_spool_path:?}"); + info!( + "Capture: enabled with local upload spool at \"{}\"", + capture_spool_path.display() + ); Some(ec) } Err(err) => { @@ -1708,7 +1715,7 @@ pub fn url_to_path( .map(|path_segment| { urlencoding::decode(path_segment) .map_err(UrlToPathError::UnableToDecode) - .map(|path_seg| path_seg.replace("\\", "%5C")) + .map(|path_seg| path_seg.replace('\\', "%5C")) }) .collect::, UrlToPathError>>()?; @@ -1770,13 +1777,14 @@ pub fn try_canonicalize(file_path: &str) -> Result, file_path: &Path) -> String { // First, convert the path to use forward slashes. let pathname = simplified(file_path) .to_slash_lossy() // The convert each part of the path to a URL-encoded string. (This // avoids encoding the slashes.) - .split("/") + .split('/') .map(|s| urlencoding::encode(s)) // Then put it all back together. .collect::>() @@ -1794,17 +1802,20 @@ pub fn path_to_url(prefix: &str, connection_id: Option<&str>, file_path: &Path) // Given a string (which is probably a pathname), drop the leading slash if it's // present. +#[must_use] pub fn drop_leading_slash(path_: &str) -> &str { path_.strip_prefix('/').unwrap_or(path_) } // Given a `Path`, transform it into a displayable HTML string (with any // necessary escaping). +#[must_use] pub fn path_display(p: &Path) -> Cow<'_, str> { escape_text(simplified(p).to_string_lossy()) } // Return a Not Found (404) error with the provided text (not HTML) body. +#[must_use] pub fn http_not_found(msg: &str) -> HttpResponse { HttpResponse::NotFound() .content_type(ContentType::html()) @@ -1812,6 +1823,7 @@ pub fn http_not_found(msg: &str) -> HttpResponse { } // Wrap the provided HTML body in DOCTYPE/html/head tags. +#[must_use] pub fn html_wrapper(body: &str) -> String { formatdoc!( r#" @@ -1862,12 +1874,12 @@ pub async fn get_server_url(port: u16) -> Result { ]) .status() .await?; - if !status.success() { - Err(GetServerUrlError::NonZeroExitStatus(status.code())) - } else { + if status.success() { Ok(format!( "https://{codespace_name}-{port}.{codespace_domain}" )) + } else { + Err(GetServerUrlError::NonZeroExitStatus(status.code())) } } else { // We're running locally, so use localhost. diff --git a/server/src/webserver/tests.rs b/server/src/webserver/tests.rs index c47a49af..de8e807d 100644 --- a/server/src/webserver/tests.rs +++ b/server/src/webserver/tests.rs @@ -153,7 +153,7 @@ fn test_other_path() { .success(); // Wait for the server to exit, since it locks the temp\_dir. - sleep(Duration::from_millis(3000)); + sleep(Duration::from_secs(3)); // Report any errors produced when removing the temporary directory. temp_dir.close().unwrap(); diff --git a/server/tests/overall_1.rs b/server/tests/overall_1.rs index 4eae324e..c12381e2 100644 --- a/server/tests/overall_1.rs +++ b/server/tests/overall_1.rs @@ -542,7 +542,7 @@ async fn test_server_core( assert_eq!(is_current, false); (toc_path.clone(), false) } else { - panic!("Unexpected path {msg_contents:?}."); + panic!("Unexpected path \"{}\".", msg_contents.display()); }; codechat_server .send_result_loadfile(server_id, None) @@ -734,7 +734,7 @@ async fn test_client_core( } async fn wait_for_mocha_success(driver: &WebDriver) -> Result<(), WebDriverError> { - const MOCHA_TEST_TIMEOUT: Duration = Duration::from_millis(30000); + const MOCHA_TEST_TIMEOUT: Duration = Duration::from_secs(30); let mocha_results = driver .query(By::Css("#mocha-stats .result")) diff --git a/server/tests/overall_4.rs b/server/tests/overall_4.rs index a391bb95..6ede865f 100644 --- a/server/tests/overall_4.rs +++ b/server/tests/overall_4.rs @@ -53,6 +53,31 @@ use code_chat_editor::{ }; use test_utils::prep_test_dir; +// Wait for the autosave timer to report the current cursor position, and check +// it against the expected code line. +async fn assert_cursor_line( + codechat_server: &CodeChatEditorServerLog, + client_id: &mut f64, + path_str: &str, + line: u32, +) { + assert_eq!( + codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), + EditorMessage { + id: *client_id, + message: EditorMessageContents::Update(UpdateMessageContents { + file_path: path_str.to_string(), + cursor_position: Some(CursorPosition::Line(line)), + scroll_position: Some(1.0), + is_re_translation: false, + contents: None, + }) + } + ); + codechat_server.send_result(*client_id, None).await.unwrap(); + *client_id += MESSAGE_ID_INCREMENT; +} + // Tests // ----- make_test!(test_xss, test_xss_core); @@ -386,31 +411,6 @@ async fn test_arrow_key_navigation_core( let mut client_id = INITIAL_CLIENT_MESSAGE_ID; - // Wait for the autosave timer to report the current cursor position, and - // check it against the expected code line. - async fn assert_cursor_line( - codechat_server: &CodeChatEditorServerLog, - client_id: &mut f64, - path_str: &str, - line: u32, - ) { - assert_eq!( - codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), - EditorMessage { - id: *client_id, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: path_str.to_string(), - cursor_position: Some(CursorPosition::Line(line)), - scroll_position: Some(1.0), - is_re_translation: false, - contents: None, - }) - } - ); - codechat_server.send_result(*client_id, None).await.unwrap(); - *client_id += MESSAGE_ID_INCREMENT; - } - // ### `ArrowDown` from a code line enters the doc block below it. // // Click near the start of line "b" (the last line of the top code block), @@ -599,31 +599,6 @@ async fn test_arrow_key_navigation_multiline_doc_block_core( let mut client_id = INITIAL_CLIENT_MESSAGE_ID; - // Wait for the autosave timer to report the current cursor position, and - // check it against the expected code line. - async fn assert_cursor_line( - codechat_server: &CodeChatEditorServerLog, - client_id: &mut f64, - path_str: &str, - line: u32, - ) { - assert_eq!( - codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), - EditorMessage { - id: *client_id, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: path_str.to_string(), - cursor_position: Some(CursorPosition::Line(line)), - scroll_position: Some(1.0), - is_re_translation: false, - contents: None, - }) - } - ); - codechat_server.send_result(*client_id, None).await.unwrap(); - *client_id += MESSAGE_ID_INCREMENT; - } - // Click directly on code line "c" -- the line immediately following the // multi-line doc block -- to give CodeMirror real focus there, then press // `Home` so the cursor sits at the exact line start `docBlockNavKeymap`'s @@ -666,7 +641,7 @@ async fn test_arrow_key_navigation_multiline_doc_block_core( EditorMessage { id: client_id, message: EditorMessageContents::Update(UpdateMessageContents { - file_path: path_str.to_string(), + file_path: path_str.clone(), cursor_position: Some(CursorPosition::Line(8)), scroll_position: Some(1.0), is_re_translation: false, diff --git a/server/tests/overall_5.rs b/server/tests/overall_5.rs index af57dcbe..0c923279 100644 --- a/server/tests/overall_5.rs +++ b/server/tests/overall_5.rs @@ -30,7 +30,7 @@ mod overall_common; // ------- // // ### Standard library -use std::path::PathBuf; +use std::{fmt::Write, path::PathBuf}; // ### Third-party use dunce::canonicalize; @@ -83,11 +83,11 @@ async fn test_edit_preserves_cursor_scroll_in_large_doc_block_core( // the end of these paragraphs, which will cause a server re-translation // when it cleans this up. for i in 0..100 { - orig_text += &format!("/// P{i}\n///\n"); + let _ = write!(orig_text, "/// P{i}\n///\n"); } // 100 instances of a one-line code block, then a one-line doc block. for i in 0..100 { - orig_text += &format!("{i}\n// {i}\n"); + let _ = write!(orig_text, "{i}\n// {i}\n"); } let server_id = perform_loadfile( @@ -190,7 +190,7 @@ async fn test_edit_preserves_cursor_scroll_in_large_doc_block_core( StringDiff { from: 1210, to: Some(1214), - insert: "".to_string() + insert: String::new() } ], doc_blocks: vec![], diff --git a/server/tests/overall_common/mod.rs b/server/tests/overall_common/mod.rs index 2139e86e..4fbe7e31 100644 --- a/server/tests/overall_common/mod.rs +++ b/server/tests/overall_common/mod.rs @@ -190,7 +190,7 @@ impl ExpectedMessages { ); } - pub fn check(&mut self, editor_message: EditorMessage) { + pub fn check(&mut self, editor_message: &EditorMessage) { if let Some((ref mut editor_message_contents, is_dynamic)) = self.0.remove(&(editor_message.id as i64)) { @@ -198,7 +198,7 @@ impl ExpectedMessages { && let EditorMessageContents::Update(emc) = editor_message_contents && let Some(contents) = &mut emc.contents { - let version = get_version(&editor_message); + let version = get_version(editor_message); contents.version = version; } // Special case: @@ -216,7 +216,7 @@ impl ExpectedMessages { codechat_server: &CodeChatEditorServerLog, timeout: Duration, ) { - self.check(codechat_server.get_message_timeout(timeout).await.unwrap()); + self.check(&codechat_server.get_message_timeout(timeout).await.unwrap()); } pub async fn assert_all_messages( @@ -226,7 +226,7 @@ impl ExpectedMessages { ) { while !self.0.is_empty() { if let Some(editor_message) = codechat_server.get_message_timeout(timeout).await { - self.check(editor_message); + self.check(&editor_message); } else { panic!( "No matching messages found. Unmatched messages:\n{:#?}", @@ -240,7 +240,7 @@ impl ExpectedMessages { // Time to wait for browser/WebDriver-backed client-server messages. This // matches the client-side response window and gives CI enough room for autosave // and loadfile acknowledgements under matrix load. -pub const TIMEOUT: Duration = Duration::from_millis(15000); +pub const TIMEOUT: Duration = Duration::from_secs(15); // Browser-backed tests share a single WebDriver endpoint. Safari on macOS CI is // unreliable with overlapping sessions, so serialize the harness. @@ -336,7 +336,7 @@ pub async fn harness< let client_html = cast!(&em_html.message, EditorMessageContents::ClientHtml); let find_str = " + "# ); diff --git a/server/src/lexer.rs b/server/src/lexer.rs index e8de7662..33c53f92 100644 --- a/server/src/lexer.rs +++ b/server/src/lexer.rs @@ -1014,11 +1014,18 @@ pub fn source_lexer( while nesting_depth != 0 { // Get the index of the next block comment // delimiter. - trace!( - "Looking for a block comment delimiter in '{}'.", - &source_code[comment_start_index - ..min(comment_start_index + 30, source_code.len())] - ); + trace!("Looking for a block comment delimiter in '{}'.", { + // Clamp to a valid char boundary, since + // `comment_start_index + 30` is an + // arbitrary byte offset that may land in + // the middle of a multi-byte UTF-8 + // character. + let mut end = min(comment_start_index + 30, source_code.len()); + while !source_code.is_char_boundary(end) { + end -= 1; + } + &source_code[comment_start_index..end] + }); let delimiter_captures_wrapped = comment_delim_regex.captures(&source_code[comment_start_index..]); if delimiter_captures_wrapped.is_none() { @@ -1302,8 +1309,24 @@ pub fn source_lexer( // and delimiter have already been split // out for that line. for line in &split_contents[1..] { + // `indent_column` is a byte offset + // computed from the (ASCII) + // indent/delimiter widths; a line's + // contents may contain multi-byte + // UTF-8 characters, so `indent_column` + // might not land on a char boundary + // within `line`. Slicing at a + // non-boundary index would panic; + // guard against that. If it's not a + // valid boundary, this line can't + // consist solely of (single-byte) + // whitespace up to that column, so + // it's not indented. let this_line_indent = if line.len() < indent_column { line + } else if !line.is_char_boundary(indent_column) { + is_indented = false; + break; } else { &line[..indent_column] }; diff --git a/server/src/lexer/supported_languages.rs b/server/src/lexer/supported_languages.rs index 3e629f35..01e70780 100644 --- a/server/src/lexer/supported_languages.rs +++ b/server/src/lexer/supported_languages.rs @@ -172,7 +172,7 @@ pub fn get_language_lexer_vec() -> Vec { // raw string syntax in C++11 and newer is IMHO so rare we won't // encounter it in older code. See the C++ // [string literals docs for the reasoning behind the start body regex.](https://en.cppreference.com/w/cpp/language/string_literal) - make_heredoc_delim("R\"", "[^()\\\\[[:space:]]]*", "(", ")", "\""), + make_heredoc_delim("R\"", "[^()\\\\\\s]*", "(", ")", "\""), SpecialCase::None, Some(pest_parser::c::parse_to_code_doc_blocks), ), diff --git a/server/src/lexer/tests.rs b/server/src/lexer/tests.rs index 3e7d30c5..040c7d95 100644 --- a/server/src/lexer/tests.rs +++ b/server/src/lexer/tests.rs @@ -746,3 +746,26 @@ fn test_compiler() { "verilog" ); } + +#[test] +fn test_utf8_indent_no_panic() { + // Regression test: a multi-byte UTF-8 character in a block comment's + // continuation line must not cause a byte-index char-boundary panic when + // computing/removing the comment's indentation. + let llc = compile_lexers(get_language_lexer_vec()); + // Use SQL, since (unlike C/C++) it's lexed by the regex-based lexer + // rather than the PEG-based parser. + let sql = llc.map_mode_to_lexer.get(&stringit("sql")).unwrap(); + let s = "/* Test\n \u{65E5}\u{672C}\u{8A9E} text\n*/\n"; + // This must not panic; also verify the contents are preserved intact + // (since the multi-byte character prevents this comment from being + // recognized as consistently indented, it should be left unchanged). + assert_eq!( + source_lexer(s, sql), + [build_doc_block( + "", + "/*", + "Test\n \u{65E5}\u{672C}\u{8A9E} text\n\n" + )] + ); +} diff --git a/server/src/main.rs b/server/src/main.rs index 8d2d60e3..7ed54638 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -91,7 +91,7 @@ enum Commands { log: Option, /// Define the username:password used to limit access to the server. By - /// default, access is unlimited. The username may not contain a colon.- + /// default, access is unlimited. The username may not contain a colon. #[arg(short, long, value_parser = parse_credentials)] auth: Option, }, @@ -241,10 +241,12 @@ impl Cli { Ok(Some(status)) => { let mut stdout_buf = String::new(); let mut stderr_buf = String::new(); - let stdout = child.stdout.as_mut().unwrap(); - let stderr = child.stderr.as_mut().unwrap(); - stdout.read_to_string(&mut stdout_buf).unwrap(); - stderr.read_to_string(&mut stderr_buf).unwrap(); + if let Some(stdout) = child.stdout.as_mut() { + let _ = stdout.read_to_string(&mut stdout_buf); + } + if let Some(stderr) = child.stderr.as_mut() { + let _ = stderr.read_to_string(&mut stderr_buf); + } if status.success() { return Err(format!("Server unexpectedly shut down.\n{stdout_buf}\n{stderr_buf}").into()); } diff --git a/server/src/processing.rs b/server/src/processing.rs index 0c48fead..cdf984c6 100644 --- a/server/src/processing.rs +++ b/server/src/processing.rs @@ -489,11 +489,18 @@ fn code_mirror_to_code_doc_blocks(code_mirror: &CodeMirror) -> Vec // Walk through each doc block, inserting the previous code block followed // by the doc block. for codemirror_doc_block in doc_blocks { - // Translate `from`. + // Translate `from`. Use a checked subtraction, since a malformed (for + // example, out-of-order or overlapping) `doc_blocks` array sent by the + // Client could otherwise cause this to underflow and wrap around to a + // huge value, producing a confusing out-of-bounds slice panic below + // instead of a clear error at the point of the actual problem. let byte_index_prev = byte_index; byte_index += byte_index_of( &code_mirror.doc[byte_index..], - codemirror_doc_block.from - utf16_index, + codemirror_doc_block + .from + .checked_sub(utf16_index) + .expect("doc_blocks must be sorted, with non-overlapping from/to ranges"), ); utf16_index = codemirror_doc_block.from; // Append the code block, unless it's empty. @@ -512,7 +519,10 @@ fn code_mirror_to_code_doc_blocks(code_mirror: &CodeMirror) -> Vec // Translate `to`. byte_index += byte_index_of( &code_mirror.doc[byte_index..], - codemirror_doc_block.to - utf16_index, + codemirror_doc_block + .to + .checked_sub(utf16_index) + .expect("doc_blocks must be sorted, with non-overlapping from/to ranges"), ); utf16_index = codemirror_doc_block.to; // Verify that everything between `from` and `to` is newlines. diff --git a/server/src/translation.rs b/server/src/translation.rs index 8b04e796..166b095c 100644 --- a/server/src/translation.rs +++ b/server/src/translation.rs @@ -1489,8 +1489,8 @@ impl TranslationTask { } } -// If a string is encoded using CRLFs (Windows style), convert it to LFs only -// (Unix style). +// Given a string using LF-only (Unix style) line endings, convert it to CRLF +// (Windows style) line endings if `eol_type` requests it. fn eol_convert(s: String, eol_type: &EolType) -> String { if eol_type == &EolType::Crlf { // There shouldn't be any Windows-style CRLFs -- but if there are, diff --git a/server/src/webserver.rs b/server/src/webserver.rs index 8592dc46..a71e4435 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -682,6 +682,15 @@ pub fn get_client_framework( )); } }; + // `connection_id` may be attacker-controlled (for example, the VSCode + // extension's `/vsc/cf/{connection_id}` endpoint takes it directly from + // the URL). Since `ws_url` is embedded verbatim inside a ` From f4c8e8af4f2aa3eaace2c62c6a8523ce2e905c14 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Sat, 25 Jul 2026 21:43:13 -0500 Subject: [PATCH 16/18] Fix: update CI tests with new build tool command. --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d6c55893..08095de8 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -55,4 +55,4 @@ jobs: rustup update cd server ./bt install --dev - ./bt test + ./bt full From 1f36983b8ec7bfa99b72abcdd6826d221b5a9f8e Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Sat, 25 Jul 2026 21:56:19 -0500 Subject: [PATCH 17/18] Fix: conditionally-included Mac dependencies. --- extensions/standalone/tests/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/standalone/tests/cli.rs b/extensions/standalone/tests/cli.rs index 1abb135b..87435caa 100644 --- a/extensions/standalone/tests/cli.rs +++ b/extensions/standalone/tests/cli.rs @@ -28,6 +28,7 @@ use assert_cmd::Command; use predicates::{prelude::predicate, str::contains}; // ### Local +#[cfg(not(target_os = "macos"))] use test_utils::prep_test_dir; use tokio::task::spawn_blocking; From 3577c546c517bd2fc8ecb878e43f5f648c5ecae1 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Sat, 25 Jul 2026 21:57:22 -0500 Subject: [PATCH 18/18] Clean: format/lint. --- extensions/standalone/src/filewatcher.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/standalone/src/filewatcher.rs b/extensions/standalone/src/filewatcher.rs index 662ee143..e6589d1a 100644 --- a/extensions/standalone/src/filewatcher.rs +++ b/extensions/standalone/src/filewatcher.rs @@ -19,13 +19,13 @@ // ------- // // ### Standard library +#[cfg(windows)] +use std::sync::LazyLock; use std::{ fmt::Write, path::{Path, PathBuf}, time::Duration, }; -#[cfg(windows)] -use std::sync::LazyLock; // ### Third-party use actix_web::{