diff --git a/Cargo.lock b/Cargo.lock index 0bc00e6..d9c297d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "async-lsp" version = "0.2.0" @@ -109,6 +159,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "console" version = "0.15.8" @@ -308,6 +404,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -347,6 +449,12 @@ dependencies = [ "similar", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -540,10 +648,11 @@ dependencies = [ [[package]] name = "protols" -version = "0.11.2" +version = "0.11.5" dependencies = [ "async-lsp", "basic-toml", + "clap", "futures", "hard-xml", "insta", @@ -753,6 +862,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -1061,6 +1176,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 536da3e..175789e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "protols" description = "Language server for proto3 files" -version = "0.11.2" +version = "0.11.5" edition = "2024" license = "MIT" homepage = "https://github.com/coder3101/protols" @@ -28,6 +28,7 @@ tempfile = "3.12.0" serde = { version = "1.0.209", features = ["derive"] } basic-toml = "0.1.9" pkg-config = "0.3.31" +clap = { version = "4.5.5", features = ["derive"] } [dev-dependencies] insta = { version = "1.39.0", features = ["yaml"] } diff --git a/README.md b/README.md index eb03524..fc78ee0 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ - [Installation](#installation) - [For Neovim](#for-neovim) + - [Command Line Options](#command-line-options) - [For Visual Studio Code](#for-visual-studio-code) - [Configuration](#configuration) - [Basic Configuration](#basic-configuration) @@ -57,6 +58,25 @@ Then, configure it in your `init.lua` using [nvim-lspconfig](https://github.com/ require'lspconfig'.protols.setup{} ``` +### Command Line Options + +Protols supports various command line options to customize its behavior: + +``` +protols [OPTIONS] + +Options: + -i, --include-paths Include paths for proto files, comma-separated + -V, --version Print version information + -h, --help Print help information +``` + +For example, to specify include paths when starting the language server: + +```bash +protols --include-paths=/path/to/protos,/another/path/to/protos +``` + ### For Visual Studio Code If you're using Visual Studio Code, you can install the [Protobuf Language Support](https://marketplace.visualstudio.com/items?itemName=ianandhum.protobuf-support) extension, which uses this LSP under the hood. @@ -86,7 +106,7 @@ protoc = "protoc" The `[config]` section contains stable settings that should generally remain unchanged. -- `include_paths`: Directories to search for `.proto` files. Absolute or relative to LSP workspace root. Worspace root is already included in include_paths +- `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. #### Path Configuration diff --git a/src/config/workspace.rs b/src/config/workspace.rs index 266791f..29797a7 100644 --- a/src/config/workspace.rs +++ b/src/config/workspace.rs @@ -18,10 +18,11 @@ pub struct WorkspaceProtoConfigs { configs: HashMap, formatters: HashMap, protoc_include_prefix: Vec, + cli_include_paths: Vec, } impl WorkspaceProtoConfigs { - pub fn new() -> Self { + pub fn new(cli_include_paths: Vec) -> 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 @@ -38,6 +39,7 @@ impl WorkspaceProtoConfigs { formatters: HashMap::new(), configs: HashMap::new(), protoc_include_prefix, + cli_include_paths, } } @@ -100,6 +102,15 @@ impl WorkspaceProtoConfigs { .map(|p| if p.is_relative() { w.join(p) } else { p }) .collect(); + // Add CLI include paths + for path in &self.cli_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) @@ -139,6 +150,7 @@ mod test { use async_lsp::lsp_types::{Url, WorkspaceFolder}; use insta::assert_yaml_snapshot; use tempfile::tempdir; + use std::path::PathBuf; use super::{CONFIG_FILE_NAMES, WorkspaceProtoConfigs}; @@ -149,7 +161,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(); + let mut ws = WorkspaceProtoConfigs::new(vec![]); ws.add_workspace(&WorkspaceFolder { uri: Url::from_directory_path(tmpdir.path()).unwrap(), name: "Test".to_string(), @@ -183,7 +195,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(); + let mut ws = WorkspaceProtoConfigs::new(vec![]); ws.add_workspace(&WorkspaceFolder { uri: Url::from_directory_path(tmpdir.path()).unwrap(), name: "Test".to_string(), @@ -218,7 +230,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(); + let mut ws = WorkspaceProtoConfigs::new(vec![]); ws.add_workspace(&WorkspaceFolder { uri: Url::from_directory_path(tmpdir.path()).unwrap(), name: "Test".to_string(), @@ -229,4 +241,36 @@ mod test { assert!(ws.get_workspace_for_uri(&workspace).is_some()); } } + + #[test] + fn test_cli_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 CLI include paths + let cli_paths = vec![ + PathBuf::from("/path/to/protos"), + PathBuf::from("relative/path"), + ]; + let mut ws = WorkspaceProtoConfigs::new(cli_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 CLI paths are included in the result + assert!(include_paths.iter().any(|p| p.ends_with("relative/path") || + p == &PathBuf::from("/path/to/protos"))); + + // The relative path should be resolved relative to the workspace + let resolved_relative_path = tmpdir.path().join("relative/path"); + assert!(include_paths.contains(&resolved_relative_path)); + + // The absolute path should be included as is + assert!(include_paths.contains(&PathBuf::from("/path/to/protos"))); + } } diff --git a/src/main.rs b/src/main.rs index 684fb5e..871b3e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use async_lsp::concurrency::ConcurrencyLayer; use async_lsp::panic::CatchUnwindLayer; use async_lsp::server::LifecycleLayer; use async_lsp::tracing::TracingLayer; +use clap::Parser; use server::{ProtoLanguageServer, TickEvent}; use tower::ServiceBuilder; use tracing::Level; @@ -21,15 +22,40 @@ mod state; mod utils; mod workspace; +/// Language server for proto3 files +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Include paths for proto files + #[arg(short, long, value_delimiter = ',')] + include_paths: Option>, +} + #[tokio::main(flavor = "current_thread")] async fn main() { - let args: Vec = std::env::args().collect(); - if args.len() == 2 && args[1] == "--version" { - println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); - return; - } + let cli = Cli::parse(); + + let dir = std::env::temp_dir(); + eprintln!("Rolling file based logging at directory: {dir:?}"); + + let file_appender = tracing_appender::rolling::daily(dir.clone(), "protols.log"); + let file_appender = tracing_appender::non_blocking(file_appender); + + tracing_subscriber::fmt() + .with_max_level(Level::INFO) + .with_ansi(false) + .with_writer(file_appender.0) + .init(); let (server, _) = async_lsp::MainLoop::new_server(|client| { + tracing::info!("Using CLI options: {:?}", cli); + let server = ProtoLanguageServer::new_router( + client.clone(), + cli.include_paths + .map(|ic| ic.into_iter().map(std::path::PathBuf::from).collect()) + .unwrap_or_default(), + ); + tokio::spawn({ let client = client.clone(); async move { @@ -49,21 +75,9 @@ async fn main() { .layer(CatchUnwindLayer::default()) .layer(ConcurrencyLayer::default()) .layer(ClientProcessMonitorLayer::new(client.clone())) - .service(ProtoLanguageServer::new_router(client)) + .service(server) }); - let dir = std::env::temp_dir(); - eprintln!("Logs are being written to directory {:?}", dir); - - let file_appender = tracing_appender::rolling::daily(dir, "protols.log"); - let (non_blocking, _gaurd) = tracing_appender::non_blocking(file_appender); - - tracing_subscriber::fmt() - .with_max_level(Level::INFO) - .with_ansi(false) - .with_writer(non_blocking) - .init(); - // Prefer truly asynchronous piped stdin/stdout without blocking tasks. #[cfg(unix)] let (stdin, stdout) = ( @@ -79,3 +93,34 @@ async fn main() { server.run_buffered(stdin, stdout).await.unwrap(); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cli_parsing() { + // Test with no arguments + let args = vec!["protols"]; + let cli = Cli::parse_from(args); + assert!(cli.include_paths.is_none()); + + // Test with include paths + let args = vec!["protols", "--include-paths=/path1,/path2"]; + let cli = Cli::parse_from(args); + assert!(cli.include_paths.is_some()); + let paths = cli.include_paths.unwrap(); + assert_eq!(paths.len(), 2); + assert_eq!(paths[0], "/path1"); + assert_eq!(paths[1], "/path2"); + + // Test with short form + let args = vec!["protols", "-i", "/path1,/path2"]; + let cli = Cli::parse_from(args); + assert!(cli.include_paths.is_some()); + let paths = cli.include_paths.unwrap(); + assert_eq!(paths.len(), 2); + assert_eq!(paths[0], "/path1"); + assert_eq!(paths[1], "/path2"); + } +} diff --git a/src/server.rs b/src/server.rs index f7909ec..d75007a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -5,6 +5,7 @@ use async_lsp::{ }; use std::{ ops::ControlFlow, + path::PathBuf, sync::{mpsc, mpsc::Sender}, thread, }; @@ -20,12 +21,12 @@ pub struct ProtoLanguageServer { } impl ProtoLanguageServer { - pub fn new_router(client: ClientSocket) -> Router { + pub fn new_router(client: ClientSocket, cli_include_paths: Vec) -> Router { let mut router = Router::from_language_server(Self { client, counter: 0, state: ProtoLanguageState::new(), - configs: WorkspaceProtoConfigs::new(), + configs: WorkspaceProtoConfigs::new(cli_include_paths), }); router.event(Self::on_tick); router