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
1 change: 1 addition & 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 @@ -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"] }
Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "stable"
3 changes: 0 additions & 3 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
49 changes: 49 additions & 0 deletions src/config/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,6 +41,7 @@ impl WorkspaceProtoConfigs {
configs: HashMap::new(),
protoc_include_prefix,
cli_include_paths,
init_include_paths: Vec::new(),
}
}

Expand Down Expand Up @@ -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()?;
Expand All @@ -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)
Expand Down Expand Up @@ -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 changes: 109 additions & 2 deletions src/lsp.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -38,6 +39,17 @@ impl ProtoLanguageServer {

info!("Connected with client {cname} {cversion}");

// Parse initialization options for include paths
if let Some(init_options) = &params.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 {
Expand Down Expand Up @@ -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"));
}
}