Add support for setting include paths via initializationParams

Co-authored-by: coder3101 <22212259+coder3101@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-09-16 06:31:23 +00:00
parent b7152c8f3c
commit 74cdb92db8
5 changed files with 202 additions and 3 deletions

1
Cargo.lock generated
View file

@ -813,6 +813,7 @@ dependencies = [
"insta",
"pkg-config",
"serde",
"serde_json",
"tempfile",
"tokio",
"tokio-util",

View file

@ -26,6 +26,7 @@ walkdir = "2.5"
hard-xml = "1.41"
tempfile = "3.21"
serde = { version = "1", features = ["derive"] }
serde_json = "1.0"
basic-toml = "0.1"
pkg-config = "0.3"
clap = { version = "4.5", features = ["derive"] }

View file

@ -58,6 +58,24 @@ Then, configure it in your `init.lua` using [nvim-lspconfig](https://github.com/
require'lspconfig'.protols.setup{}
```
#### Setting Include Paths in Neovim
For dynamic configuration of include paths, you can use the `before_init` callback to set them via `initializationParams`:
```lua
require'lspconfig'.protols.setup{
before_init = function(_, config)
config.init_options = {
include_paths = {
"/usr/local/include/protobuf",
"vendor/protos",
"../shared-protos"
}
}
end
}
```
### Command Line Options
Protols supports various command line options to customize its behavior:
@ -106,7 +124,12 @@ protoc = "protoc"
The `[config]` section contains stable settings that should generally remain unchanged.
- `include_paths`: These are directories where `.proto` files are searched. Paths can be absolute or relative to the LSP workspace root, which is already included in the `include_paths`. You can also specify this using the `--include-paths` flag in the command line. The include paths from the CLI are combined with those from the configuration. While configuration-based include paths are specific to a workspace, the CLI-specified paths apply to all workspaces on the server.
- `include_paths`: These are directories where `.proto` files are searched. Paths can be absolute or relative to the LSP workspace root, which is already included in the `include_paths`. You can also specify include paths using:
- **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)
All include paths from these sources are combined when resolving proto imports.
#### Path Configuration

View file

@ -19,6 +19,7 @@ pub struct WorkspaceProtoConfigs {
formatters: HashMap<Url, ClangFormatter>,
protoc_include_prefix: Vec<PathBuf>,
cli_include_paths: Vec<PathBuf>,
init_include_paths: Vec<PathBuf>,
}
impl WorkspaceProtoConfigs {
@ -40,6 +41,7 @@ impl WorkspaceProtoConfigs {
configs: HashMap::new(),
protoc_include_prefix,
cli_include_paths,
init_include_paths: Vec::new(),
}
}
@ -90,6 +92,10 @@ impl WorkspaceProtoConfigs {
.find(|&k| upath.starts_with(k.to_file_path().unwrap()))
}
pub fn set_init_include_paths(&mut self, paths: Vec<PathBuf>) {
self.init_include_paths = paths;
}
pub fn get_include_paths(&self, uri: &Url) -> Option<Vec<PathBuf>> {
let cfg = self.get_config_for_uri(uri)?;
let w = self.get_workspace_for_uri(uri)?.to_file_path().ok()?;
@ -111,6 +117,15 @@ impl WorkspaceProtoConfigs {
}
}
// Add initialization include paths
for path in &self.init_include_paths {
if path.is_relative() {
ipath.push(w.join(path));
} else {
ipath.push(path.clone());
}
}
ipath.push(w.to_path_buf());
ipath.extend_from_slice(&self.protoc_include_prefix);
Some(ipath)
@ -276,4 +291,38 @@ mod test {
// The absolute path should be included as is
assert!(include_paths.contains(&PathBuf::from("/path/to/protos")));
}
#[test]
fn test_init_include_paths() {
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);
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();
// Check that initialization paths are included
assert!(include_paths.contains(&PathBuf::from("/init/path1")));
// 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")));
}
}

View file

@ -1,6 +1,6 @@
use std::ops::ControlFlow;
use std::{collections::HashMap, fs::read_to_string};
use tracing::{error, info};
use std::{collections::HashMap, fs::read_to_string, path::PathBuf};
use tracing::{error, info, warn};
use async_lsp::lsp_types::{
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
@ -18,6 +18,7 @@ use async_lsp::lsp_types::{
};
use async_lsp::{LanguageClient, ResponseError};
use futures::future::BoxFuture;
use serde_json::Value;
use crate::docs;
use crate::formatter::ProtoFormatter;
@ -38,6 +39,14 @@ impl ProtoLanguageServer {
info!("Connected with client {cname} {cversion}");
// Parse initialization options for include paths
if let Some(init_options) = &params.initialization_options {
if let Some(include_paths) = parse_init_include_paths(init_options) {
info!("Setting include paths from initialization options: {:?}", include_paths);
self.configs.set_init_include_paths(include_paths);
}
}
let file_operation_filers = vec![FileOperationFilter {
scheme: Some(String::from("file")),
pattern: FileOperationPattern {
@ -523,3 +532,119 @@ impl ProtoLanguageServer {
ControlFlow::Continue(())
}
}
/// Parse include_paths from initialization options
fn parse_init_include_paths(init_options: &Value) -> Option<Vec<PathBuf>> {
match init_options {
Value::Object(obj) => {
if let Some(Value::Array(paths)) = obj.get("include_paths") {
let mut result = Vec::new();
for path_value in paths {
if let Value::String(path) = path_value {
result.push(PathBuf::from(path));
} else {
warn!("Invalid include path in initialization options: {:?}", path_value);
}
}
if !result.is_empty() {
return Some(result);
}
} else if let Some(Value::String(paths_str)) = obj.get("include_paths") {
// Support comma-separated string format like CLI
let result: Vec<PathBuf> = paths_str
.split(',')
.map(|s| PathBuf::from(s.trim()))
.collect();
if !result.is_empty() {
return Some(result);
}
}
}
_ => {
warn!("initialization_options is not an object: {:?}", init_options);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_init_include_paths_array() {
let init_options = json!({
"include_paths": ["/path/to/protos", "relative/path"]
});
let result = parse_init_include_paths(&init_options).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], PathBuf::from("/path/to/protos"));
assert_eq!(result[1], PathBuf::from("relative/path"));
}
#[test]
fn test_parse_init_include_paths_string() {
let init_options = json!({
"include_paths": "/path1,/path2,relative/path"
});
let result = parse_init_include_paths(&init_options).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0], PathBuf::from("/path1"));
assert_eq!(result[1], PathBuf::from("/path2"));
assert_eq!(result[2], PathBuf::from("relative/path"));
}
#[test]
fn test_parse_init_include_paths_missing() {
let init_options = json!({
"other_option": "value"
});
let result = parse_init_include_paths(&init_options);
assert!(result.is_none());
}
#[test]
fn test_parse_init_include_paths_invalid_format() {
let init_options = json!({
"include_paths": 123
});
let result = parse_init_include_paths(&init_options);
assert!(result.is_none());
}
#[test]
fn test_parse_init_include_paths_mixed_array() {
let init_options = json!({
"include_paths": ["/valid/path", 123, "another/valid/path"]
});
let result = parse_init_include_paths(&init_options).unwrap();
assert_eq!(result.len(), 2); // Only valid strings should be included
assert_eq!(result[0], PathBuf::from("/valid/path"));
assert_eq!(result[1], PathBuf::from("another/valid/path"));
}
#[test]
fn test_initialization_options_integration() {
// Test what a real client would send
let neovim_style_init_options = json!({
"include_paths": [
"/usr/local/include/protobuf",
"vendor/protos",
"../shared-protos"
]
});
let include_paths = parse_init_include_paths(&neovim_style_init_options).unwrap();
assert_eq!(include_paths.len(), 3);
assert_eq!(include_paths[0], PathBuf::from("/usr/local/include/protobuf"));
assert_eq!(include_paths[1], PathBuf::from("vendor/protos"));
assert_eq!(include_paths[2], PathBuf::from("../shared-protos"));
}
}