diff --git a/Cargo.lock b/Cargo.lock index 5878482..2ed4822 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -884,6 +904,7 @@ dependencies = [ "async-lsp", "basic-toml", "clap", + "const_format", "futures", "hard-xml", "insta", @@ -1451,6 +1472,12 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "url" version = "2.5.7" diff --git a/Cargo.toml b/Cargo.toml index c891e33..a449506 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ serde_json = "1.0" basic-toml = "0.1" pkg-config = "0.3" clap = { version = "4.5", features = ["derive"] } +const_format = "0.2" [dev-dependencies] insta = { version = "1.43", features = ["yaml", "redactions"] } diff --git a/README.md b/README.md index 02afed8..348fc9c 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ - [Hover Information](#hover-information) - [Rename Symbols](#rename-symbols) - [Find References](#find-references) +- [Protocol Buffers Well-Known Types](#protocol-buffers-well-known-types) +- [Packaging](#📦-packaging) - [Contributing](#contributing) - [Setting Up Locally](#setting-up-locally) - [License](#license) @@ -130,7 +132,12 @@ The `[config]` section contains stable settings that should generally remain unc - **Configuration file**: Workspace-specific paths defined in `protols.toml` - **Command line**: Global paths using `--include-paths` flag that apply to all workspaces - **Initialization parameters**: Dynamic paths set via LSP `initializationParams` (useful for editors like Neovim) - + + When a file is not found in any of the paths above, the following directories are searched: + - **Protobuf Include Path**: the path containing the [Protocol Buffers Well-Known Types](https://protobuf.dev/reference/protobuf/google.protobuf/) as detected by [`pkg-config`](https://www.freedesktop.org/wiki/Software/pkg-config/) (requires `pkg-config` present in environment and capable of finding the installation of `protobuf`) + - **Fallback Include Path**: the fallback include path configured at compile time + + All include paths from these sources are combined when resolving proto imports. #### Path Configuration @@ -182,8 +189,27 @@ Rename symbols like messages or enums, and Propagate the changes throughout the Find all references to user-defined types like messages or enums. Nested fields are fully supported, making it easier to track symbol usage across your project. +## Protocol Buffers Well-Known Types + +Protols does not ship with the [Protocol Buffers Well-Known Types](https://protobuf.dev/reference/protobuf/google.protobuf/) unless configured to do so by a distribution. +In order for features above to work for the well-known types, the well-known imports must either resolve against one of the configured import paths or the environment must contain in `PATH` a `pkg-config` executable capable of resolving the package `protobuf`. +You can verify this by running + +```bash +pkg-config --modversion protobuf +``` + +in protols' environment. + --- +## 📦 Packaging + +Distributions may set an absolute include path which contains the Protocol Buffers Well-Known Types, +for example pointing to the files provided by the `protobuf` package, by compiling protols with the +environment variable `FALLBACK_INCLUDE_PATH` set to the desired path. This path will be used by the +compiled executable for resolution of any proto files that could not be resolved otherwise. + ## 🤝 Contributing We welcome contributions from developers of all experience levels! To get started: diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..ec80ab6 --- /dev/null +++ b/build.rs @@ -0,0 +1,11 @@ +use std::path::Path; + +fn main() { + if let Some(path) = option_env!("FALLBACK_INCLUDE_PATH") { + let path = Path::new(path); + assert!( + path.is_absolute(), + "Environment variable FALLBACK_INCLUDE_PATH must be absolute: {path:?}" + ); + } +} diff --git a/src/config/workspace.rs b/src/config/workspace.rs index d094d1e..db3c838 100644 --- a/src/config/workspace.rs +++ b/src/config/workspace.rs @@ -20,10 +20,11 @@ pub struct WorkspaceProtoConfigs { protoc_include_prefix: Vec, cli_include_paths: Vec, init_include_paths: Vec, + fallback_include_path: Option, } impl WorkspaceProtoConfigs { - pub fn new(cli_include_paths: Vec) -> Self { + pub fn new(cli_include_paths: Vec, fallback_include_path: Option) -> Self { // Try to find protobuf library and get its include paths // Do not emit metadata on stdout as LSP programs can consider // it part of spec @@ -39,6 +40,7 @@ impl WorkspaceProtoConfigs { workspaces: HashSet::new(), formatters: HashMap::new(), configs: HashMap::new(), + fallback_include_path, protoc_include_prefix, cli_include_paths, init_include_paths: Vec::new(), @@ -128,6 +130,7 @@ impl WorkspaceProtoConfigs { ipath.push(w.to_path_buf()); ipath.extend_from_slice(&self.protoc_include_prefix); + ipath.extend_from_slice(self.fallback_include_path.as_slice()); Some(ipath) } @@ -180,7 +183,7 @@ mod test { let f = tmpdir.path().join("protols.toml"); std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap(); - let mut ws = WorkspaceProtoConfigs::new(vec![]); + let mut ws = WorkspaceProtoConfigs::new(vec![], None); ws.add_workspace(&WorkspaceFolder { uri: Url::from_directory_path(tmpdir.path()).unwrap(), name: "Test".to_string(), @@ -214,7 +217,7 @@ mod test { let f = tmpdir.path().join("protols.toml"); std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap(); - let mut ws = WorkspaceProtoConfigs::new(vec![]); + let mut ws = WorkspaceProtoConfigs::new(vec![], None); ws.add_workspace(&WorkspaceFolder { uri: Url::from_directory_path(tmpdir.path()).unwrap(), name: "Test".to_string(), @@ -249,7 +252,7 @@ mod test { let f = tmpdir.path().join(file); std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap(); - let mut ws = WorkspaceProtoConfigs::new(vec![]); + let mut ws = WorkspaceProtoConfigs::new(vec![], None); ws.add_workspace(&WorkspaceFolder { uri: Url::from_directory_path(tmpdir.path()).unwrap(), name: "Test".to_string(), @@ -272,7 +275,7 @@ mod test { PathBuf::from("/path/to/protos"), PathBuf::from("relative/path"), ]; - let mut ws = WorkspaceProtoConfigs::new(cli_paths); + let mut ws = WorkspaceProtoConfigs::new(cli_paths, None); ws.add_workspace(&WorkspaceFolder { uri: Url::from_directory_path(tmpdir.path()).unwrap(), name: "Test".to_string(), @@ -309,7 +312,7 @@ mod test { PathBuf::from("relative/init/path"), ]; - let mut ws = WorkspaceProtoConfigs::new(cli_paths); + let mut ws = WorkspaceProtoConfigs::new(cli_paths, None); ws.set_init_include_paths(init_paths); ws.add_workspace(&WorkspaceFolder { uri: Url::from_directory_path(tmpdir.path()).unwrap(), @@ -329,4 +332,37 @@ mod test { // CLI paths should still be included assert!(include_paths.contains(&PathBuf::from("/cli/path"))); } + + #[test] + fn test_fallback_include_path() { + let tmpdir = tempdir().expect("failed to create temp directory"); + let f = tmpdir.path().join("protols.toml"); + std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap(); + + // Set both CLI and initialization include paths + let cli_paths = vec![PathBuf::from("/cli/path")]; + let init_paths = vec![ + PathBuf::from("/init/path1"), + PathBuf::from("relative/init/path"), + ]; + + let mut ws = WorkspaceProtoConfigs::new(cli_paths, Some("fallback_path".into())); + ws.set_init_include_paths(init_paths); + ws.add_workspace(&WorkspaceFolder { + uri: Url::from_directory_path(tmpdir.path()).unwrap(), + name: "Test".to_string(), + }); + + let inworkspace = Url::from_file_path(tmpdir.path().join("foobar.proto")).unwrap(); + let include_paths = ws.get_include_paths(&inworkspace).unwrap(); + + // Fallback path should be included and on the last position + assert_eq!( + include_paths + .iter() + .rev() + .position(|p| p == "fallback_path"), + Some(0) + ); + } } diff --git a/src/main.rs b/src/main.rs index 348f17f..8b796f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use async_lsp::panic::CatchUnwindLayer; use async_lsp::server::LifecycleLayer; use async_lsp::tracing::TracingLayer; use clap::Parser; +use const_format::concatcp; use server::{ProtoLanguageServer, TickEvent}; use tower::ServiceBuilder; use tracing::Level; @@ -25,13 +26,32 @@ mod workspace; /// Language server for proto3 files #[derive(Parser, Debug)] -#[command(author, version, about, long_about = None, ignore_errors(true))] +#[command( + author, + version = concatcp!( + env!("CARGO_PKG_VERSION"), + "\n", + BUILD_INFO + ), + about, + long_about = None, + ignore_errors(true) +)] struct Cli { /// Include paths for proto files #[arg(short, long, value_delimiter = ',')] include_paths: Option>, } +const FALLBACK_INCLUDE_PATH: Option<&str> = option_env!("FALLBACK_INCLUDE_PATH"); +const BUILD_INFO: &str = concatcp!( + "fallback include path: ", + match FALLBACK_INCLUDE_PATH { + Some(path) => path, + None => "not set", + } +); + #[tokio::main(flavor = "current_thread")] async fn main() { let cli = Cli::parse(); @@ -48,14 +68,18 @@ async fn main() { .with_writer(file_appender.0) .init(); + let fallback_include_path = FALLBACK_INCLUDE_PATH.map(Into::into); + tracing::info!("server version: {}", env!("CARGO_PKG_VERSION")); let (server, _) = async_lsp::MainLoop::new_server(|client| { tracing::info!("Using CLI options: {:?}", cli); + tracing::info!("Using fallback include path: {:?}", fallback_include_path); let router = ProtoLanguageServer::new_router( client.clone(), cli.include_paths .map(|ic| ic.into_iter().map(std::path::PathBuf::from).collect()) .unwrap_or_default(), + fallback_include_path, ); tokio::spawn({ diff --git a/src/server.rs b/src/server.rs index 11e299d..b5a3ada 100644 --- a/src/server.rs +++ b/src/server.rs @@ -32,12 +32,16 @@ pub struct ProtoLanguageServer { } impl ProtoLanguageServer { - pub fn new_router(client: ClientSocket, cli_include_paths: Vec) -> Router { + pub fn new_router( + client: ClientSocket, + cli_include_paths: Vec, + fallback_include_path: Option, + ) -> Router { let mut router = Router::new(Self { client, counter: 0, state: ProtoLanguageState::new(), - configs: WorkspaceProtoConfigs::new(cli_include_paths), + configs: WorkspaceProtoConfigs::new(cli_include_paths, fallback_include_path), }); router.event::(|st, _| {