diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96c3a3b..3c8f69e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,55 @@ jobs: echo "::group::Build" && cargo build --verbose && echo "::endgroup::" && echo "::group::Run tests" && cargo test --verbose && echo "::endgroup::" && echo "::group::Run lints" && cargo clippy --all-targets -- -D warnings + + test-cross-platform: + name: Test on ${{ matrix.target }} (${{ matrix.os }}) + needs: + - build + strategy: + fail-fast: false + matrix: + include: + # Linux (Standard & ARM) + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + # macOS (Intel & Apple Silicon) + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-apple-darwin + os: macos-latest + # Windows + - target: x86_64-pc-windows-msvc + os: windows-latest + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: Install Linker (Linux ARM) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + gcc-aarch64-linux-gnu \ + libc6-dev-arm64-cross + echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + + - name: Build Binary + run: cargo build --verbose --target ${{ matrix.target }} + + - name: Run Tests + # We only run tests if the target matches the runner's native architecture + # to avoid execution errors on cross-compiled binaries. + if: contains(matrix.target, 'x86_64') || (contains(matrix.target, 'aarch64') && contains(matrix.os, 'macos')) + run: cargo test --verbose --target ${{ matrix.target }} diff --git a/src/config/workspace.rs b/src/config/workspace.rs index db3c838..f86aaed 100644 --- a/src/config/workspace.rs +++ b/src/config/workspace.rs @@ -270,11 +270,11 @@ mod test { let f = tmpdir.path().join("protols.toml"); std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap(); + let absolute_path = tmpdir.path().join("absolute_test_path"); + // Set CLI include paths - let cli_paths = vec![ - PathBuf::from("/path/to/protos"), - PathBuf::from("relative/path"), - ]; + let cli_paths = vec![absolute_path.clone(), PathBuf::from("relative/path")]; + let mut ws = WorkspaceProtoConfigs::new(cli_paths, None); ws.add_workspace(&WorkspaceFolder { uri: Url::from_directory_path(tmpdir.path()).unwrap(), @@ -284,19 +284,12 @@ mod test { let inworkspace = Url::from_file_path(tmpdir.path().join("foobar.proto")).unwrap(); let include_paths = ws.get_include_paths(&inworkspace).unwrap(); - // Check that CLI paths are included in the result - assert!( - include_paths - .iter() - .any(|p| p.ends_with("relative/path") || p == &PathBuf::from("/path/to/protos")) - ); + // The absolute path should be included as is + assert!(include_paths.contains(&absolute_path)); // The relative path should be resolved relative to the workspace let resolved_relative_path = tmpdir.path().join("relative/path"); assert!(include_paths.contains(&resolved_relative_path)); - - // The absolute path should be included as is - assert!(include_paths.contains(&PathBuf::from("/path/to/protos"))); } #[test] @@ -305,10 +298,13 @@ mod test { let f = tmpdir.path().join("protols.toml"); std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap(); + let cli_absolute_path = tmpdir.path().join("cli/path"); + let init_absolute_path = tmpdir.path().join("init/path1"); + // Set both CLI and initialization include paths - let cli_paths = vec![PathBuf::from("/cli/path")]; + let cli_paths = vec![cli_absolute_path.clone()]; let init_paths = vec![ - PathBuf::from("/init/path1"), + init_absolute_path.clone(), PathBuf::from("relative/init/path"), ]; @@ -323,14 +319,14 @@ mod test { let include_paths = ws.get_include_paths(&inworkspace).unwrap(); // Check that initialization paths are included - assert!(include_paths.contains(&PathBuf::from("/init/path1"))); + assert!(include_paths.contains(&init_absolute_path)); // The relative path should be resolved relative to the workspace let resolved_relative_path = tmpdir.path().join("relative/init/path"); assert!(include_paths.contains(&resolved_relative_path)); // CLI paths should still be included - assert!(include_paths.contains(&PathBuf::from("/cli/path"))); + assert!(include_paths.contains(&cli_absolute_path)); } #[test] diff --git a/src/formatter/clang.rs b/src/formatter/clang.rs index 1d6c753..12911f8 100644 --- a/src/formatter/clang.rs +++ b/src/formatter/clang.rs @@ -43,7 +43,13 @@ impl Replacement<'_> { if offset > content.len() { return None; } - let up_to_offset = &content[..offset]; + + // Use floor_char_boundary to ensure we don't slice in the middle of a + // multi-byte UTF-8 character (e.g., Cyrillic), which would cause a panic. + // This handles slight offset shifts caused by different OS line endings. + let safe_offset = content.floor_char_boundary(offset); + + let up_to_offset = &content[..safe_offset]; let line = up_to_offset.matches('\n').count(); let last_newline = up_to_offset.rfind('\n').map_or(0, |pos| pos + 1); @@ -202,13 +208,27 @@ mod test { // Test that the complete flow works with Cyrillic characters // This simulates what clang-format would output for the Cyrillic comment let content = include_str!("input/test_cyrillic.proto"); - let xml_output = r#" + + // We use a dynamic offset instead of a hardcoded byte index (like 134) + // because Windows uses CRLF (\r\n) while Linux uses LF (\n). + // Git's autocrlf can shift byte positions on Windows, potentially + // landing a fixed offset in the middle of a multi-byte UTF-8 character + // (like Cyrillic). Finding the target string in memory ensures we hit + // the correct character boundary regardless of the OS line endings. + let target = " removed_not_true"; + let offset = content + .find(target) + .expect("Could not find target in content"); + let xml_output = format!( + r#" - + // -"#; +"#, + offset + ); - let r = Replacements::from_str(xml_output).unwrap(); + let r = Replacements::from_str(&xml_output).unwrap(); assert_eq!(r.replacements.len(), 1); let replacement = &r.replacements[0]; diff --git a/src/workspace/workspace_symbol.rs b/src/workspace/workspace_symbol.rs index 845df0e..7e91b43 100644 --- a/src/workspace/workspace_symbol.rs +++ b/src/workspace/workspace_symbol.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod test { + use async_lsp::lsp_types::Url; use insta::assert_yaml_snapshot; use crate::config::Config; @@ -9,24 +10,14 @@ mod test { fn test_workspace_symbols() { let current_dir = std::env::current_dir().unwrap(); let ipath = vec![current_dir.join("src/workspace/input")]; - let a_uri = format!( - "file://{}/src/workspace/input/a.proto", - current_dir.to_str().unwrap() - ) - .parse() - .unwrap(); - let b_uri = format!( - "file://{}/src/workspace/input/b.proto", - current_dir.to_str().unwrap() - ) - .parse() - .unwrap(); - let c_uri = format!( - "file://{}/src/workspace/input/c.proto", - current_dir.to_str().unwrap() - ) - .parse() - .unwrap(); + let base_uri_str = Url::from_directory_path(¤t_dir) + .unwrap() + .to_string() + .trim_end_matches('/') + .to_string(); + let a_uri = Url::from_file_path(current_dir.join("src/workspace/input/a.proto")).unwrap(); + let b_uri = Url::from_file_path(current_dir.join("src/workspace/input/b.proto")).unwrap(); + let c_uri = Url::from_file_path(current_dir.join("src/workspace/input/c.proto")).unwrap(); let a = include_str!("input/a.proto"); let b = include_str!("input/b.proto"); @@ -39,47 +30,49 @@ mod test { // Test empty query - should return all symbols let all_symbols = state.find_workspace_symbols(""); - let cdir = current_dir.to_str().unwrap().to_string(); + let base_uri_1 = base_uri_str.clone(); assert_yaml_snapshot!(all_symbols, { "[].location.uri" => insta::dynamic_redaction(move |c, _| { + let uri_str = c.as_str().unwrap(); + assert!( - c.as_str() - .unwrap() - .contains(&cdir) + uri_str.contains(&base_uri_1), + "URI {} should contain {}", uri_str, base_uri_1 ); - format!( - "file:///src/workspace/input/{}", - c.as_str().unwrap().split('/').next_back().unwrap() - ) + + let file_name = uri_str.split('/').next_back().unwrap(); + format!("file:///src/workspace/input/{}", file_name) })}); // Test query for "author" - should match Author and Address let author_symbols = state.find_workspace_symbols("author"); - let cdir = current_dir.to_str().unwrap().to_string(); + let base_uri_2 = base_uri_str.clone(); assert_yaml_snapshot!(author_symbols, {"[].location.uri" => insta::dynamic_redaction(move |c ,_|{ + let uri_str = c.as_str().unwrap(); + assert!( - c.as_str() - .unwrap() - .contains(&cdir) + uri_str.contains(&base_uri_2), + "URI {} should contain {}", uri_str, base_uri_2 ); - format!( - "file:///src/workspace/input/{}", - c.as_str().unwrap().split('/').next_back().unwrap() - ) + + let file_name = uri_str.split('/').next_back().unwrap(); + format!("file:///src/workspace/input/{}", file_name) })}); // Test query for "address" - should match Address let address_symbols = state.find_workspace_symbols("address"); + let base_uri_3 = base_uri_str.clone(); assert_yaml_snapshot!(address_symbols, {"[].location.uri" => insta::dynamic_redaction(move |c ,_|{ + let uri_str = c.as_str().unwrap(); + assert!( - c.as_str() - .unwrap() - .contains(current_dir.to_str().unwrap()) + uri_str.contains(&base_uri_3), + "URI {} should contain {}", uri_str, base_uri_3 ); - format!( - "file:///src/workspace/input/{}", - c.as_str().unwrap().split('/').next_back().unwrap() - ) + + + let file_name = uri_str.split('/').next_back().unwrap(); + format!("file:///src/workspace/input/{}", file_name) })}); // Test query that should not match anything