Skip to content
Merged
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
27 changes: 27 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -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:?}"
);
}
}
48 changes: 42 additions & 6 deletions src/config/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ pub struct WorkspaceProtoConfigs {
protoc_include_prefix: Vec<PathBuf>,
cli_include_paths: Vec<PathBuf>,
init_include_paths: Vec<PathBuf>,
fallback_include_path: Option<PathBuf>,
}

impl WorkspaceProtoConfigs {
pub fn new(cli_include_paths: Vec<PathBuf>) -> Self {
pub fn new(cli_include_paths: Vec<PathBuf>, fallback_include_path: Option<PathBuf>) -> 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
Expand All @@ -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(),
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
Expand Down Expand Up @@ -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(),
Expand All @@ -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)
);
}
}
26 changes: 25 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Vec<String>>,
}

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();
Expand All @@ -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({
Expand Down
8 changes: 6 additions & 2 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ pub struct ProtoLanguageServer {
}

impl ProtoLanguageServer {
pub fn new_router(client: ClientSocket, cli_include_paths: Vec<PathBuf>) -> Router<Self> {
pub fn new_router(
client: ClientSocket,
cli_include_paths: Vec<PathBuf>,
fallback_include_path: Option<PathBuf>,
) -> Router<Self> {
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::<TickEvent>(|st, _| {
Expand Down