Skip to content

Commit

Permalink
feat: Read import path and produce import diagnostics (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
coder3101 authored Jan 12, 2025
1 parent f5cda97 commit a0f6e51
Show file tree
Hide file tree
Showing 16 changed files with 257 additions and 170 deletions.
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -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
Expand Down
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
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
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};
Expand Down Expand Up @@ -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)]
Expand Down
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};

Expand All @@ -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;
Expand Down Expand Up @@ -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")
}
Expand All @@ -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(());
};

Expand All @@ -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");
}
Expand Down
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 {
Expand All @@ -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()
}
}

Expand All @@ -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
Expand Up @@ -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() {
Expand Down
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;

Expand Down Expand Up @@ -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()
}
Expand Down
1 change: 1 addition & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading

0 comments on commit a0f6e51

Please sign in to comment.