mirror of
https://github.com/coder3101/protols.git
synced 2025-12-23 05:36:51 +00:00
Add support for setting include paths via initializationParams (#91)
This PR adds the ability to configure include paths dynamically through
LSP `initializationParams`, addressing a key limitation for Neovim users
where command-line arguments must be static but initialization options
can be set dynamically.
## Problem
Neovim's LSP configuration requires `cmd` and `args` to be static,
making it impossible to dynamically configure include paths based on
project context. While Neovim supports changing `initializationParams`
via the `before_init` callback, protols didn't support include path
configuration through this mechanism.
## Solution
Extended the LSP initialization process to parse and use include paths
from `initializationParams.include_paths`. The implementation:
- **Integrates seamlessly**: Merges with existing CLI and configuration
file include paths
- **Handles errors gracefully**: Invalid formats are logged but don't
crash the server
- **Maintains compatibility**: All existing functionality continues to
work unchanged
## Usage
Neovim users can now configure include paths dynamically:
```lua
require'lspconfig'.protols.setup{
before_init = function(_, config)
config.init_options = {
include_paths = {
"/usr/local/include/protobuf",
"vendor/protos",
"../shared-protos"
}
}
end
}
```
## Implementation Details
- Extended `WorkspaceProtoConfigs` to store initialization include paths
- Added `parse_init_include_paths()` function to handle JSON parsing
with robust error handling
- Updated include path resolution to merge all sources (config file +
CLI + initialization)
- Added comprehensive test coverage with 6 new test cases
- Updated documentation with usage examples
## Testing
All existing tests continue to pass, plus new tests covering:
- Array format parsing
- String format parsing
- Invalid format handling
- Integration with existing include path sources
- Real-world usage scenarios
Fixes #90.
<!-- START COPILOT CODING AGENT TIPS -->
---
💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: coder3101 <22212259+coder3101@users.noreply.github.com>
Co-authored-by: Ashar <ashar786khan@gmail.com>
Co-authored-by: Ashar <coder3101@users.noreply.github.com>
This commit is contained in:
parent
9c473598a7
commit
e3a23dbe7b
7 changed files with 186 additions and 6 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -806,6 +806,7 @@ dependencies = [
|
|||
"insta",
|
||||
"pkg-config",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
25
README.md
25
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
||||
|
|
@ -16,9 +16,6 @@ pub struct ProtolsConfig {
|
|||
pub config: Config,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct FormatterConfig {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
|
|
|
|||
|
|
@ -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")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
111
src/lsp.rs
111
src/lsp.rs
|
|
@ -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,17 @@ impl ProtoLanguageServer {
|
|||
|
||||
info!("Connected with client {cname} {cversion}");
|
||||
|
||||
// Parse initialization options for include paths
|
||||
if let Some(init_options) = ¶ms.initialization_options
|
||||
&& 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 +535,98 @@ impl ProtoLanguageServer {
|
|||
ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse include_paths from initialization options
|
||||
fn parse_init_include_paths(init_options: &Value) -> Option<Vec<PathBuf>> {
|
||||
let mut result = vec![];
|
||||
let paths = init_options["include_paths"].as_array()?;
|
||||
|
||||
for path_value in paths {
|
||||
if let Some(path) = path_value.as_str() {
|
||||
result.push(PathBuf::from(path));
|
||||
} else {
|
||||
warn!(
|
||||
"Invalid include path in initialization options: {:?}",
|
||||
path_value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[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_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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue