Skip to content

Commit d7eeca8

Browse files
authored
Merge branch 'main' into rename-services-and-references
2 parents a6b6f40 + be80bd3 commit d7eeca8

8 files changed

Lines changed: 144 additions & 65 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"name": "Rust",
3-
"image": "ghcr.io/coder3101/protols/devcontainer:latest",
43
"build": {
54
"dockerfile": "Dockerfile",
6-
"context": ".."
5+
"context": "..",
6+
"cacheFrom": "ghcr.io/coder3101/protols/devcontainer:latest"
77
},
88
"features": {
99
"ghcr.io/devcontainers/features/rust:1": {

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* text=auto eol=lf

.github/workflows/ci.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,55 @@ jobs:
2828
echo "::group::Build" && cargo build --verbose && echo "::endgroup::" &&
2929
echo "::group::Run tests" && cargo test --verbose && echo "::endgroup::" &&
3030
echo "::group::Run lints" && cargo clippy --all-targets -- -D warnings
31+
32+
test-cross-platform:
33+
name: Test on ${{ matrix.target }} (${{ matrix.os }})
34+
needs:
35+
- build
36+
strategy:
37+
fail-fast: false
38+
matrix:
39+
include:
40+
# Linux (Standard & ARM)
41+
- target: x86_64-unknown-linux-gnu
42+
os: ubuntu-latest
43+
- target: aarch64-unknown-linux-gnu
44+
os: ubuntu-latest
45+
# macOS (Intel & Apple Silicon)
46+
- target: x86_64-apple-darwin
47+
os: macos-latest
48+
- target: aarch64-apple-darwin
49+
os: macos-latest
50+
# Windows
51+
- target: x86_64-pc-windows-msvc
52+
os: windows-latest
53+
runs-on: ${{ matrix.os }}
54+
steps:
55+
- uses: actions/checkout@v6
56+
57+
- name: Install Rust
58+
uses: dtolnay/rust-toolchain@stable
59+
with:
60+
targets: ${{ matrix.target }}
61+
62+
- name: Rust Cache
63+
uses: Swatinem/rust-cache@v2
64+
65+
- name: Install Linker (Linux ARM)
66+
if: matrix.target == 'aarch64-unknown-linux-gnu'
67+
run: |
68+
sudo apt-get update
69+
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
70+
gcc-aarch64-linux-gnu \
71+
libc6-dev-arm64-cross
72+
echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
73+
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
74+
75+
- name: Build Binary
76+
run: cargo build --verbose --target ${{ matrix.target }}
77+
78+
- name: Run Tests
79+
# We only run tests if the target matches the runner's native architecture
80+
# to avoid execution errors on cross-compiled binaries.
81+
if: contains(matrix.target, 'x86_64') || (contains(matrix.target, 'aarch64') && contains(matrix.os, 'macos'))
82+
run: cargo test --verbose --target ${{ matrix.target }}

.github/workflows/prebuild_devcontainer.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,23 @@ jobs:
2020
steps:
2121
- name: Checkout
2222
uses: actions/checkout@v6
23+
24+
- name: Set up Docker Buildx
25+
uses: docker/setup-buildx-action@v4
26+
27+
- name: Log in to GitHub Container Registry
28+
uses: docker/login-action@v3
29+
with:
30+
registry: ghcr.io
31+
username: ${{ github.actor }}
32+
password: ${{ secrets.GITHUB_TOKEN }}
33+
2334
- name: Pre-build and push
2435
uses: devcontainers/ci@v0.3
2536
with:
2637
imageName: ghcr.io/${{ github.repository }}/devcontainer
2738
cacheFrom: ghcr.io/${{ github.repository }}/devcontainer
39+
cacheTo: ghcr.io/${{ github.repository }}/devcontainer
2840
push: filter
2941
refFilterForPush: refs/heads/main
3042
eventFilterForPush: push, workflow_dispatch

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,8 @@ const_format = "0.2"
3434

3535
[dev-dependencies]
3636
insta = { version = "1.47", features = ["yaml", "redactions"] }
37+
38+
[profile.release]
39+
strip = "debuginfo" # Removes heavy debug data but keeps function names for panic logs
40+
lto = true # Enables Link-Time Optimization for cross-crate improvements
41+
codegen-units = 1 # Maximizes LLVM optimization passes

src/config/workspace.rs

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,11 @@ mod test {
270270
let f = tmpdir.path().join("protols.toml");
271271
std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap();
272272

273+
let absolute_path = tmpdir.path().join("absolute_test_path");
274+
273275
// Set CLI include paths
274-
let cli_paths = vec![
275-
PathBuf::from("/path/to/protos"),
276-
PathBuf::from("relative/path"),
277-
];
276+
let cli_paths = vec![absolute_path.clone(), PathBuf::from("relative/path")];
277+
278278
let mut ws = WorkspaceProtoConfigs::new(cli_paths, None);
279279
ws.add_workspace(&WorkspaceFolder {
280280
uri: Url::from_directory_path(tmpdir.path()).unwrap(),
@@ -284,19 +284,12 @@ mod test {
284284
let inworkspace = Url::from_file_path(tmpdir.path().join("foobar.proto")).unwrap();
285285
let include_paths = ws.get_include_paths(&inworkspace).unwrap();
286286

287-
// Check that CLI paths are included in the result
288-
assert!(
289-
include_paths
290-
.iter()
291-
.any(|p| p.ends_with("relative/path") || p == &PathBuf::from("/path/to/protos"))
292-
);
287+
// The absolute path should be included as is
288+
assert!(include_paths.contains(&absolute_path));
293289

294290
// The relative path should be resolved relative to the workspace
295291
let resolved_relative_path = tmpdir.path().join("relative/path");
296292
assert!(include_paths.contains(&resolved_relative_path));
297-
298-
// The absolute path should be included as is
299-
assert!(include_paths.contains(&PathBuf::from("/path/to/protos")));
300293
}
301294

302295
#[test]
@@ -305,10 +298,13 @@ mod test {
305298
let f = tmpdir.path().join("protols.toml");
306299
std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap();
307300

301+
let cli_absolute_path = tmpdir.path().join("cli/path");
302+
let init_absolute_path = tmpdir.path().join("init/path1");
303+
308304
// Set both CLI and initialization include paths
309-
let cli_paths = vec![PathBuf::from("/cli/path")];
305+
let cli_paths = vec![cli_absolute_path.clone()];
310306
let init_paths = vec![
311-
PathBuf::from("/init/path1"),
307+
init_absolute_path.clone(),
312308
PathBuf::from("relative/init/path"),
313309
];
314310

@@ -323,14 +319,14 @@ mod test {
323319
let include_paths = ws.get_include_paths(&inworkspace).unwrap();
324320

325321
// Check that initialization paths are included
326-
assert!(include_paths.contains(&PathBuf::from("/init/path1")));
322+
assert!(include_paths.contains(&init_absolute_path));
327323

328324
// The relative path should be resolved relative to the workspace
329325
let resolved_relative_path = tmpdir.path().join("relative/init/path");
330326
assert!(include_paths.contains(&resolved_relative_path));
331327

332328
// CLI paths should still be included
333-
assert!(include_paths.contains(&PathBuf::from("/cli/path")));
329+
assert!(include_paths.contains(&cli_absolute_path));
334330
}
335331

336332
#[test]

src/formatter/clang.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ impl Replacement<'_> {
4343
if offset > content.len() {
4444
return None;
4545
}
46-
let up_to_offset = &content[..offset];
46+
47+
// Use floor_char_boundary to ensure we don't slice in the middle of a
48+
// multi-byte UTF-8 character (e.g., Cyrillic), which would cause a panic.
49+
// This handles slight offset shifts caused by different OS line endings.
50+
let safe_offset = content.floor_char_boundary(offset);
51+
52+
let up_to_offset = &content[..safe_offset];
4753
let line = up_to_offset.matches('\n').count();
4854
let last_newline = up_to_offset.rfind('\n').map_or(0, |pos| pos + 1);
4955

@@ -202,13 +208,27 @@ mod test {
202208
// Test that the complete flow works with Cyrillic characters
203209
// This simulates what clang-format would output for the Cyrillic comment
204210
let content = include_str!("input/test_cyrillic.proto");
205-
let xml_output = r#"<?xml version='1.0'?>
211+
212+
// We use a dynamic offset instead of a hardcoded byte index (like 134)
213+
// because Windows uses CRLF (\r\n) while Linux uses LF (\n).
214+
// Git's autocrlf can shift byte positions on Windows, potentially
215+
// landing a fixed offset in the middle of a multi-byte UTF-8 character
216+
// (like Cyrillic). Finding the target string in memory ensures we hit
217+
// the correct character boundary regardless of the OS line endings.
218+
let target = " removed_not_true";
219+
let offset = content
220+
.find(target)
221+
.expect("Could not find target in content");
222+
let xml_output = format!(
223+
r#"<?xml version='1.0'?>
206224
<replacements xml:space='preserve' incomplete_format='false'>
207-
<replacement offset='134' length='1'>
225+
<replacement offset='{}' length='1'>
208226
// </replacement>
209-
</replacements>"#;
227+
</replacements>"#,
228+
offset
229+
);
210230

211-
let r = Replacements::from_str(xml_output).unwrap();
231+
let r = Replacements::from_str(&xml_output).unwrap();
212232
assert_eq!(r.replacements.len(), 1);
213233

214234
let replacement = &r.replacements[0];

src/workspace/workspace_symbol.rs

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#[cfg(test)]
22
mod test {
3+
use async_lsp::lsp_types::Url;
34
use insta::assert_yaml_snapshot;
45

56
use crate::config::Config;
@@ -9,24 +10,14 @@ mod test {
910
fn test_workspace_symbols() {
1011
let current_dir = std::env::current_dir().unwrap();
1112
let ipath = vec![current_dir.join("src/workspace/input")];
12-
let a_uri = format!(
13-
"file://{}/src/workspace/input/a.proto",
14-
current_dir.to_str().unwrap()
15-
)
16-
.parse()
17-
.unwrap();
18-
let b_uri = format!(
19-
"file://{}/src/workspace/input/b.proto",
20-
current_dir.to_str().unwrap()
21-
)
22-
.parse()
23-
.unwrap();
24-
let c_uri = format!(
25-
"file://{}/src/workspace/input/c.proto",
26-
current_dir.to_str().unwrap()
27-
)
28-
.parse()
29-
.unwrap();
13+
let base_uri_str = Url::from_directory_path(&current_dir)
14+
.unwrap()
15+
.to_string()
16+
.trim_end_matches('/')
17+
.to_string();
18+
let a_uri = Url::from_file_path(current_dir.join("src/workspace/input/a.proto")).unwrap();
19+
let b_uri = Url::from_file_path(current_dir.join("src/workspace/input/b.proto")).unwrap();
20+
let c_uri = Url::from_file_path(current_dir.join("src/workspace/input/c.proto")).unwrap();
3021

3122
let a = include_str!("input/a.proto");
3223
let b = include_str!("input/b.proto");
@@ -39,47 +30,49 @@ mod test {
3930

4031
// Test empty query - should return all symbols
4132
let all_symbols = state.find_workspace_symbols("");
42-
let cdir = current_dir.to_str().unwrap().to_string();
33+
let base_uri_1 = base_uri_str.clone();
4334
assert_yaml_snapshot!(all_symbols, { "[].location.uri" => insta::dynamic_redaction(move |c, _| {
35+
let uri_str = c.as_str().unwrap();
36+
4437
assert!(
45-
c.as_str()
46-
.unwrap()
47-
.contains(&cdir)
38+
uri_str.contains(&base_uri_1),
39+
"URI {} should contain {}", uri_str, base_uri_1
4840
);
49-
format!(
50-
"file://<redacted>/src/workspace/input/{}",
51-
c.as_str().unwrap().split('/').next_back().unwrap()
52-
)
41+
42+
let file_name = uri_str.split('/').next_back().unwrap();
43+
format!("file://<redacted>/src/workspace/input/{}", file_name)
5344

5445
})});
5546

5647
// Test query for "author" - should match Author and Address
5748
let author_symbols = state.find_workspace_symbols("author");
58-
let cdir = current_dir.to_str().unwrap().to_string();
49+
let base_uri_2 = base_uri_str.clone();
5950
assert_yaml_snapshot!(author_symbols, {"[].location.uri" => insta::dynamic_redaction(move |c ,_|{
51+
let uri_str = c.as_str().unwrap();
52+
6053
assert!(
61-
c.as_str()
62-
.unwrap()
63-
.contains(&cdir)
54+
uri_str.contains(&base_uri_2),
55+
"URI {} should contain {}", uri_str, base_uri_2
6456
);
65-
format!(
66-
"file://<redacted>/src/workspace/input/{}",
67-
c.as_str().unwrap().split('/').next_back().unwrap()
68-
)
57+
58+
let file_name = uri_str.split('/').next_back().unwrap();
59+
format!("file://<redacted>/src/workspace/input/{}", file_name)
6960
})});
7061

7162
// Test query for "address" - should match Address
7263
let address_symbols = state.find_workspace_symbols("address");
64+
let base_uri_3 = base_uri_str.clone();
7365
assert_yaml_snapshot!(address_symbols, {"[].location.uri" => insta::dynamic_redaction(move |c ,_|{
66+
let uri_str = c.as_str().unwrap();
67+
7468
assert!(
75-
c.as_str()
76-
.unwrap()
77-
.contains(current_dir.to_str().unwrap())
69+
uri_str.contains(&base_uri_3),
70+
"URI {} should contain {}", uri_str, base_uri_3
7871
);
79-
format!(
80-
"file://<redacted>/src/workspace/input/{}",
81-
c.as_str().unwrap().split('/').next_back().unwrap()
82-
)
72+
73+
74+
let file_name = uri_str.split('/').next_back().unwrap();
75+
format!("file://<redacted>/src/workspace/input/{}", file_name)
8376
})});
8477

8578
// Test query that should not match anything

0 commit comments

Comments
 (0)