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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ Protols is configured using a `protols.toml` file, which you can place in any di
include_paths = ["foobar", "bazbaaz"] # Include paths to look for protofiles during parsing
disable_parse_diagnostics = true # Disable diagnostics for parsing

[config.experimental] # Experimental configuration; this should be considered unsafe and not fully tested
use_protoc_diagnostics = true # Use diagnostics from protoc
[config.experimental] # experimental configuration; this should be considered unsafe and not fully tested
use_protoc_diagnostics = true # use diagnostics from protoc
protoc_path = "protoc" # Path to proto compiler (protoc)

[formatter] # Formatter specific configuration
clang_format_path = "/usr/bin/clang-format" # clang-format binary to execute in formatting
Expand All @@ -96,13 +97,14 @@ clang_format_path = "/usr/bin/clang-format" # clang-format binary to execute in
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.
- `disable_parse_diagnostics`: Set to `true` to disable diagnostics during parsing.
- `disable_parse_diagnostics`: Set to `true` to disable tree-sitter parse diagnostics during parsing.

#### Experimental Configuration

The `[config.experimental]` section contains settings that are in development or not fully tested.

- `use_protoc_diagnostics`: Enable diagnostics from the `protoc` compiler when set to `true`.
- `protoc_path`: Uses protoc from this path for diagnostics

#### Formatter Configuration

Expand Down
3 changes: 3 additions & 0 deletions protols.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[config]
include_paths = ["src/workspace/input"]

[config.experimental]
use_protoc_diagnostics = true
11 changes: 7 additions & 4 deletions sample/simple.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ syntax = "proto3";

package com.book;

import "google/protobuf/any.proto";

// This is a book represeted by some comments that we like to address in the
// review
message Book {
Expand All @@ -24,6 +26,7 @@ message Book {
}

enum BookState {
UNSPECIFIED = 0;
HARD_COVER = 1;
SOFT_COVER = 2;
}
Expand Down Expand Up @@ -52,10 +55,10 @@ service BookService {

message BookStore {
reserved 1;
Book book = 0;
string name = 1;
map<int64, string> books = 2;
EnumSample sample = 3;
Book book = 5;
string name = 2;
map<int64, string> books = 3;
EnumSample sample = 4;
}

// These are enum options representing some operation in the proto
Expand Down
16 changes: 15 additions & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ fn default_clang_format_path() -> String {
"clang-format".to_string()
}

fn default_protoc_path() -> String {
"protoc".to_string()
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct ProtolsConfig {
Expand All @@ -28,10 +32,11 @@ pub struct Config {
pub experimental: ExperimentalConfig,
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(default)]
pub struct ExperimentalConfig {
pub use_protoc_diagnostics: bool,
pub protoc_path: String,
}

impl Default for FormatterConfig {
Expand All @@ -41,3 +46,12 @@ impl Default for FormatterConfig {
}
}
}

impl Default for ExperimentalConfig {
fn default() -> Self {
Self {
protoc_path: default_protoc_path(),
use_protoc_diagnostics: false,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ config:
disable_parse_diagnostics: false
experimental:
use_protoc_diagnostics: false
protoc_path: protoc
formatter:
clang_format_path: clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ config:
disable_parse_diagnostics: true
experimental:
use_protoc_diagnostics: true
protoc_path: protoc
formatter:
clang_format_path: /usr/bin/clang-format
19 changes: 11 additions & 8 deletions src/config/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ pub struct WorkspaceProtoConfigs {

impl WorkspaceProtoConfigs {
pub fn new() -> Self {
// Try to find protobuf library and get its include paths
let protoc_include_prefix = Config::new()
.atleast_version("3.0.0")
.probe("protobuf")
.map(|lib| lib.include_paths)
.unwrap_or_default();

Self {
workspaces: Default::default(),
formatters: Default::default(),
protoc_include_prefix: Config::new()
.atleast_version("3.0.0")
.probe("protobuf")
.map(|l| l.include_paths)
.unwrap_or_default(),
configs: Default::default(),
workspaces: HashSet::new(),
formatters: HashMap::new(),
configs: HashMap::new(),
protoc_include_prefix,
}
}

Expand Down
42 changes: 29 additions & 13 deletions src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,26 @@ impl LanguageServer for ProtoLanguageServer {
Box::pin(async move { Ok(response) })
}

fn did_save(&mut self, _: DidSaveTextDocumentParams) -> Self::NotifyResult {
fn did_save(&mut self, params: DidSaveTextDocumentParams) -> Self::NotifyResult {
let uri = params.text_document.uri;
let content = self.state.get_content(&uri);

let Some(ipath) = self.configs.get_include_paths(&uri) else {
return ControlFlow::Continue(());
};

let Some(pconf) = self.configs.get_config_for_uri(&uri) else {
return ControlFlow::Continue(());
};

if let Some(diagnostics) = self
.state
.upsert_file(&uri, content, &ipath, 8, &pconf.config)
{
if let Err(e) = self.client.publish_diagnostics(diagnostics) {
error!(error=%e, "failed to publish diagnostics")
}
}
ControlFlow::Continue(())
}

Expand All @@ -408,15 +427,14 @@ impl LanguageServer for ProtoLanguageServer {
return ControlFlow::Continue(());
};

let Some(diagnostics) = self.state.upsert_file(&uri, content.clone(), &ipath, 8) else {
return ControlFlow::Continue(());
};

let Some(pconf) = self.configs.get_config_for_uri(&uri) else {
return ControlFlow::Continue(());
};

if !pconf.config.disable_parse_diagnostics {
if let Some(diagnostics) = self
.state
.upsert_file(&uri, content, &ipath, 8, &pconf.config)
{
if let Err(e) = self.client.publish_diagnostics(diagnostics) {
error!(error=%e, "failed to publish diagnostics")
}
Expand All @@ -432,20 +450,18 @@ impl LanguageServer for ProtoLanguageServer {
return ControlFlow::Continue(());
};

let Some(diagnostics) = self.state.upsert_file(&uri, content, &ipath, 2) else {
return ControlFlow::Continue(());
};

let Some(ws) = self.configs.get_config_for_uri(&uri) else {
let Some(pconf) = self.configs.get_config_for_uri(&uri) else {
return ControlFlow::Continue(());
};

if !ws.config.disable_parse_diagnostics {
// override config to disable protoc diagnostics during change
let mut pconf = pconf.config.clone();
pconf.experimental.use_protoc_diagnostics = false;
if let Some(diagnostics) = self.state.upsert_file(&uri, content, &ipath, 8, &pconf) {
if let Err(e) = self.client.publish_diagnostics(diagnostics) {
error!(error=%e, "failed to publish diagnostics")
}
}

ControlFlow::Continue(())
}

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod formatter;
mod lsp;
mod nodekind;
mod parser;
mod protoc;
mod server;
mod state;
mod utils;
Expand Down
85 changes: 85 additions & 0 deletions src/protoc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use crate::utils::ts_to_lsp_position;
use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Range};
use std::process::Command;
use tree_sitter::Point;

pub struct ProtocDiagnostics {}

impl ProtocDiagnostics {
pub fn new() -> Self {
Self {}
}

pub fn collect_diagnostics(
&self,
protoc_path: &str,
file_path: &str,
include_paths: &[String],
) -> Vec<Diagnostic> {
let mut cmd = Command::new(protoc_path);

// Add include paths
for path in include_paths {
cmd.arg("-I").arg(path);
}

// Generate descriptor but discard its output
cmd.arg("-o")
.arg(if cfg!(windows) { "NUL" } else { "/dev/null" });

// Add the file to check
cmd.arg(file_path);

// Run protoc and capture output
match cmd.output() {
Ok(output) => {
if !output.status.success() {
let error = String::from_utf8_lossy(&output.stderr);
self.parse_protoc_output(&error)
} else {
Vec::new()
}
}
Err(e) => {
tracing::error!(error=%e, "failed to run protoc");
Vec::new()
}
}
}

fn parse_protoc_output(&self, output: &str) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();

for line in output.lines() {
// Parse protoc error format: file:line:column: message
if let Some((file_info, message)) = line.split_once(": ") {
let parts: Vec<&str> = file_info.split(':').collect();
if parts.len() >= 3 {
if let (Ok(line), Ok(col)) = (parts[1].parse::<u32>(), parts[2].parse::<u32>())
{
let point = Point {
row: (line - 1) as usize,
column: (col - 1) as usize,
};
let diagnostic = Diagnostic {
range: Range {
start: ts_to_lsp_position(&point),
end: ts_to_lsp_position(&Point {
row: point.row,
column: point.column + 1,
}),
},
severity: Some(DiagnosticSeverity::ERROR),
source: Some("protoc".to_string()),
message: message.to_string(),
..Default::default()
};
diagnostics.push(diagnostic);
}
}
}
}

diagnostics
}
}
32 changes: 29 additions & 3 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ use tree_sitter::Node;
use walkdir::WalkDir;

use crate::{
config::Config,
nodekind::NodeKind,
parser::{ParsedTree, ProtoParser},
};

use crate::protoc::ProtocDiagnostics;

pub struct ProtoLanguageState {
documents: Arc<RwLock<HashMap<Url, String>>>,
trees: Arc<RwLock<HashMap<Url, ParsedTree>>>,
parser: Arc<Mutex<ProtoParser>>,
parsed_workspaces: Arc<RwLock<HashSet<String>>>,
protoc_diagnostics: Arc<Mutex<ProtocDiagnostics>>,
}

impl ProtoLanguageState {
Expand All @@ -30,6 +34,7 @@ impl ProtoLanguageState {
trees: Default::default(),
parser: Arc::new(Mutex::new(ProtoParser::new())),
parsed_workspaces: Arc::new(RwLock::new(HashSet::new())),
protoc_diagnostics: Arc::new(Mutex::new(ProtocDiagnostics::new())),
}
}

Expand Down Expand Up @@ -217,13 +222,34 @@ impl ProtoLanguageState {
content: String,
ipath: &[PathBuf],
depth: usize,
config: &Config,
) -> Option<PublishDiagnosticsParams> {
info!(%uri, %depth, "upserting file");
let diag = self.upsert_content(uri, content.clone(), ipath, depth);
self.get_tree(uri).map(|tree| {
let diag = tree.collect_import_diagnostics(content.as_ref(), diag);
let mut d = tree.collect_parse_diagnostics();
d.extend(diag);
let mut d = vec![];
if !config.disable_parse_diagnostics {
d.extend(tree.collect_parse_diagnostics());
}
d.extend(tree.collect_import_diagnostics(content.as_ref(), diag));

// Add protoc diagnostics if enabled
if config.experimental.use_protoc_diagnostics {
if let Ok(protoc_diagnostics) = self.protoc_diagnostics.lock() {
if let Ok(file_path) = uri.to_file_path() {
let protoc_diags = protoc_diagnostics.collect_diagnostics(
&config.experimental.protoc_path,
file_path.to_str().unwrap_or_default(),
&ipath
.iter()
.map(|p| p.to_str().unwrap_or_default().to_string())
.collect::<Vec<_>>(),
);
d.extend(protoc_diags);
}
}
}

PublishDiagnosticsParams {
uri: tree.uri.clone(),
diagnostics: d,
Expand Down
Loading