Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,57 @@ 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
with:
key: ${{ matrix.target }}

- 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 }}
30 changes: 13 additions & 17 deletions src/config/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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]
Expand All @@ -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"),
];

Expand All @@ -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]
Expand Down
30 changes: 25 additions & 5 deletions src/formatter/clang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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#"<?xml version='1.0'?>

// 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#"<?xml version='1.0'?>
<replacements xml:space='preserve' incomplete_format='false'>
<replacement offset='134' length='1'>
<replacement offset='{}' length='1'>
// </replacement>
</replacements>"#;
</replacements>"#,
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];
Expand Down
75 changes: 34 additions & 41 deletions src/workspace/workspace_symbol.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#[cfg(test)]
mod test {
use async_lsp::lsp_types::Url;
use insta::assert_yaml_snapshot;

use crate::config::Config;
Expand All @@ -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(&current_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");
Expand All @@ -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://<redacted>/src/workspace/input/{}",
c.as_str().unwrap().split('/').next_back().unwrap()
)

let file_name = uri_str.split('/').next_back().unwrap();
format!("file://<redacted>/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://<redacted>/src/workspace/input/{}",
c.as_str().unwrap().split('/').next_back().unwrap()
)

let file_name = uri_str.split('/').next_back().unwrap();
format!("file://<redacted>/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://<redacted>/src/workspace/input/{}",
c.as_str().unwrap().split('/').next_back().unwrap()
)


let file_name = uri_str.split('/').next_back().unwrap();
format!("file://<redacted>/src/workspace/input/{}", file_name)
})});

// Test query that should not match anything
Expand Down
Loading