Skip to content

feat: Read import path and produce import diagnostics #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 12, 2025
Merged
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: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@
[![Crates.io](https://img.shields.io/crates/v/protols.svg)](https://crates.io/crates/protols)
[![Build and Test](https://github.com/coder3101/protols/actions/workflows/ci.yml/badge.svg)](https://github.com/coder3101/protols/actions/workflows/ci.yml)

**WARNING** : Master branch is undergoing a massive refactoring, please use last relesed tag instead.

**Protols** is an open-source, feature-rich [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/) for **Protocol Buffers (proto)** files. Powered by the efficient [tree-sitter](https://tree-sitter.github.io/tree-sitter/) parser, Protols offers intelligent code assistance for protobuf development, including features like auto-completion, diagnostics, formatting, and more.

![Protols Demo](./assets/protols.mov)
@@ -71,7 +73,7 @@ If you're using Visual Studio Code, you can install the [Protobuf Language Suppo

## ⚙️ Configuration

Protols is configured using a `protols.toml` file, which you can place in any directory. **Protols** will search for the closest configuration file by recursively traversing the parent directories.
Protols is configured using a `protols.toml` file, which you can place in any directory.

### Sample `protols.toml`

@@ -108,10 +110,6 @@ The `[formatter]` section allows configuration for code formatting.

- `clang_format_path`: Specify the path to the `clang-format` binary.

### Multiple Configuration Files

You can place multiple `protols.toml` files across different directories. **Protols** will use the closest configuration file by searching up the directory tree.

---

## 🛠️ Usage
2 changes: 2 additions & 0 deletions protols.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[config]
include_paths = ["sample", "src/workspace/input"]
File renamed without changes.
11 changes: 1 addition & 10 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ fn default_clang_format_path() -> String {
"clang-format".to_string()
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct ProtolsConfig {
pub config: Config,
@@ -34,15 +34,6 @@ pub struct ExperimentalConfig {
pub use_protoc_diagnostics: bool,
}

impl Default for ProtolsConfig {
fn default() -> Self {
Self {
config: Config::default(),
formatter: FormatterConfig::default(),
}
}
}

impl Default for FormatterConfig {
fn default() -> Self {
Self {
17 changes: 16 additions & 1 deletion src/config/workspace.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
collections::{HashMap, HashSet},
path::Path,
path::{Path, PathBuf},
};

use async_lsp::lsp_types::{Url, WorkspaceFolder};
@@ -61,6 +61,21 @@ impl WorkspaceProtoConfigs {
.iter()
.find(|&k| upath.starts_with(k.to_file_path().unwrap()))
}

pub fn get_include_paths(&self, uri: &Url) -> Option<Vec<PathBuf>> {
let c = self.get_config_for_uri(uri)?;
let w = self.get_workspace_for_uri(uri)?.to_file_path().ok()?;
let mut ipath: Vec<PathBuf> = c
.config
.include_paths
.iter()
.map(PathBuf::from)
.map(|p| if p.is_relative() { w.join(p) } else { p })
.collect();

ipath.push(w.to_path_buf());
Some(ipath)
}
}

#[cfg(test)]
31 changes: 19 additions & 12 deletions src/lsp.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
use std::ops::ControlFlow;
use std::sync::mpsc;
use std::thread;
use std::{collections::HashMap, fs::read_to_string};
use tracing::{error, info};

@@ -12,11 +10,12 @@ use async_lsp::lsp_types::{
DocumentSymbolParams, DocumentSymbolResponse, FileOperationFilter, FileOperationPattern,
FileOperationPatternKind, FileOperationRegistrationOptions, GotoDefinitionParams,
GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability,
InitializeParams, InitializeResult, Location, OneOf, PrepareRenameResponse, ProgressParams,
ReferenceParams, RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities,
ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind,
TextEdit, Url, WorkspaceEdit, WorkspaceFileOperationsServerCapabilities,
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
InitializeParams, InitializeResult, Location, OneOf, PrepareRenameResponse,
ReferenceParams, RenameFilesParams, RenameOptions, RenameParams,
ServerCapabilities, ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability,
TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit,
WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
WorkspaceServerCapabilities,
};
use async_lsp::{LanguageClient, LanguageServer, ResponseError};
use futures::future::BoxFuture;
@@ -377,15 +376,19 @@ impl LanguageServer for ProtoLanguageServer {
let uri = params.text_document.uri;
let content = params.text_document.text;

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

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

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

if !pconf.config.disable_parse_diagnostics {
if let Err(e) = self.client.publish_diagnostics(diagnostics) {
error!(error=%e, "failed to publish diagnostics")
}
@@ -397,7 +400,11 @@ impl LanguageServer for ProtoLanguageServer {
let uri = params.text_document.uri;
let content = params.content_changes[0].text.clone();

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

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

@@ -419,7 +426,7 @@ impl LanguageServer for ProtoLanguageServer {
if let Ok(uri) = Url::from_file_path(&file.uri) {
// Safety: The uri is always a file type
let content = read_to_string(uri.to_file_path().unwrap()).unwrap_or_default();
self.state.upsert_content(&uri, content);
self.state.upsert_content(&uri, content, &[]);
} else {
error!(uri=%file.uri, "failed parse uri");
}
35 changes: 23 additions & 12 deletions src/parser/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams, Range};
use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Range};

use crate::{nodekind::NodeKind, utils::ts_to_lsp_position};

use super::ParsedTree;

impl ParsedTree {
pub fn collect_parse_errors(&self) -> PublishDiagnosticsParams {
let diagnostics = self
.find_all_nodes(NodeKind::is_error)
pub fn collect_parse_diagnostics(&self) -> Vec<Diagnostic> {
self.find_all_nodes(NodeKind::is_error)
.into_iter()
.map(|n| Diagnostic {
range: Range {
@@ -19,12 +18,24 @@ impl ParsedTree {
message: "Syntax error".to_string(),
..Default::default()
})
.collect();
PublishDiagnosticsParams {
uri: self.uri.clone(),
diagnostics,
version: None,
}
.collect()
}

pub fn collect_import_diagnostics(
&self,
content: &[u8],
import: Vec<String>,
) -> Vec<Diagnostic> {
self.get_import_path_range(content, import)
.into_iter()
.map(|r| Diagnostic {
range: r,
severity: Some(DiagnosticSeverity::ERROR),
source: Some(String::from("protols")),
message: "failed to find proto file".to_string(),
..Default::default()
})
.collect()
}
}

@@ -42,12 +53,12 @@ mod test {

let parsed = ProtoParser::new().parse(url.clone(), contents);
assert!(parsed.is_some());
assert_yaml_snapshot!(parsed.unwrap().collect_parse_errors());
assert_yaml_snapshot!(parsed.unwrap().collect_parse_diagnostics());

let contents = include_str!("input/test_collect_parse_error2.proto");

let parsed = ProtoParser::new().parse(url.clone(), contents);
assert!(parsed.is_some());
assert_yaml_snapshot!(parsed.unwrap().collect_parse_errors());
assert_yaml_snapshot!(parsed.unwrap().collect_parse_diagnostics());
}
}
2 changes: 1 addition & 1 deletion src/parser/docsymbol.rs
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ impl DocumentSymbolTreeBuilder {
}

pub(super) fn maybe_pop(&mut self, node: usize) {
let should_pop = self.stack.last().map_or(false, |(n, _)| *n == node);
let should_pop = self.stack.last().is_some_and(|(n, _)| *n == node);
if should_pop {
let (_, explored) = self.stack.pop().unwrap();
if let Some((_, parent)) = self.stack.last_mut() {
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
---
source: src/parser/diagnostics.rs
expression: parsed.unwrap().collect_parse_errors()
expression: parsed.unwrap().collect_parse_diagnostics()
snapshot_kind: text
---
uri: "file://foo/bar.proto"
diagnostics:
- range:
start:
line: 6
character: 8
end:
line: 6
character: 19
severity: 1
source: protols
message: Syntax error
- range:
start:
line: 6
character: 8
end:
line: 6
character: 19
severity: 1
source: protols
message: Syntax error
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: src/parser/diagnostics.rs
expression: parsed.unwrap().collect_parse_errors()
expression: parsed.unwrap().collect_parse_diagnostics()
snapshot_kind: text
---
uri: "file://foo/bar.proto"
diagnostics: []
[]
44 changes: 35 additions & 9 deletions src/parser/tree.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use async_lsp::lsp_types::Position;
use async_lsp::lsp_types::{Position, Range};
use tree_sitter::{Node, TreeCursor};

use crate::{nodekind::NodeKind, utils::lsp_to_ts_point};
use crate::{
nodekind::NodeKind,
utils::{lsp_to_ts_point, ts_to_lsp_position},
};

use super::ParsedTree;

@@ -133,15 +136,38 @@ impl ParsedTree {
.first()
.map(|n| n.utf8_text(content).expect("utf-8 parse error"))
}
pub fn get_import_path<'a>(&self, content: &'a [u8]) -> Vec<&'a str> {

pub fn get_import_node(&self) -> Vec<Node> {
self.find_all_nodes(NodeKind::is_import_path)
.into_iter()
.filter_map(|n| {
n.child_by_field_name("path").map(|c| {
c.utf8_text(content)
.expect("utf-8 parse error")
.trim_matches('"')
})
.filter_map(|n| n.child_by_field_name("path"))
.collect()
}

pub fn get_import_path<'a>(&self, content: &'a [u8]) -> Vec<&'a str> {
self.get_import_node()
.into_iter()
.map(|n| {
n.utf8_text(content)
.expect("utf-8 parse error")
.trim_matches('"')
})
.collect()
}

pub fn get_import_path_range(&self, content: &[u8], import: Vec<String>) -> Vec<Range> {
self.get_import_node()
.into_iter()
.filter(|n| {
let t = n
.utf8_text(content)
.expect("utf8-parse error")
.trim_matches('"');
import.iter().any(|i| i == t)
})
.map(|n| Range {
start: ts_to_lsp_position(&n.start_position()),
end: ts_to_lsp_position(&n.end_position()),
})
.collect()
}
1 change: 1 addition & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -36,6 +36,7 @@ impl ProtoLanguageServer {
ControlFlow::Continue(())
}

#[allow(unused)]
fn with_report_progress(&self, token: NumberOrString) -> Sender<ProgressParamsValue> {
let (tx, rx) = mpsc::channel();
let mut socket = self.client.clone();
Loading