├── .gitignore
├── .github
├── FUNDING.yml
├── workflows
│ ├── ci.yml
│ └── release.yml
└── copilot-instructions.md
├── rust-toolchain.toml
├── src
├── context
│ ├── mod.rs
│ ├── jumpable.rs
│ └── hoverable.rs
├── workspace
│ ├── mod.rs
│ ├── input
│ │ ├── inner
│ │ │ ├── secret
│ │ │ │ └── y.proto
│ │ │ └── x.proto
│ │ ├── c.proto
│ │ ├── b.proto
│ │ └── a.proto
│ ├── snapshots
│ │ ├── protols__workspace__hover__test__workspace_test_hover-6.snap
│ │ ├── protols__workspace__hover__test__workspace_test_hover-2.snap
│ │ ├── protols__workspace__hover__test__workspace_test_hover-5.snap
│ │ ├── protols__workspace__hover__test__workspace_test_hover-4.snap
│ │ ├── protols__workspace__definition__test__workspace_test_definition-5.snap
│ │ ├── protols__workspace__definition__test__workspace_test_definition-4.snap
│ │ ├── protols__workspace__definition__test__workspace_test_definition.snap
│ │ ├── protols__workspace__hover__test__workspace_test_hover-7.snap
│ │ ├── protols__workspace__hover__test__workspace_test_hover-9.snap
│ │ ├── protols__workspace__definition__test__workspace_test_definition-2.snap
│ │ ├── protols__workspace__rename__test__reference-3.snap
│ │ ├── protols__workspace__definition__test__workspace_test_definition-3.snap
│ │ ├── protols__workspace__hover__test__workspace_test_hover-8.snap
│ │ ├── protols__workspace__rename__test__reference.snap
│ │ ├── protols__workspace__rename__test__reference-2.snap
│ │ ├── protols__workspace__workspace_symbol__test__workspace_symbols-2.snap
│ │ ├── protols__workspace__rename__test__rename-2.snap
│ │ ├── protols__workspace__rename__test__rename-3.snap
│ │ ├── protols__workspace__workspace_symbol__test__workspace_symbols-3.snap
│ │ ├── protols__workspace__hover__test__workspace_test_hover-3.snap
│ │ ├── protols__workspace__rename__test__rename.snap
│ │ ├── protols__workspace__workspace_symbol__test__author_symbols.snap
│ │ ├── protols__workspace__workspace_symbol__test__address_symbols.snap
│ │ ├── protols__workspace__hover__test__workspace_test_hover.snap
│ │ ├── protols__workspace__workspace_symbol__test__all_symbols.snap
│ │ └── protols__workspace__workspace_symbol__test__workspace_symbols.snap
│ ├── workspace_symbol.rs
│ ├── definition.rs
│ ├── hover.rs
│ └── rename.rs
├── parser
│ ├── snapshots
│ │ ├── protols__parser__hover__test__hover-2.snap
│ │ ├── protols__parser__hover__test__hover.snap
│ │ ├── protols__parser__tree__test__filter-2.snap
│ │ ├── protols__parser__tree__test__filter.snap
│ │ ├── protols__parser__rename__test__reference-3.snap
│ │ ├── protols__parser__rename__test__can_rename-2.snap
│ │ ├── protols__parser__rename__test__can_rename-3.snap
│ │ ├── protols__parser__rename__test__can_rename-4.snap
│ │ ├── protols__parser__rename__test__rename-3.snap
│ │ ├── protols__parser__tree__test__filter-3.snap
│ │ ├── protols__parser__hover__test__hover-4.snap
│ │ ├── protols__parser__definition__test__goto_definition-2.snap
│ │ ├── protols__parser__rename__test__rename_fields-3.snap
│ │ ├── protols__parser__diagnostics__test__collect_parse_error.snap
│ │ ├── protols__parser__hover__test__hover-3.snap
│ │ ├── protols__parser__rename__test__can_rename.snap
│ │ ├── protols__parser__hover__test__hover-5.snap
│ │ ├── protols__parser__definition__test__goto_definition.snap
│ │ ├── protols__parser__rename__test__rename_fields-2.snap
│ │ ├── protols__parser__diagnostics__test__collect_parse_error-2.snap
│ │ ├── protols__parser__rename__test__reference.snap
│ │ ├── protols__parser__rename__test__rename_fields.snap
│ │ ├── protols__parser__rename__test__rename-2.snap
│ │ ├── protols__parser__rename__test__reference-2.snap
│ │ ├── protols__parser__rename__test__rename.snap
│ │ └── protols__parser__docsymbol__test__document_symbols.snap
│ ├── input
│ │ ├── test_collect_parse_error1.proto
│ │ ├── test_collect_parse_error2.proto
│ │ ├── test_goto_definition.proto
│ │ ├── test_filter.proto
│ │ ├── test_can_rename.proto
│ │ ├── test_document_symbols.proto
│ │ ├── test_hover.proto
│ │ ├── test_reference.proto
│ │ └── test_rename.proto
│ ├── mod.rs
│ ├── diagnostics.rs
│ ├── definition.rs
│ ├── hover.rs
│ ├── docsymbol.rs
│ ├── tree.rs
│ └── rename.rs
├── docs
│ ├── builtin
│ │ ├── double.md
│ │ ├── float.md
│ │ ├── bytes.md
│ │ ├── fixed32.md
│ │ ├── sint32.md
│ │ ├── uint32.md
│ │ ├── bool.md
│ │ ├── sfixed32.md
│ │ ├── fixed64.md
│ │ ├── uint64.md
│ │ ├── sfixed64.md
│ │ ├── sint64.md
│ │ ├── string.md
│ │ ├── int32.md
│ │ ├── int64.md
│ │ └── default.md
│ └── wellknown
│ │ ├── Empty.md
│ │ ├── BytesValue.md
│ │ ├── Int64Value.md
│ │ ├── BoolValue.md
│ │ ├── FieldMask.md
│ │ ├── FloatValue.md
│ │ ├── Int32Value.md
│ │ ├── DoubleValue.md
│ │ ├── StringValue.md
│ │ ├── UInt32Value.md
│ │ ├── UInt64Value.md
│ │ ├── Syntax.md
│ │ ├── Struct.md
│ │ ├── EnumValue.md
│ │ ├── NullValue.md
│ │ ├── ListValue.md
│ │ ├── Mixin.md
│ │ ├── Option.md
│ │ ├── SourceContext.md
│ │ ├── Field.Cardinality.md
│ │ ├── Enum.md
│ │ ├── Value.md
│ │ ├── Any.md
│ │ ├── Type.md
│ │ ├── Method.md
│ │ ├── Api.md
│ │ ├── Timestamp.md
│ │ ├── Field.md
│ │ ├── Duration.md
│ │ └── Field.Kind.md
├── formatter
│ ├── input
│ │ ├── empty.xml
│ │ ├── test.proto
│ │ └── replacement.xml
│ ├── snapshots
│ │ ├── protols__formatter__clang__test__reading_empty_xml.snap
│ │ ├── protols__formatter__clang__test__offset_to_position.snap
│ │ ├── protols__formatter__clang__test__reading_xml.snap
│ │ ├── protols__formatter__clang__test__offset_to_position-4.snap
│ │ ├── protols__formatter__clang__test__offset_to_position-2.snap
│ │ └── protols__formatter__clang__test__offset_to_position-3.snap
│ ├── mod.rs
│ └── clang.rs
├── config
│ ├── input
│ │ └── protols-valid.toml
│ ├── snapshots
│ │ ├── protols__config__workspace__test__get_for_workspace-2.snap
│ │ ├── protols__config__workspace__test__get_for_workspace.snap
│ │ └── protols__config__workspace__test__workspace.snap
│ ├── mod.rs
│ └── workspace.rs
├── docs.rs
├── nodekind.rs
├── utils.rs
├── protoc.rs
├── server.rs
├── main.rs
└── state.rs
├── protols.toml
├── assets
└── protols.mov
├── .clang-format
├── sample
├── test.proto
├── everything.proto
└── simple.proto
├── Cargo.toml
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /logs
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [coder3101]
2 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "stable"
3 |
--------------------------------------------------------------------------------
/src/context/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod hoverable;
2 | pub mod jumpable;
3 |
--------------------------------------------------------------------------------
/protols.toml:
--------------------------------------------------------------------------------
1 | [config]
2 | include_paths = ["src/workspace/input"]
3 |
--------------------------------------------------------------------------------
/assets/protols.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coder3101/protols/HEAD/assets/protols.mov
--------------------------------------------------------------------------------
/src/workspace/mod.rs:
--------------------------------------------------------------------------------
1 | mod definition;
2 | mod hover;
3 | mod rename;
4 | mod workspace_symbol;
5 |
--------------------------------------------------------------------------------
/.clang-format:
--------------------------------------------------------------------------------
1 | BasedOnStyle: Google
2 | DerivePointerAlignment: false
3 | PointerAlignment: Left
4 |
--------------------------------------------------------------------------------
/src/context/jumpable.rs:
--------------------------------------------------------------------------------
1 | pub enum Jumpable {
2 | Import(String),
3 | Identifier(String),
4 | }
5 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__hover__test__hover-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/hover.rs
3 | expression: res
4 | ---
5 | []
6 |
--------------------------------------------------------------------------------
/src/context/hoverable.rs:
--------------------------------------------------------------------------------
1 | pub enum Hoverables {
2 | FieldType(String),
3 | ImportPath(String),
4 | Identifier(String),
5 | }
6 |
--------------------------------------------------------------------------------
/src/docs/builtin/double.md:
--------------------------------------------------------------------------------
1 | *double* builtin type,
2 |
3 | ---
4 | A double-precision floating point number (IEEE-745.2008 binary64).
5 |
6 |
--------------------------------------------------------------------------------
/src/docs/builtin/float.md:
--------------------------------------------------------------------------------
1 | *float* builtin type
2 |
3 | ---
4 | A single-precision floating point number (IEEE-745.2008 binary32).
5 |
6 |
--------------------------------------------------------------------------------
/src/formatter/input/empty.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__hover__test__hover.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/hover.rs
3 | expression: res
4 | ---
5 | - A Book is book
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__tree__test__filter-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/tree.rs
3 | expression: package_name
4 | ---
5 | com.parser
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__tree__test__filter.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/tree.rs
3 | expression: names
4 | ---
5 | - Book
6 | - Author
7 |
--------------------------------------------------------------------------------
/src/docs/builtin/bytes.md:
--------------------------------------------------------------------------------
1 | *bytes* builtin type, A blob of arbitrary bytes.
2 |
3 | ---
4 | Stores at most 4GB of binary data. Encoded as base64 in JSON.
5 |
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__reference-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: reference_fn(&pos_non_ref)
4 | ---
5 | []
6 |
--------------------------------------------------------------------------------
/src/formatter/snapshots/protols__formatter__clang__test__reading_empty_xml.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/formatter/clang.rs
3 | expression: r
4 | ---
5 | replacements: []
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__can_rename-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: tree.can_rename(&pos_non_rename)
4 | ---
5 | ~
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__can_rename-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: tree.can_rename(&pos_inner_type)
4 | ---
5 | ~
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__can_rename-4.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: tree.can_rename(&pos_outer_type)
4 | ---
5 | ~
6 |
--------------------------------------------------------------------------------
/src/docs/builtin/fixed32.md:
--------------------------------------------------------------------------------
1 | *fixed32* builtin type, A 32-bit unsigned integer (4-byte encoding)
2 |
3 | ---
4 | Values of this type range between `0` and `4294967295`.
5 |
6 |
--------------------------------------------------------------------------------
/src/docs/builtin/sint32.md:
--------------------------------------------------------------------------------
1 | *sint32* builtin type, A 32-bit integer (ZigZag encoding)
2 |
3 | ---
4 | Values of this type range between `-2147483648` and `2147483647`.
5 |
6 |
--------------------------------------------------------------------------------
/src/docs/builtin/uint32.md:
--------------------------------------------------------------------------------
1 | *uint32* builtin type, A 32-bit unsigned integer (varint encoding)
2 |
3 | ---
4 | Values of this type range between `0` and `4294967295`.
5 |
6 |
--------------------------------------------------------------------------------
/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/formatter/clang.rs
3 | expression: r
4 | ---
5 | line: 0
6 | character: 0
7 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__rename-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: "rename_fn(\"xyx\", &pos_non_rename)"
4 | ---
5 | []
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__tree__test__filter-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/tree.rs
3 | expression: imports
4 | ---
5 | - foo/bar.proto
6 | - baz/bar.proto
7 |
--------------------------------------------------------------------------------
/src/docs/builtin/bool.md:
--------------------------------------------------------------------------------
1 | *bool* builtin type, A Boolean value: `true` or `false`.
2 |
3 | ---
4 | Encoded as a single byte: `0x00` or `0xff` (all non-zero bytes decode to `true`)
5 |
--------------------------------------------------------------------------------
/src/docs/builtin/sfixed32.md:
--------------------------------------------------------------------------------
1 | *sfixed32* builtin type, A 32-bit integer (4-byte encoding)
2 |
3 | ---
4 | Values of this type range between `-2147483648` and `2147483647`.
5 |
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__hover__test__hover-4.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/hover.rs
3 | expression: res
4 | ---
5 | - Author of a comic is different from others
6 |
--------------------------------------------------------------------------------
/src/config/input/protols-valid.toml:
--------------------------------------------------------------------------------
1 | [config]
2 | include_paths = ["foobar", "bazbaaz"]
3 |
4 | [config.path]
5 | protoc = "/usr/bin/protoc"
6 | clang_format = "/usr/bin/clang-format"
7 |
--------------------------------------------------------------------------------
/src/docs/builtin/fixed64.md:
--------------------------------------------------------------------------------
1 | *fixed64* builtin type, A 64-bit unsigned integer (8-byte encoding)
2 |
3 | ---
4 | Values of this type range between `0` and `18446744073709551615`.
5 |
6 |
--------------------------------------------------------------------------------
/src/docs/builtin/uint64.md:
--------------------------------------------------------------------------------
1 | *uint64* builtin type, A 64-bit unsigned integer (varint encoding)
2 |
3 | ---
4 | Values of this type range between `0` and `18446744073709551615`.
5 |
6 |
--------------------------------------------------------------------------------
/src/docs/builtin/sfixed64.md:
--------------------------------------------------------------------------------
1 | *sfixed64* builtin type, A 64-bit integer (8-byte encoding)
2 |
3 | ---
4 | Values of this type range between `-9223372036854775808` and `9223372036854775807`.
5 |
--------------------------------------------------------------------------------
/src/docs/builtin/sint64.md:
--------------------------------------------------------------------------------
1 | *sint64* builtin type, A 64-bit integer (ZigZag encoding)
2 |
3 | ---
4 | Values of this type range between `-9223372036854775808` and `9223372036854775807`.
5 |
6 |
--------------------------------------------------------------------------------
/src/docs/builtin/string.md:
--------------------------------------------------------------------------------
1 | *string* builtin type, A string of text.
2 |
3 | ---
4 | Stores at most 4GB of text. Intended to be UTF-8 encoded Unicode; use `bytes` if you need other encodings.
5 |
--------------------------------------------------------------------------------
/src/parser/input/test_collect_parse_error1.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.parser;
4 |
5 | message Foo {
6 | reserved 1;
7 | reserved "baz";
8 | int bar = 2;
9 | }
10 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__definition__test__goto_definition-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/definition.rs
3 | expression: "tree.definition(&posinvalid, contents)"
4 | ---
5 | []
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__rename_fields-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: "tree.rename_fields(\"xyz.abc\", \"Doesn't matter\", contents)"
4 | ---
5 | []
6 |
--------------------------------------------------------------------------------
/src/workspace/input/inner/secret/y.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.inner.secret;
4 |
5 | // SomeSecret is a real secret with hidden string
6 | message SomeSecret {
7 | string secret = 1;
8 | }
9 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/diagnostics.rs
3 | expression: parsed.unwrap().collect_parse_diagnostics()
4 | snapshot_kind: text
5 | ---
6 | []
7 |
--------------------------------------------------------------------------------
/src/parser/input/test_collect_parse_error2.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.parser;
4 |
5 | message Book {
6 | message Author {
7 | string name;
8 | string country = 2;
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/docs/builtin/int32.md:
--------------------------------------------------------------------------------
1 | *int32* builtin type, A 32-bit integer (varint encoding)
2 |
3 | ---
4 | Values of this type range between `-2147483648` and `2147483647`.
5 | Beware that negative values are encoded as five bytes on the wire!
6 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Empty.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Empty* well known type
2 | ---
3 | A generic empty message that you can re-use to avoid defining duplicated empty messages in your APIs.
4 | The JSON representation for Empty is empty JSON object `{}`
--------------------------------------------------------------------------------
/src/docs/builtin/int64.md:
--------------------------------------------------------------------------------
1 | *int64* builtin type, A 64-bit integer (varint encoding)
2 |
3 | ---
4 | Values of this type range between `-9223372036854775808` and `9223372036854775807`.
5 | Beware that negative values are encoded as ten bytes on the wire!
6 |
--------------------------------------------------------------------------------
/src/docs/wellknown/BytesValue.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.BytesValue* well known type, Wrapper message for bytes
2 | ---
3 | The JSON representation for `BytesValue` is JSON string.
4 | ---
5 | ```proto
6 | message BytesValue {
7 | bytes value = 1;
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/Int64Value.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Int64Value* well known type, Wrapper message for `int64`
2 | ---
3 | The JSON representation for `Int64Value` is JSON string
4 | ---
5 | ```proto
6 | message Int64Value {
7 | int64 value = 1;
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/BoolValue.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.BoolValue* well known type, Wrapper message for bool
2 | ---
3 | The JSON representation for `BoolValue` is JSON `true` and `false`
4 | ---
5 | ```proto
6 | message BoolValue {
7 | bool value = 1;
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/FieldMask.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.FieldMask* well known type
2 | ---
3 | `FieldMask` represents a set of symbolic field paths
4 | ---
5 | ```proto
6 | message FieldMask {
7 | repeated string paths = 1; // The set of field mask paths.
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/FloatValue.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.FloatValue* well known type, Wrapper message for `float`
2 | ---
3 | The JSON representation for `FloatValue` is JSON number.
4 | ---
5 | ```proto
6 | message FloatValue {
7 | float value = 1;
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/Int32Value.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Int32Value* well known type, Wrapper message for `int32`
2 | ---
3 | The JSON representation for `Int32Value` is JSON number.
4 | ---
5 | ```proto
6 | message Int32Value {
7 | int32 value = 1;
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/formatter/input/test.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package foo.bar;
4 |
5 | message Box {
6 | int64 height = 1;
7 | int64 width = 2;
8 | int64 depth = 3;
9 | }
10 |
11 | service BoxAreaFinder { rpc FindArea(Box) returns (int64); }
12 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__hover__test__hover-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/hover.rs
3 | expression: res
4 | ---
5 | - "This is represents author\nA author is a someone who writes books\n\nAuthor has a name and a country where they were born"
6 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__can_rename.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: tree.can_rename(&pos_rename)
4 | ---
5 | start:
6 | line: 5
7 | character: 8
8 | end:
9 | line: 5
10 | character: 12
11 |
--------------------------------------------------------------------------------
/src/docs/wellknown/DoubleValue.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.DoubleValue* well known type, Wrapper message for `double`
2 | ---
3 | The JSON representation for `DoubleValue` is JSON number.
4 | ---
5 | ```proto
6 | message DoubleValue {
7 | double value = 1;
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/StringValue.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.StringValue* well known type, Wrapper message for `string`
2 | ---
3 | The JSON representation for `StringValue` is JSON string
4 | ---
5 | ```proto
6 | message StringValue {
7 | string value = 1;
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/UInt32Value.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.UInt32Value* well known type, Wrapper message for `uint32`
2 | ---
3 | The JSON representation for `UInt32Value` is JSON number
4 | ---
5 | ```proto
6 | message UInt32Value {
7 | uint32 value = 1;
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/UInt64Value.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.UInt64Value* well known type, Wrapper message for `uint64`
2 | ---
3 | The JSON representation for `UInt64Value` is JSON string
4 | ---
5 | ```proto
6 | message UInt64Value {
7 | uint64 value = 1;
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/formatter/input/replacement.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/workspace/input/c.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.utility;
4 |
5 | // A foobar is a dummy message
6 | message Foobar {
7 |
8 | // What is baz?
9 | message Baz {
10 | int64 b = 1;
11 | }
12 |
13 | Baz a = 2;
14 | }
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Syntax.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Syntax* well known type
2 | ---
3 | The syntax in which a protocol buffer element is defined
4 | ---
5 | ```proto
6 | enum Syntax {
7 | SYNTAX_PROTO2 = 1;
8 | SYNTAX_PROTO3 = 2;
9 | SYNTAX_EDITIONS = 3;
10 | }
11 | ```
--------------------------------------------------------------------------------
/src/formatter/snapshots/protols__formatter__clang__test__reading_xml.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/formatter/clang.rs
3 | expression: r
4 | ---
5 | replacements:
6 | - offset: 56
7 | length: 1
8 | text: "\n "
9 | - offset: 76
10 | length: 1
11 | text: "\n"
12 |
--------------------------------------------------------------------------------
/src/parser/input/test_goto_definition.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.parser;
4 |
5 | message Book {
6 | message Author {
7 | string name = 1;
8 | string country = 2;
9 | };
10 |
11 | Author author = 1;
12 | string isbn = 2;
13 | }
14 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-6.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/hover.rs
3 | expression: "state.hover(\"com.utility\", \"Baz\")"
4 | ---
5 | kind: markdown
6 | value: "`Baz` message or enum type, package: `com.utility`\n---\nWhat is baz?"
7 |
--------------------------------------------------------------------------------
/src/docs/builtin/default.md:
--------------------------------------------------------------------------------
1 | *default* builtin type, A magic option that specifies the field's default value.
2 |
3 | ---
4 |
5 | Unlike every other option on a field, this does not have a corresponding field in
6 | `google.protobuf.FieldOptions`; it is implemented by compiler magic.
7 |
8 |
--------------------------------------------------------------------------------
/src/workspace/input/b.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.workspace;
4 |
5 | // A author is a author
6 | message Author {
7 | string name = 1;
8 |
9 | // Address is a Address
10 | message Address {
11 | int64 zip = 1;
12 | }
13 |
14 | Address foo = 2;
15 | }
16 |
--------------------------------------------------------------------------------
/src/config/snapshots/protols__config__workspace__test__get_for_workspace-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/config/workspace.rs
3 | expression: ws.get_config_for_uri(&inworkspace2).unwrap()
4 | ---
5 | config:
6 | include_paths: []
7 | path:
8 | clang_format: clang-format
9 | protoc: protoc
10 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__hover__test__hover-5.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/hover.rs
3 | expression: res
4 | ---
5 | - "This is represents author\nA author is a someone who writes books\n\nAuthor has a name and a country where they were born"
6 | - Author of a comic is different from others
7 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/hover.rs
3 | expression: "state.hover(\"com.workspace\", \"Author\")"
4 | ---
5 | kind: markdown
6 | value: "`Author` message or enum type, package: `com.workspace`\n---\nA author is a author"
7 |
--------------------------------------------------------------------------------
/src/workspace/input/inner/x.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.inner;
4 |
5 | import "inner/secret/y.proto";
6 |
7 | // Why is a reason with secret
8 | message Why {
9 | string reason = 1;
10 | .com.inner.secret.SomeSecret secret = 2;
11 | secret.SomeSecret secret2 = 3;
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-5.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/hover.rs
3 | expression: "state.hover(\"com.workspace\", \"com.utility.Foobar.Baz\")"
4 | ---
5 | kind: markdown
6 | value: "`Foobar.Baz` message or enum type, package: `com.utility`\n---\nWhat is baz?"
7 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Struct.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Struct* well known type
2 | ---
3 | `Struct` represents a structured data value, consisting of fields which map to dynamically typed values.
4 | ---
5 | ```proto
6 | message Struct {
7 | map fields = 1; // Unordered map of dynamically typed values.
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-4.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/hover.rs
3 | expression: "state.hover(\"com.workspace\", \"Author.Address\")"
4 | ---
5 | kind: markdown
6 | value: "`Author.Address` message or enum type, package: `com.workspace`\n---\nAddress is a Address"
7 |
--------------------------------------------------------------------------------
/src/docs/wellknown/EnumValue.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.EnumValue* well known type
2 | ---
3 | Enum value definition
4 | ---
5 | ```proto
6 | message EnumValue {
7 | string name = 1; // Enum value name.
8 | int32 number = 2; // Enum value number.
9 | repeated Option options = 3; // Protocol buffer options.
10 | }
11 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/NullValue.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.NullValue* well known type
2 | ---
3 | `NullValue` is a singleton enumeration to represent the null value for the `Value` type union.
4 | The JSON representation for `NullValue` is JSON `null`.
5 | ---
6 | ```proto
7 | enum NullValue {
8 | NULL_VALUE = 0; // Null value.
9 | }
10 | ```
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-5.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/definition.rs
3 | expression: loc
4 | ---
5 | - uri: "file:///c.proto"
6 | range:
7 | start:
8 | line: 0
9 | character: 0
10 | end:
11 | line: 0
12 | character: 0
13 |
--------------------------------------------------------------------------------
/src/config/snapshots/protols__config__workspace__test__get_for_workspace.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/config/workspace.rs
3 | expression: ws.get_config_for_uri(&inworkspace).unwrap()
4 | ---
5 | config:
6 | include_paths:
7 | - foobar
8 | - bazbaaz
9 | path:
10 | clang_format: /usr/bin/clang-format
11 | protoc: /usr/bin/protoc
12 |
--------------------------------------------------------------------------------
/src/docs/wellknown/ListValue.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.ListValue* well known type
2 | ---
3 | `ListValue` is a wrapper around a repeated field of values.
4 | The JSON representation for `ListValue` is JSON array.
5 | ---
6 | ```proto
7 | message ListValue {
8 | repeated Value values = 1; // Repeated field of dynamically typed values.
9 | }
10 | ```
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__definition__test__goto_definition.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/definition.rs
3 | expression: "tree.definition(&posauthor, contents)"
4 | ---
5 | - uri: "file://foo/bar.proto"
6 | range:
7 | start:
8 | line: 5
9 | character: 12
10 | end:
11 | line: 5
12 | character: 18
13 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__rename_fields-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: "tree.rename_fields(\"Book.Author\", \"Writer\", contents)"
4 | ---
5 | - range:
6 | start:
7 | line: 21
8 | character: 4
9 | end:
10 | line: 21
11 | character: 15
12 | newText: Book.Writer
13 |
--------------------------------------------------------------------------------
/src/workspace/input/a.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.workspace;
4 |
5 | import "c.proto";
6 | import "b.proto";
7 | import "inner/x.proto";
8 |
9 | // A Book is a book
10 | message Book {
11 | Author author = 1;
12 | Author.Address foo = 2;
13 | com.utility.Foobar.Baz z = 3;
14 | com.inner.Why reason = 4;
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-4.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/definition.rs
3 | expression: "state.definition(\"com.utility\", \"Baz\")"
4 | ---
5 | - uri: "file://input/c.proto"
6 | range:
7 | start:
8 | line: 8
9 | character: 11
10 | end:
11 | line: 8
12 | character: 14
13 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/definition.rs
3 | expression: "state.definition(\"com.library\", \"Author\")"
4 | ---
5 | - uri: "file://input/b.proto"
6 | range:
7 | start:
8 | line: 5
9 | character: 8
10 | end:
11 | line: 5
12 | character: 14
13 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Mixin.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Mixin* well known type
2 | ---
3 | Declares an API Interface to be included in this interface.
4 | ---
5 | ```proto
6 | message Mixin {
7 | string name = 1; // The fully qualified name of the interface which is included.
8 | string root = 2; // If non-empty specifies a path under which the interface is served.
9 | }
10 | ```
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-7.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/hover.rs
3 | expression: "state.hover(&ipath, \"com.workspace\",\nHoverables::Identifier(\"com.inner.Why\".to_string()))"
4 | snapshot_kind: text
5 | ---
6 | kind: markdown
7 | value: "`Why` message or enum type, package: `com.inner`\n---\nWhy is a reason with secret"
8 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-9.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/hover.rs
3 | expression: "state.hover(&ipath, \"com.inner\",\nHoverables::Identifier(\"secret.SomeSecret\".to_string()))"
4 | ---
5 | kind: markdown
6 | value: "`SomeSecret` message or enum type, package: `secret`\n---\nSomeSecret is a real secret with hidden string"
7 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Option.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Option* well known type
2 | ---
3 | A protocol buffer option, which can be attached to a message, field, enumeration, etc.
4 | ---
5 | ```proto
6 | message Option {
7 | string name = 1; // The option's name. For example, "java_package".
8 | Any value = 2; // The option's value. For example, "com.google.protobuf".
9 | }
10 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/SourceContext.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.SourceContext* well known type
2 | ---
3 | `SourceContext` represents information about the source of a protobuf element, like the file in which it is defined.
4 | ---
5 | ```proto
6 | message SourceContext {
7 | string file_name = 1; // The path-qualified name of the .proto file that contained the associated protobuf element.
8 | }
9 | ```
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/definition.rs
3 | expression: "state.definition(\"com.library\", \"Author.Address\")"
4 | ---
5 | - uri: "file://input/b.proto"
6 | range:
7 | start:
8 | line: 9
9 | character: 11
10 | end:
11 | line: 9
12 | character: 18
13 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__rename__test__reference-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/rename.rs
3 | expression: "state.reference_fields(\"com.utility\", \"Foobar.Baz\")"
4 | snapshot_kind: text
5 | ---
6 | - uri: "file://input/a.proto"
7 | range:
8 | start:
9 | line: 11
10 | character: 3
11 | end:
12 | line: 11
13 | character: 25
14 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__definition__test__workspace_test_definition-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/definition.rs
3 | expression: "state.definition(\"com.library\", \"com.utility.Foobar.Baz\")"
4 | ---
5 | - uri: "file://input/c.proto"
6 | range:
7 | start:
8 | line: 8
9 | character: 11
10 | end:
11 | line: 8
12 | character: 14
13 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-8.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/hover.rs
3 | expression: "state.hover(&ipath, \"com.inner\",\nHoverables::Identifier(\".com.inner.secret.SomeSecret\".to_string()))"
4 | ---
5 | kind: markdown
6 | value: "`SomeSecret` message or enum type, package: `com.inner.secret`\n---\nSomeSecret is a real secret with hidden string"
7 |
--------------------------------------------------------------------------------
/src/formatter/mod.rs:
--------------------------------------------------------------------------------
1 | use async_lsp::lsp_types::{Range, TextEdit};
2 |
3 | pub mod clang;
4 |
5 | pub trait ProtoFormatter: Sized {
6 | fn format_document(&self, filename: &str, content: &str) -> Option>;
7 | fn format_document_range(
8 | &self,
9 | r: &Range,
10 | filename: &str,
11 | content: &str,
12 | ) -> Option>;
13 | }
14 |
--------------------------------------------------------------------------------
/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-4.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/formatter/clang.rs
3 | description: "syntax = \"proto3\";\n\npackage foo.bar;\n\nmessage Box {\n int64 height = 1;\n int64 width = 2;\n int64 depth = 3;\n}\n\nservice BoxAreaFinder { rpc FindArea(Box) returns (int64); }\n"
4 | expression: "Replacement::offset_to_position(i, c)"
5 | info: 999
6 | ---
7 | ~
8 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__rename__test__reference.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/rename.rs
3 | expression: "state.reference_fields(\"com.workspace\", \"Author\",\nPathBuf::from(\"src/workspace/input\"), None)"
4 | ---
5 | - uri: "file://input/a.proto"
6 | range:
7 | start:
8 | line: 10
9 | character: 3
10 | end:
11 | line: 10
12 | character: 9
13 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__rename__test__reference-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/rename.rs
3 | expression: "state.reference_fields(\"com.workspace\", \"Author.Address\",\nPathBuf::from(\"src/workspace/input\"), None)"
4 | ---
5 | - uri: "file://input/a.proto"
6 | range:
7 | start:
8 | line: 11
9 | character: 3
10 | end:
11 | line: 11
12 | character: 17
13 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__diagnostics__test__collect_parse_error-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/diagnostics.rs
3 | expression: parsed.unwrap().collect_parse_diagnostics()
4 | snapshot_kind: text
5 | ---
6 | - range:
7 | start:
8 | line: 6
9 | character: 8
10 | end:
11 | line: 6
12 | character: 19
13 | severity: 1
14 | source: protols
15 | message: Syntax error
16 |
--------------------------------------------------------------------------------
/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/formatter/clang.rs
3 | description: "syntax = \"proto3\";\n\npackage foo.bar;\n\nmessage Box {\n int64 height = 1;\n int64 width = 2;\n int64 depth = 3;\n}\n\nservice BoxAreaFinder { rpc FindArea(Box) returns (int64); }\n"
4 | expression: "Replacement::offset_to_position(i, c)"
5 | info: 4
6 | ---
7 | line: 0
8 | character: 4
9 |
--------------------------------------------------------------------------------
/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/formatter/clang.rs
3 | description: "syntax = \"proto3\";\n\npackage foo.bar;\n\nmessage Box {\n int64 height = 1;\n int64 width = 2;\n int64 depth = 3;\n}\n\nservice BoxAreaFinder { rpc FindArea(Box) returns (int64); }\n"
4 | expression: "Replacement::offset_to_position(i, c)"
5 | info: 22
6 | ---
7 | line: 2
8 | character: 2
9 |
--------------------------------------------------------------------------------
/src/config/snapshots/protols__config__workspace__test__workspace.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/config/workspace.rs
3 | expression: ws.get_config_for_uri(&inworkspace)
4 | ---
5 | config:
6 | include_paths:
7 | - foobar
8 | - bazbaaz
9 | single_file_mode: false
10 | disable_parse_diagnostics: true
11 | experimental:
12 | use_protoc_diagnostics: true
13 | formatter:
14 | clang_format_path: /usr/bin/clang-format
15 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/workspace_symbol.rs
3 | expression: author_symbols
4 | ---
5 | - name: Author
6 | kind: 23
7 | location:
8 | uri: "file:///src/workspace/input/b.proto"
9 | range:
10 | start:
11 | line: 5
12 | character: 0
13 | end:
14 | line: 14
15 | character: 1
16 |
--------------------------------------------------------------------------------
/sample/test.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package a.b.c;
4 |
5 | message CustomType { bool attribute = 1; }
6 |
7 | message SomeMessage {
8 | int64 someAttribute = 1;
9 |
10 | CustomType another = 2;
11 | }
12 |
13 | message CapitalA {
14 | // B is a b
15 | message CapitalB {
16 |
17 | }
18 |
19 | a.b.c.CapitalA.CapitalB b = 1;
20 | }
21 |
22 | message C {
23 | CapitalA.CapitalB ab = 1;
24 | .a.b.c.CapitalA a = 2;
25 | }
26 |
--------------------------------------------------------------------------------
/src/parser/input/test_filter.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.parser;
4 |
5 | import "foo/bar.proto";
6 | import "baz/bar.proto";
7 |
8 | message Book {
9 |
10 | message Author {
11 | string name = 1;
12 | string country = 2;
13 | };
14 | // This is a multi line comment on the field name
15 | // Of a message called Book
16 | int64 isbn = 1;
17 | string title = 2;
18 | Author author = 3;
19 | }
20 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__rename__test__rename-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/rename.rs
3 | expression: "state.rename_fields(\"com.workspace\", \"Author.Address\", \"Author.Location\",\nPathBuf::from(\"src/workspace/input\"), None)"
4 | ---
5 | "file://input/a.proto":
6 | - range:
7 | start:
8 | line: 11
9 | character: 3
10 | end:
11 | line: 11
12 | character: 17
13 | newText: Author.Location
14 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__rename__test__rename-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/rename.rs
3 | expression: "state.rename_fields(\"com.utility\", \"Foobar.Baz\", \"Foobar.Baaz\",\nPathBuf::from(\"src/workspace/input\"), None)"
4 | ---
5 | "file://input/a.proto":
6 | - range:
7 | start:
8 | line: 12
9 | character: 3
10 | end:
11 | line: 12
12 | character: 25
13 | newText: com.utility.Foobar.Baaz
14 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/workspace_symbol.rs
3 | expression: address_symbols
4 | ---
5 | - name: Address
6 | kind: 23
7 | containerName: Author
8 | location:
9 | uri: "file:///src/workspace/input/b.proto"
10 | range:
11 | start:
12 | line: 9
13 | character: 3
14 | end:
15 | line: 11
16 | character: 4
17 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Field.Cardinality.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Field.Cardinality* well known type
2 | ---
3 | Whether a field is optional, required, or repeated.
4 | ---
5 | ```proto
6 | enum Cardinality {
7 | CARDINALITY_UNKNOWN = 0; // For fields with unknown cardinality.
8 | CARDINALITY_OPTIONAL = 1; // For optional fields.
9 | CARDINALITY_REQUIRED = 2; // For required fields. Proto2 syntax only.
10 | CARDINALITY_REPEATED = 3; // For repeated fields.
11 | }
12 | ```
--------------------------------------------------------------------------------
/src/parser/input/test_can_rename.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.parser;
4 |
5 | // A Book is book
6 | message Book {
7 |
8 | // This is represents author
9 | // A author is a someone who writes books
10 | //
11 | // Author has a name and a country where they were born
12 | message Author {
13 | string name = 1;
14 | string country = 2;
15 | };
16 |
17 | }
18 |
19 | message Outer {
20 | Book.Author a = 1;
21 | }
22 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover-3.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/hover.rs
3 | expression: "state.hover(&ipath, \"com.workspace\",\nHoverables::FieldType(\"int64\".to_string()))"
4 | snapshot_kind: text
5 | ---
6 | kind: markdown
7 | value: "*int64* builtin type, A 64-bit integer (varint encoding)\n\n---\nValues of this type range between `-9223372036854775808` and `9223372036854775807`.\nBeware that negative values are encoded as ten bytes on the wire!\n"
8 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Build
20 | run: cargo build --verbose
21 | - name: Run tests
22 | run: cargo test --verbose
23 | - name: Run lints
24 | run: cargo clippy --all-targets -- -D warnings
25 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Enum.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Enum* well known type
2 | ---
3 | Enum type definition
4 | ---
5 | ```proto
6 | message Enum {
7 | string name = 1; // Enum type name.
8 | repeated EnumValue enumvalue = 2; // Enum value definitions.
9 | repeated Option options = 3; // Protocol buffer options.
10 | SourceContext source_context = 4; // The source context.
11 | Syntax syntax = 5; // The source syntax.
12 | string edition = 6; // The source edition string, only valid when syntax is SYNTAX_EDITIONS.
13 | }
14 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/Value.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Value* well known type
2 | ---
3 | `Value` represents a dynamically typed value which can be either null, a number, a string, a boolean, a recursive struct value, or a list of values.
4 | ---
5 | ```proto
6 | message Value {
7 | oneof kind {
8 | NullValue null_value = 1;
9 | double number_value = 2;
10 | string string_value = 3;
11 | bool bool_value = 4;
12 | Struct struct_value = 5;
13 | ListValue list_value = 6;
14 | }
15 | }
16 | ```
--------------------------------------------------------------------------------
/src/parser/input/test_document_symbols.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.parser;
4 |
5 | // outer 1 comment
6 | message Outer1 {
7 | // Inner 1
8 | message Inner1 {
9 | string name = 1;
10 | };
11 |
12 | Inner1 i = 1;
13 | }
14 |
15 | message Outer2 {
16 | message Inner2 {
17 | string name = 1;
18 | };
19 | // Inner 3 comment here
20 | message Inner3 {
21 | string name = 1;
22 |
23 | enum X {
24 | a = 1;
25 | b = 2;
26 | }
27 | }
28 | Inner1 i = 1;
29 | Inner2 y = 2;
30 | }
31 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Any.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Any* wellknown type
2 | ---
3 | `Any` contains an arbitrary serialized message along with a URL that describes the type of the serialized message.
4 | The JSON representation of an Any value uses the regular representation of the deserialized, embedded message, with an additional field @type which contains the type URL.
5 | ---
6 | ```proto
7 | message Any {
8 | string type_url = 1; // A URL/resource name that uniquely identifies the type of the serialized protocol buffer message
9 | bytes value = 2; // Must be a valid serialized protocol buffer
10 | }
11 | ```
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__rename__test__rename.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/rename.rs
3 | expression: "state.rename_fields(\"com.workspace\", \"Author\", \"Writer\",\nPathBuf::from(\"src/workspace/input\"), None)"
4 | ---
5 | "file://input/a.proto":
6 | - range:
7 | start:
8 | line: 10
9 | character: 3
10 | end:
11 | line: 10
12 | character: 9
13 | newText: Writer
14 | - range:
15 | start:
16 | line: 11
17 | character: 3
18 | end:
19 | line: 11
20 | character: 17
21 | newText: Writer.Address
22 |
--------------------------------------------------------------------------------
/src/parser/input/test_hover.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.parser;
4 |
5 | // A Book is book
6 | message Book {
7 |
8 | // This is represents author
9 | // A author is a someone who writes books
10 | //
11 | // Author has a name and a country where they were born
12 | message Author {
13 | string name = 1;
14 | string country = 2;
15 | };
16 | }
17 |
18 | // Comic is a type of book but who cares
19 | message Comic {
20 | // Author of a comic is different from others
21 | message Author {
22 | string name = 1;
23 | string country = 2;
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__reference.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: reference_fn(&pos_book)
4 | ---
5 | - uri: "file://foo/bar.proto"
6 | range:
7 | start:
8 | line: 22
9 | character: 11
10 | end:
11 | line: 22
12 | character: 15
13 | - uri: "file://foo/bar.proto"
14 | range:
15 | start:
16 | line: 28
17 | character: 30
18 | end:
19 | line: 28
20 | character: 34
21 | - uri: "file://foo/bar.proto"
22 | range:
23 | start:
24 | line: 5
25 | character: 8
26 | end:
27 | line: 5
28 | character: 12
29 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__rename_fields.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: "tree.rename_fields(\"Book\", \"Kitab\", contents)"
4 | ---
5 | - range:
6 | start:
7 | line: 20
8 | character: 13
9 | end:
10 | line: 20
11 | character: 17
12 | newText: Kitab
13 | - range:
14 | start:
15 | line: 21
16 | character: 4
17 | end:
18 | line: 21
19 | character: 15
20 | newText: Kitab.Author
21 | - range:
22 | start:
23 | line: 25
24 | character: 32
25 | end:
26 | line: 25
27 | character: 36
28 | newText: Kitab
29 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Type.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Type* well known type
2 | ---
3 | A protocol buffer message type.
4 | ---
5 | ```proto
6 | message Type {
7 | string name = 1; // The fully qualified message name
8 | repeated Field fields = 2; // The list of fields
9 | repeated string oneofs = 3; // The list of types appearing in `oneof` definitions in this type
10 | repeated Option options = 4; // The protocol buffer options
11 | SourceContext source_context = 5; // The source context
12 | Syntax syntax = 6; // The source syntax
13 | string edition = 7; // The source edition string, only valid when syntax is SYNTAX_EDITIONS
14 | }
15 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/Method.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Method* well known type
2 | ---
3 | Method represents a method of an API interface.
4 | ---
5 | ```proto
6 | message Method {
7 | string name = 1; // The simple name of this method.
8 | string request_type_url = 2; // A URL of the input message type.
9 | bool request_streaming = 3; // If true, the request is streamed.
10 | string response_type_url = 4; // The URL of the output message type.
11 | bool response_streaming = 5; // If true, the response is streamed.
12 | repeated Option options = 6; // Any metadata attached to the method.
13 | Syntax syntax = 7; // The source syntax of this method.
14 | }
15 | ```
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__workspace_symbol__test__author_symbols.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/workspace_symbol.rs
3 | expression: author_symbols
4 | ---
5 | - name: Author
6 | kind: 23
7 | location:
8 | uri: "file:///home/runner/work/protols/protols/src/workspace/input/b.proto"
9 | range:
10 | start:
11 | line: 5
12 | character: 0
13 | end:
14 | line: 14
15 | character: 1
16 | - name: Author
17 | kind: 23
18 | location:
19 | uri: "file://input/b.proto"
20 | range:
21 | start:
22 | line: 5
23 | character: 0
24 | end:
25 | line: 14
26 | character: 1
27 |
--------------------------------------------------------------------------------
/src/parser/input/test_reference.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.parser;
4 |
5 | // A Book is book
6 | message Book {
7 |
8 | // This is represents author
9 | // A author is a someone who writes books
10 | //
11 | // Author has a name and a country where they were born
12 | message Author {
13 | string name = 1;
14 | string country = 2;
15 | };
16 | Author author = 1;
17 | int price_usd = 2;
18 | }
19 |
20 | message BookShelf {}
21 |
22 | message Library {
23 | repeated Book books = 1;
24 | Book.Author collection = 2;
25 | BookShelf shelf = 3;
26 | }
27 |
28 | service Myservice {
29 | rpc GetBook(Empty) returns (Book);
30 | rpc GetAuthor(Empty) returns (Book.Author)
31 | }
32 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Api.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Api* well known type
2 | ---
3 | `Api` is a light-weight descriptor for a protocol buffer service.
4 | ---
5 | ```proto
6 | message Api {
7 | string name = 1; // The fully qualified name of this api, including package name followed by the api's simple name
8 | repeated Method methods = 2; // The methods of this api, in unspecified order
9 | repeated Option options = 3; // Any metadata attached to the API
10 | string version = 4; // A version string fo this interface
11 | SourceContext source_context = 5; // Source context for the protocol buffer service
12 | repeated Mixin mixins = 6; // Included interfaces
13 | Syntax syntax = 7; // Source syntax of the service
14 | }
15 | ```
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__workspace_symbol__test__address_symbols.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/workspace_symbol.rs
3 | expression: address_symbols
4 | ---
5 | - name: Address
6 | kind: 23
7 | containerName: Author
8 | location:
9 | uri: "file:///home/runner/work/protols/protols/src/workspace/input/b.proto"
10 | range:
11 | start:
12 | line: 9
13 | character: 3
14 | end:
15 | line: 11
16 | character: 4
17 | - name: Address
18 | kind: 23
19 | containerName: Author
20 | location:
21 | uri: "file://input/b.proto"
22 | range:
23 | start:
24 | line: 9
25 | character: 3
26 | end:
27 | line: 11
28 | character: 4
29 |
--------------------------------------------------------------------------------
/src/parser/input/test_rename.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.parser;
4 |
5 | // A Book is book
6 | message Book {
7 |
8 | // This is represents author
9 | // A author is a someone who writes books
10 | //
11 | // Author has a name and a country where they were born
12 | message Author {
13 | string name = 1;
14 | string country = 2;
15 | };
16 | Author author = 1;
17 | int price_usd = 2;
18 | }
19 |
20 | message BookShelf {}
21 |
22 | message Library {
23 | repeated Book books = 1;
24 | Book.Author collection = 2;
25 | BookShelf shelf = 3;
26 | }
27 |
28 | service Myservice {
29 | rpc GetBook(Empty) returns (Book);
30 | rpc GetAuthor(Empty) returns (Book.Author)
31 | }
32 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__rename-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: "rename_fn(\"Writer\", &pos_author)"
4 | ---
5 | - range:
6 | start:
7 | line: 23
8 | character: 4
9 | end:
10 | line: 23
11 | character: 15
12 | newText: Book.Writer
13 | - range:
14 | start:
15 | line: 29
16 | character: 34
17 | end:
18 | line: 29
19 | character: 45
20 | newText: Book.Writer
21 | - range:
22 | start:
23 | line: 11
24 | character: 12
25 | end:
26 | line: 11
27 | character: 18
28 | newText: Writer
29 | - range:
30 | start:
31 | line: 15
32 | character: 4
33 | end:
34 | line: 15
35 | character: 10
36 | newText: Writer
37 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__reference-2.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: reference_fn(&pos_author)
4 | ---
5 | - uri: "file://foo/bar.proto"
6 | range:
7 | start:
8 | line: 23
9 | character: 2
10 | end:
11 | line: 23
12 | character: 13
13 | - uri: "file://foo/bar.proto"
14 | range:
15 | start:
16 | line: 29
17 | character: 32
18 | end:
19 | line: 29
20 | character: 43
21 | - uri: "file://foo/bar.proto"
22 | range:
23 | start:
24 | line: 11
25 | character: 10
26 | end:
27 | line: 11
28 | character: 16
29 | - uri: "file://foo/bar.proto"
30 | range:
31 | start:
32 | line: 15
33 | character: 2
34 | end:
35 | line: 15
36 | character: 8
37 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Timestamp.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Timestamp* well known type
2 | ---
3 | A Timestamp represents a point in time independent of any time zone or calendar, represented as seconds and fractions of seconds at nanosecond resolution in UTC Epoch time.
4 |
5 | It is encoded using the Proleptic Gregorian Calendar which extends the Gregorian calendar backwards to year one. It is encoded assuming all minutes are 60 seconds long, i.e. leap seconds are "smeared" so that no leap second table is needed for interpretation. Range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z.
6 | ---
7 | ```proto
8 | message Timestamp {
9 | int64 seconds = 1; // Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z
10 | int32 nanos = 2; // Non-negative fractions of a second at nanosecond resolution
11 | }
12 | ```
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__hover__test__workspace_test_hover.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/hover.rs
3 | expression: "state.hover(&ipath, \"com.workspace\",\nHoverables::Identifier(\"google.protobuf.Any\".to_string()))"
4 | snapshot_kind: text
5 | ---
6 | kind: markdown
7 | value: "*google.protobuf.Any* wellknown type\n---\n`Any` contains an arbitrary serialized message along with a URL that describes the type of the serialized message.\nThe JSON representation of an Any value uses the regular representation of the deserialized, embedded message, with an additional field @type which contains the type URL.\n---\n```proto\nmessage Any {\n string type_url = 1; // A URL/resource name that uniquely identifies the type of the serialized protocol buffer message\n bytes value = 2; // Must be a valid serialized protocol buffer\n}\n```"
8 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Field.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Field* well known type
2 | ---
3 | A single field of a message type.
4 | ---
5 | ```proto
6 | message Field {
7 | Kind kind = 1; // The field type.
8 | Cardinality cardinality = 2; // The field cardinality.
9 | int32 number = 3; // The field number.
10 | string name = 4; // The field name.
11 | string type_url = 6; // The field type URL, without the scheme, for message or enumeration types
12 | int32 oneof_index = 7; // The index of the field type in `Type.oneofs`, for message or enumeration types.
13 | bool packed = 8; // Whether to use alternative packed wire representation.
14 | repeated Option options = 9; // The protocol buffer options.
15 | string json_name = 10; // The field JSON name.
16 | string default_value = 11; // The string value of the default value of this field. Proto2 syntax only.
17 | }
18 | ```
--------------------------------------------------------------------------------
/src/docs/wellknown/Duration.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Duration* well known type
2 | ---
3 | A Duration represents a signed, fixed-length span of time represented as a count of seconds and fractions of seconds at nanosecond resolution.
4 | It is independent of any calendar and concepts like "day" or "month".
5 | It is related to Timestamp in that the difference between two Timestamp values is a Duration and it can be added or subtracted from a Timestamp.
6 | Range is approximately +-10,000 years.
7 | ---
8 | ```proto
9 | message Duration {
10 | int64 seconds = 1; // Signed seconds of the span of time Must be from -315,576,000,000 to +315,576,000,000 inclusive
11 | int32 nanos = 2; // Signed fractions of a second at nanosecond resolution of the span of time. Durations less than one second are represented with a 0 `seconds` field and a positive or negative `nanos` field.
12 | }
13 | ```
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__rename__test__rename.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/rename.rs
3 | expression: "rename_fn(\"Kitab\", &pos_book)"
4 | ---
5 | - range:
6 | start:
7 | line: 22
8 | character: 13
9 | end:
10 | line: 22
11 | character: 17
12 | newText: Kitab
13 | - range:
14 | start:
15 | line: 23
16 | character: 4
17 | end:
18 | line: 23
19 | character: 15
20 | newText: Kitab.Author
21 | - range:
22 | start:
23 | line: 28
24 | character: 32
25 | end:
26 | line: 28
27 | character: 36
28 | newText: Kitab
29 | - range:
30 | start:
31 | line: 29
32 | character: 34
33 | end:
34 | line: 29
35 | character: 45
36 | newText: Kitab.Author
37 | - range:
38 | start:
39 | line: 5
40 | character: 8
41 | end:
42 | line: 5
43 | character: 12
44 | newText: Kitab
45 |
--------------------------------------------------------------------------------
/src/parser/mod.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use async_lsp::lsp_types::Url;
4 | use tree_sitter::Tree;
5 |
6 | mod definition;
7 | mod diagnostics;
8 | mod docsymbol;
9 | mod hover;
10 | mod rename;
11 | mod tree;
12 |
13 | pub struct ProtoParser {
14 | parser: tree_sitter::Parser,
15 | }
16 |
17 | #[derive(Clone)]
18 | pub struct ParsedTree {
19 | pub uri: Url,
20 | tree: Arc,
21 | }
22 |
23 | impl ProtoParser {
24 | pub fn new() -> Self {
25 | let mut parser = tree_sitter::Parser::new();
26 | if let Err(e) = parser.set_language(&tree_sitter_proto::LANGUAGE.into()) {
27 | panic!("failed to set ts language parser {:?}", e);
28 | }
29 | Self { parser }
30 | }
31 |
32 | pub fn parse(&mut self, uri: Url, contents: impl AsRef<[u8]>) -> Option {
33 | self.parser.parse(contents, None).map(|t| ParsedTree {
34 | tree: Arc::new(t),
35 | uri,
36 | })
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/config/mod.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | pub mod workspace;
4 |
5 | fn default_clang_format_path() -> String {
6 | "clang-format".to_string()
7 | }
8 |
9 | fn default_protoc_path() -> String {
10 | "protoc".to_string()
11 | }
12 |
13 | #[derive(Serialize, Deserialize, Debug, Clone, Default)]
14 | #[serde(default)]
15 | pub struct ProtolsConfig {
16 | pub config: Config,
17 | }
18 |
19 | #[derive(Serialize, Deserialize, Debug, Clone, Default)]
20 | #[serde(default)]
21 | pub struct Config {
22 | pub include_paths: Vec,
23 | pub path: PathConfig,
24 | }
25 |
26 | #[derive(Serialize, Deserialize, Debug, Clone)]
27 | #[serde(default)]
28 | pub struct PathConfig {
29 | pub clang_format: String,
30 | pub protoc: String,
31 | }
32 |
33 | impl Default for PathConfig {
34 | fn default() -> Self {
35 | Self {
36 | clang_format: default_clang_format_path(),
37 | protoc: default_protoc_path(),
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/docs/wellknown/Field.Kind.md:
--------------------------------------------------------------------------------
1 | *google.protobuf.Field.Kind* well known type
2 | ---
3 | Basic field types.
4 | ---
5 | ```proto
6 | enum Kind {
7 | TYPE_UNKNOWN = 0; // Field type unknown.
8 | TYPE_DOUBLE = 1; // Field type double.
9 | TYPE_FLOAT = 2; // Field type float.
10 | TYPE_INT64 = 3; // Field type int64.
11 | TYPE_UINT64 = 4; // Field type uint64.
12 | TYPE_INT32 = 5; // Field type int32.
13 | TYPE_FIXED64 = 6; // Field type fixed64.
14 | TYPE_FIXED32 = 7; // Field type fixed32.
15 | TYPE_BOOL = 8; // Field type bool.
16 | TYPE_STRING = 9; // Field type string.
17 | TYPE_GROUP = 10; // Field type group. Proto2 syntax only, and deprecated.
18 | TYPE_MESSAGE = 11; // Field type message.
19 | TYPE_BYTES = 12; // Field type bytes.
20 | TYPE_UINT32 = 13; // Field type uint32.
21 | TYPE_ENUM = 14; // Field type enum.
22 | TYPE_SFIXED32 = 15; // Field type sfixed32.
23 | TYPE_SFIXED64 = 16; // Field type sfixed64.
24 | TYPE_SINT32 = 17; // Field type sint32.
25 | TYPE_SINT64 = 18; // Field type sint64.
26 | }
27 | ```
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "protols"
3 | description = "Language server for proto3 files"
4 | version = "0.13.1"
5 | edition = "2024"
6 | license = "MIT"
7 | homepage = "https://github.com/coder3101/protols"
8 | repository = "https://github.com/coder3101/protols"
9 | readme = "README.md"
10 | keywords = ["lsp", "proto"]
11 |
12 | exclude = ["assets/*", "sample/*"]
13 |
14 | [dependencies]
15 | async-lsp = { version = "0.2", features = ["tokio"] }
16 | futures = "0.3"
17 | tokio = { version = "1.47", features = ["time", "full"] }
18 | tokio-util = { version = "0.7", features = ["compat"] }
19 | tower = "0.5"
20 | tracing = "0.1"
21 | tracing-subscriber = "0.3"
22 | tree-sitter = "0.25"
23 | tracing-appender = "0.2"
24 | tree-sitter-proto = "0.3"
25 | walkdir = "2.5"
26 | hard-xml = "1.41"
27 | tempfile = "3.21"
28 | serde = { version = "1", features = ["derive"] }
29 | serde_json = "1.0"
30 | basic-toml = "0.1"
31 | pkg-config = "0.3"
32 | clap = { version = "4.5", features = ["derive"] }
33 | const_format = "0.2"
34 |
35 | [dev-dependencies]
36 | insta = { version = "1.43", features = ["yaml", "redactions"] }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Ashar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | permissions:
4 | contents: write
5 |
6 | on:
7 | push:
8 | tags:
9 | - '[0-9]+.*'
10 |
11 | jobs:
12 | create-release:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: taiki-e/create-gh-release-action@v1
17 | with:
18 | token: ${{ secrets.GITHUB_TOKEN }}
19 |
20 | upload-assets:
21 | needs: create-release
22 | strategy:
23 | matrix:
24 | include:
25 | - target: aarch64-unknown-linux-gnu
26 | os: ubuntu-latest
27 | - target: aarch64-apple-darwin
28 | os: macos-latest
29 | - target: x86_64-unknown-linux-gnu
30 | os: ubuntu-latest
31 | - target: x86_64-apple-darwin
32 | os: macos-latest
33 | - target: x86_64-pc-windows-msvc
34 | os: windows-latest
35 | runs-on: ${{ matrix.os }}
36 | steps:
37 | - uses: actions/checkout@v4
38 | - uses: taiki-e/upload-rust-binary-action@v1
39 | with:
40 | bin: protols
41 | target: ${{ matrix.target }}
42 | token: ${{ secrets.GITHUB_TOKEN }}
43 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__workspace_symbol__test__all_symbols.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/workspace_symbol.rs
3 | expression: all_symbols
4 | ---
5 | - name: Address
6 | kind: 23
7 | containerName: Author
8 | location:
9 | uri: "file://input/b.proto"
10 | range:
11 | start:
12 | line: 9
13 | character: 3
14 | end:
15 | line: 11
16 | character: 4
17 | - name: Author
18 | kind: 23
19 | location:
20 | uri: "file://input/b.proto"
21 | range:
22 | start:
23 | line: 5
24 | character: 0
25 | end:
26 | line: 14
27 | character: 1
28 | - name: Baz
29 | kind: 23
30 | containerName: Foobar
31 | location:
32 | uri: "file://input/c.proto"
33 | range:
34 | start:
35 | line: 8
36 | character: 3
37 | end:
38 | line: 10
39 | character: 4
40 | - name: Book
41 | kind: 23
42 | location:
43 | uri: "file://input/a.proto"
44 | range:
45 | start:
46 | line: 9
47 | character: 0
48 | end:
49 | line: 14
50 | character: 1
51 | - name: Foobar
52 | kind: 23
53 | location:
54 | uri: "file://input/c.proto"
55 | range:
56 | start:
57 | line: 5
58 | character: 0
59 | end:
60 | line: 13
61 | character: 1
62 |
--------------------------------------------------------------------------------
/sample/everything.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | // Import Google's Well-Known Types
4 | import "google/protobuf/timestamp.proto";
5 | import "google/protobuf/duration.proto";
6 | import "google/protobuf/empty.proto";
7 | import "google/protobuf/struct.proto";
8 | import "google/protobuf/any.proto";
9 |
10 | package example;
11 |
12 | // Enum example
13 | enum Status {
14 | UNKNOWN = 0;
15 | ACTIVE = 1;
16 | INACTIVE = 2;
17 | }
18 |
19 | // Nested message example
20 | message Address {
21 | string street = 1;
22 | string city = 2;
23 | string state = 3;
24 | string zip_code = 4;
25 | }
26 |
27 | // Main message example
28 | message Person {
29 | // Scalar types
30 | string name = 1;
31 | int32 age = 2;
32 | bool is_verified = 3;
33 |
34 | // Repeated field (array)
35 | repeated string phone_numbers = 4;
36 |
37 | // Map example
38 | map attributes = 5;
39 |
40 | // Enum field
41 | Status status = 6;
42 |
43 | // Nested message
44 | Address address = 7;
45 |
46 | // Oneof example
47 | oneof contact_method {
48 | string email = 8;
49 | string phone = 9;
50 | }
51 |
52 | // Google Well-Known Types
53 | google.protobuf.Timestamp last_updated = 10;
54 | google.protobuf.Duration session_duration = 11;
55 | google.protobuf.Empty metadata = 12;
56 | google.protobuf.Struct extra_data = 13;
57 | google.protobuf.Any any_data = 14;
58 | }
59 |
60 | // Service example
61 | service PersonService {
62 | rpc GetPerson(google.protobuf.Empty) returns (Person);
63 | rpc UpdatePerson(Person) returns (google.protobuf.Empty);
64 | }
65 |
--------------------------------------------------------------------------------
/src/workspace/snapshots/protols__workspace__workspace_symbol__test__workspace_symbols.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/workspace/workspace_symbol.rs
3 | expression: all_symbols
4 | ---
5 | - name: Address
6 | kind: 23
7 | containerName: Author
8 | location:
9 | uri: "file:///src/workspace/input/b.proto"
10 | range:
11 | start:
12 | line: 9
13 | character: 3
14 | end:
15 | line: 11
16 | character: 4
17 | - name: Author
18 | kind: 23
19 | location:
20 | uri: "file:///src/workspace/input/b.proto"
21 | range:
22 | start:
23 | line: 5
24 | character: 0
25 | end:
26 | line: 14
27 | character: 1
28 | - name: Baz
29 | kind: 23
30 | containerName: Foobar
31 | location:
32 | uri: "file:///src/workspace/input/c.proto"
33 | range:
34 | start:
35 | line: 8
36 | character: 3
37 | end:
38 | line: 10
39 | character: 4
40 | - name: Book
41 | kind: 23
42 | location:
43 | uri: "file:///src/workspace/input/a.proto"
44 | range:
45 | start:
46 | line: 9
47 | character: 0
48 | end:
49 | line: 14
50 | character: 1
51 | - name: Foobar
52 | kind: 23
53 | location:
54 | uri: "file:///src/workspace/input/c.proto"
55 | range:
56 | start:
57 | line: 5
58 | character: 0
59 | end:
60 | line: 13
61 | character: 1
62 | - name: SomeSecret
63 | kind: 23
64 | location:
65 | uri: "file:///src/workspace/input/y.proto"
66 | range:
67 | start:
68 | line: 5
69 | character: 0
70 | end:
71 | line: 7
72 | character: 1
73 | - name: Why
74 | kind: 23
75 | location:
76 | uri: "file:///src/workspace/input/x.proto"
77 | range:
78 | start:
79 | line: 7
80 | character: 0
81 | end:
82 | line: 11
83 | character: 1
84 |
--------------------------------------------------------------------------------
/sample/simple.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.book;
4 |
5 | import "google/protobuf/any.proto";
6 |
7 | // This is a book represeted by some comments that we like to address in the
8 | // review
9 | message Book {
10 | // This is a multi line comment on the field name
11 | // Of a message called Book
12 | int64 isbn = 1;
13 | string title = 2;
14 | Author author = 3;
15 | google.protobuf.Any data = 4;
16 | BookState state = 5;
17 |
18 | // # Author is a author of a book
19 | // Usage is as follow:
20 | // ```rust
21 | // println!("hello world")
22 | // ```
23 | message Author {
24 | string name = 1;
25 | int64 age = 2;
26 | }
27 |
28 | enum BookState {
29 | UNSPECIFIED = 0;
30 | HARD_COVER = 1;
31 | SOFT_COVER = 2;
32 | }
33 | }
34 |
35 | // This is a comment on message
36 | message GetBookRequest {
37 | // This is a sigle line comment on the field of a message
38 | int64 isbn = 1;
39 | }
40 |
41 | message GotoBookRequest { bool flag = 1; }
42 |
43 | message GetBookViaAuthor { Book.Author author = 1; }
44 |
45 | // It is a BookService Implementation
46 | service BookService {
47 | // This is GetBook RPC that takes a book request
48 | // and returns a Book, simple and sweet
49 | rpc GetBook(GetBookRequest) returns (Book) {}
50 | rpc GetBookAuthor(GetBookRequest) returns (Book.Author) {}
51 | rpc GetBooksViaAuthor(GetBookViaAuthor) returns (stream Book) {}
52 | rpc GetGreatestBook(stream GetBookRequest) returns (Book) {}
53 | rpc GetBooks(stream GetBookRequest) returns (stream Book) {}
54 | }
55 |
56 | message BookStore {
57 | reserved 1;
58 | Book book = 5;
59 | string name = 2;
60 | map books = 3;
61 | EnumSample sample = 4;
62 | }
63 |
64 | // These are enum options representing some operation in the proto
65 | // these are meant to be ony called from one place,
66 |
67 | // Note: Please set only to started or running
68 | enum EnumSample {
69 | option allow_alias = true;
70 | UNKNOWN = 0;
71 | STARTED = 1;
72 | RUNNING = 1;
73 | }
74 |
--------------------------------------------------------------------------------
/src/parser/diagnostics.rs:
--------------------------------------------------------------------------------
1 | use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Range};
2 |
3 | use crate::{nodekind::NodeKind, utils::ts_to_lsp_position};
4 |
5 | use super::ParsedTree;
6 |
7 | impl ParsedTree {
8 | pub fn collect_parse_diagnostics(&self) -> Vec {
9 | self.find_all_nodes(NodeKind::is_error)
10 | .into_iter()
11 | .map(|n| Diagnostic {
12 | range: Range {
13 | start: ts_to_lsp_position(&n.start_position()),
14 | end: ts_to_lsp_position(&n.end_position()),
15 | },
16 | severity: Some(DiagnosticSeverity::ERROR),
17 | source: Some("protols".to_string()),
18 | message: "Syntax error".to_string(),
19 | ..Default::default()
20 | })
21 | .collect()
22 | }
23 |
24 | pub fn collect_import_diagnostics(
25 | &self,
26 | content: &[u8],
27 | import: Vec,
28 | ) -> Vec {
29 | self.get_import_path_range(content, import)
30 | .into_iter()
31 | .map(|r| Diagnostic {
32 | range: r,
33 | severity: Some(DiagnosticSeverity::ERROR),
34 | source: Some(String::from("protols")),
35 | message: "failed to find proto file".to_string(),
36 | ..Default::default()
37 | })
38 | .collect()
39 | }
40 | }
41 |
42 | #[cfg(test)]
43 | mod test {
44 | use async_lsp::lsp_types::Url;
45 | use insta::assert_yaml_snapshot;
46 |
47 | use crate::parser::ProtoParser;
48 |
49 | #[test]
50 | fn test_collect_parse_error() {
51 | let url: Url = "file://foo/bar.proto".parse().unwrap();
52 | let contents = include_str!("input/test_collect_parse_error1.proto");
53 |
54 | let parsed = ProtoParser::new().parse(url.clone(), contents);
55 | assert!(parsed.is_some());
56 | assert_yaml_snapshot!(parsed.unwrap().collect_parse_diagnostics());
57 |
58 | let contents = include_str!("input/test_collect_parse_error2.proto");
59 |
60 | let parsed = ProtoParser::new().parse(url.clone(), contents);
61 | assert!(parsed.is_some());
62 | assert_yaml_snapshot!(parsed.unwrap().collect_parse_diagnostics());
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/parser/snapshots/protols__parser__docsymbol__test__document_symbols.snap:
--------------------------------------------------------------------------------
1 | ---
2 | source: src/parser/docsymbol.rs
3 | expression: tree.find_document_locations(contents)
4 | ---
5 | - name: Outer1
6 | detail: outer 1 comment
7 | kind: 23
8 | range:
9 | start:
10 | line: 5
11 | character: 0
12 | end:
13 | line: 12
14 | character: 1
15 | selectionRange:
16 | start:
17 | line: 5
18 | character: 8
19 | end:
20 | line: 5
21 | character: 14
22 | children:
23 | - name: Inner1
24 | detail: Inner 1
25 | kind: 23
26 | range:
27 | start:
28 | line: 7
29 | character: 4
30 | end:
31 | line: 9
32 | character: 5
33 | selectionRange:
34 | start:
35 | line: 7
36 | character: 12
37 | end:
38 | line: 7
39 | character: 18
40 | children: []
41 | - name: Outer2
42 | kind: 23
43 | range:
44 | start:
45 | line: 14
46 | character: 0
47 | end:
48 | line: 29
49 | character: 1
50 | selectionRange:
51 | start:
52 | line: 14
53 | character: 8
54 | end:
55 | line: 14
56 | character: 14
57 | children:
58 | - name: Inner2
59 | kind: 23
60 | range:
61 | start:
62 | line: 15
63 | character: 4
64 | end:
65 | line: 17
66 | character: 5
67 | selectionRange:
68 | start:
69 | line: 15
70 | character: 12
71 | end:
72 | line: 15
73 | character: 18
74 | children: []
75 | - name: Inner3
76 | detail: Inner 3 comment here
77 | kind: 23
78 | range:
79 | start:
80 | line: 19
81 | character: 4
82 | end:
83 | line: 26
84 | character: 5
85 | selectionRange:
86 | start:
87 | line: 19
88 | character: 12
89 | end:
90 | line: 19
91 | character: 18
92 | children:
93 | - name: X
94 | kind: 10
95 | range:
96 | start:
97 | line: 22
98 | character: 8
99 | end:
100 | line: 25
101 | character: 9
102 | selectionRange:
103 | start:
104 | line: 22
105 | character: 13
106 | end:
107 | line: 22
108 | character: 14
109 | children: []
110 |
--------------------------------------------------------------------------------
/src/docs.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, sync::LazyLock};
2 |
3 | macro_rules! docmap_builtin {
4 | ($name:literal) => {
5 | ($name, include_str!(concat!("docs/builtin/", $name, ".md")))
6 | };
7 | }
8 |
9 | macro_rules! docmap_wellknown {
10 | ($name:literal) => {
11 | (
12 | concat!("google.protobuf.", $name),
13 | include_str!(concat!("docs/wellknown/", $name, ".md")),
14 | )
15 | };
16 | }
17 |
18 | pub static BUITIN: LazyLock> = LazyLock::new(|| {
19 | HashMap::from([
20 | docmap_builtin!("int32"),
21 | docmap_builtin!("int64"),
22 | docmap_builtin!("uint32"),
23 | docmap_builtin!("uint64"),
24 | docmap_builtin!("sint32"),
25 | docmap_builtin!("sint64"),
26 | docmap_builtin!("fixed32"),
27 | docmap_builtin!("fixed64"),
28 | docmap_builtin!("sfixed32"),
29 | docmap_builtin!("sfixed64"),
30 | docmap_builtin!("float"),
31 | docmap_builtin!("double"),
32 | docmap_builtin!("string"),
33 | docmap_builtin!("bytes"),
34 | docmap_builtin!("bool"),
35 | docmap_builtin!("default"),
36 | ])
37 | });
38 |
39 | pub static WELLKNOWN: LazyLock> = LazyLock::new(|| {
40 | HashMap::from([
41 | docmap_wellknown!("Any"),
42 | docmap_wellknown!("Api"),
43 | docmap_wellknown!("BoolValue"),
44 | docmap_wellknown!("BytesValue"),
45 | docmap_wellknown!("DoubleValue"),
46 | docmap_wellknown!("Duration"),
47 | docmap_wellknown!("Empty"),
48 | docmap_wellknown!("Enum"),
49 | docmap_wellknown!("EnumValue"),
50 | docmap_wellknown!("Field"),
51 | docmap_wellknown!("Field.Cardinality"),
52 | docmap_wellknown!("Field.Kind"),
53 | docmap_wellknown!("FieldMask"),
54 | docmap_wellknown!("FloatValue"),
55 | docmap_wellknown!("Int32Value"),
56 | docmap_wellknown!("Int64Value"),
57 | docmap_wellknown!("ListValue"),
58 | docmap_wellknown!("Method"),
59 | docmap_wellknown!("Mixin"),
60 | docmap_wellknown!("NullValue"),
61 | docmap_wellknown!("Option"),
62 | docmap_wellknown!("SourceContext"),
63 | docmap_wellknown!("StringValue"),
64 | docmap_wellknown!("Struct"),
65 | docmap_wellknown!("Syntax"),
66 | docmap_wellknown!("Timestamp"),
67 | docmap_wellknown!("Type"),
68 | docmap_wellknown!("UInt32Value"),
69 | docmap_wellknown!("UInt64Value"),
70 | docmap_wellknown!("Value"),
71 | ])
72 | });
73 |
--------------------------------------------------------------------------------
/src/nodekind.rs:
--------------------------------------------------------------------------------
1 | use async_lsp::lsp_types::SymbolKind;
2 | use tree_sitter::Node;
3 |
4 | pub enum NodeKind {
5 | Identifier,
6 | Error,
7 | MessageName,
8 | Message,
9 | EnumName,
10 | FieldName,
11 | ServiceName,
12 | RpcName,
13 | PackageName,
14 | PackageImport,
15 | }
16 |
17 | #[allow(unused)]
18 | impl NodeKind {
19 | pub fn as_str(&self) -> &'static str {
20 | match self {
21 | NodeKind::Identifier => "identifier",
22 | NodeKind::Error => "ERROR",
23 | NodeKind::MessageName => "message_name",
24 | NodeKind::Message => "message",
25 | NodeKind::EnumName => "enum_name",
26 | NodeKind::FieldName => "message_or_enum_type",
27 | NodeKind::ServiceName => "service_name",
28 | NodeKind::RpcName => "rpc_name",
29 | NodeKind::PackageName => "full_ident",
30 | NodeKind::PackageImport => "import",
31 | }
32 | }
33 |
34 | pub fn is_identifier(n: &Node) -> bool {
35 | n.kind() == Self::Identifier.as_str()
36 | }
37 |
38 | pub fn is_error(n: &Node) -> bool {
39 | n.kind() == Self::Error.as_str()
40 | }
41 |
42 | pub fn is_import_path(n: &Node) -> bool {
43 | n.kind() == Self::PackageImport.as_str()
44 | }
45 |
46 | pub fn is_package_name(n: &Node) -> bool {
47 | n.kind() == Self::PackageName.as_str()
48 | }
49 |
50 | pub fn is_enum_name(n: &Node) -> bool {
51 | n.kind() == Self::EnumName.as_str()
52 | }
53 |
54 | pub fn is_message_name(n: &Node) -> bool {
55 | n.kind() == Self::MessageName.as_str()
56 | }
57 |
58 | pub fn is_message(n: &Node) -> bool {
59 | n.kind() == Self::Message.as_str()
60 | }
61 |
62 | pub fn is_field_name(n: &Node) -> bool {
63 | n.kind() == Self::FieldName.as_str()
64 | }
65 |
66 | pub fn is_userdefined(n: &Node) -> bool {
67 | n.kind() == Self::EnumName.as_str() || n.kind() == Self::MessageName.as_str()
68 | }
69 |
70 | pub fn is_actionable(n: &Node) -> bool {
71 | n.kind() == Self::MessageName.as_str()
72 | || n.kind() == Self::EnumName.as_str()
73 | || n.kind() == Self::FieldName.as_str()
74 | || n.kind() == Self::PackageName.as_str()
75 | || n.kind() == Self::ServiceName.as_str()
76 | || n.kind() == Self::RpcName.as_str()
77 | }
78 |
79 | pub fn to_symbolkind(n: &Node) -> SymbolKind {
80 | if n.kind() == Self::MessageName.as_str() {
81 | SymbolKind::STRUCT
82 | } else if n.kind() == Self::EnumName.as_str() {
83 | SymbolKind::ENUM
84 | } else {
85 | SymbolKind::NULL
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/utils.rs:
--------------------------------------------------------------------------------
1 | use async_lsp::lsp_types::Position;
2 | use tree_sitter::Point;
3 |
4 | pub fn ts_to_lsp_position(p: &Point) -> Position {
5 | Position {
6 | line: p.row as u32,
7 | character: p.column as u32,
8 | }
9 | }
10 |
11 | pub fn lsp_to_ts_point(p: &Position) -> Point {
12 | Point {
13 | row: p.line as usize,
14 | column: p.character as usize,
15 | }
16 | }
17 |
18 | fn is_title_case(s: &str) -> bool {
19 | s.chars()
20 | .next()
21 | .map(|x| x.is_uppercase())
22 | .unwrap_or_default()
23 | }
24 |
25 | fn is_first_lower_case(s: &&str) -> bool {
26 | s.chars()
27 | .next()
28 | .map(|x| x.is_lowercase())
29 | .unwrap_or_default()
30 | }
31 |
32 | pub fn is_inner_identifier(s: &str) -> bool {
33 | if !s.contains('.') {
34 | return false;
35 | }
36 | s.split('.').all(is_title_case)
37 | }
38 |
39 | pub fn split_identifier_package(s: &str) -> (&str, &str) {
40 | let s = s.trim_start_matches(".");
41 | if is_inner_identifier(s) || !s.contains('.') {
42 | return ("", s);
43 | }
44 |
45 | let i = s
46 | .split('.')
47 | .take_while(is_first_lower_case)
48 | .fold(0, |mut c, s| {
49 | if c != 0 {
50 | c += 1;
51 | }
52 | c += s.len();
53 | c
54 | });
55 |
56 | let (package, identifier) = s.split_at(i);
57 | (package, identifier.trim_matches('.'))
58 | }
59 |
60 | #[cfg(test)]
61 | mod test {
62 | use crate::utils::{is_inner_identifier, split_identifier_package};
63 |
64 | #[test]
65 | fn test_is_inner_identifier() {
66 | assert!(is_inner_identifier("Book.Author"));
67 | assert!(is_inner_identifier("Book.Author.Address"));
68 |
69 | assert!(!is_inner_identifier("com.book.Foo"));
70 | assert!(!is_inner_identifier("Book"));
71 | assert!(!is_inner_identifier("foo.Bar"));
72 | }
73 |
74 | #[test]
75 | fn test_split_identifier_package() {
76 | assert_eq!(
77 | split_identifier_package("com.book.Book"),
78 | ("com.book", "Book")
79 | );
80 | assert_eq!(
81 | split_identifier_package(".com.book.Book"),
82 | ("com.book", "Book")
83 | );
84 | assert_eq!(
85 | split_identifier_package("com.book.Book.Author"),
86 | ("com.book", "Book.Author")
87 | );
88 |
89 | assert_eq!(split_identifier_package("com.Book"), ("com", "Book"));
90 | assert_eq!(split_identifier_package("Book"), ("", "Book"));
91 | assert_eq!(split_identifier_package("Book.Author"), ("", "Book.Author"));
92 | assert_eq!(split_identifier_package("com.book"), ("com.book", ""));
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/parser/definition.rs:
--------------------------------------------------------------------------------
1 | use async_lsp::lsp_types::{Location, Range};
2 | use tree_sitter::Node;
3 |
4 | use crate::{nodekind::NodeKind, utils::ts_to_lsp_position};
5 |
6 | use super::ParsedTree;
7 |
8 | impl ParsedTree {
9 | pub fn definition(&self, identifier: &str, content: impl AsRef<[u8]>) -> Vec {
10 | let mut results = vec![];
11 | self.definition_impl(identifier, self.tree.root_node(), &mut results, content);
12 | results
13 | }
14 |
15 | fn definition_impl(
16 | &self,
17 | identifier: &str,
18 | n: Node,
19 | v: &mut Vec,
20 | content: impl AsRef<[u8]>,
21 | ) {
22 | if identifier.is_empty() {
23 | return;
24 | }
25 |
26 | match identifier.split_once('.') {
27 | Some((parent_identifier, remaining)) => {
28 | let child_node = self
29 | .find_all_nodes_from(n, NodeKind::is_userdefined)
30 | .into_iter()
31 | .find(|n| {
32 | n.utf8_text(content.as_ref()).expect("utf8-parse error")
33 | == parent_identifier
34 | })
35 | .and_then(|n| n.parent());
36 |
37 | if let Some(inner) = child_node {
38 | self.definition_impl(remaining, inner, v, content);
39 | }
40 | }
41 | None => {
42 | let locations: Vec = self
43 | .find_all_nodes_from(n, NodeKind::is_userdefined)
44 | .into_iter()
45 | .filter(|n| {
46 | n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier
47 | })
48 | .map(|n| Location {
49 | uri: self.uri.clone(),
50 | range: Range {
51 | start: ts_to_lsp_position(&n.start_position()),
52 | end: ts_to_lsp_position(&n.end_position()),
53 | },
54 | })
55 | .collect();
56 |
57 | v.extend(locations);
58 | }
59 | }
60 | }
61 | }
62 |
63 | #[cfg(test)]
64 | mod test {
65 | use async_lsp::lsp_types::Url;
66 | use insta::assert_yaml_snapshot;
67 |
68 | use crate::parser::ProtoParser;
69 |
70 | #[test]
71 | fn test_goto_definition() {
72 | let url: Url = "file://foo/bar.proto".parse().unwrap();
73 | let contents = include_str!("input/test_goto_definition.proto");
74 | let parsed = ProtoParser::new().parse(url, contents);
75 |
76 | assert!(parsed.is_some());
77 | let tree = parsed.unwrap();
78 | assert_yaml_snapshot!(tree.definition("Author", contents));
79 | assert_yaml_snapshot!(tree.definition("", contents));
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/protoc.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::ts_to_lsp_position;
2 | use async_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Range};
3 | use std::process::Command;
4 | use tree_sitter::Point;
5 |
6 | pub struct ProtocDiagnostics {}
7 |
8 | impl ProtocDiagnostics {
9 | pub fn new() -> Self {
10 | Self {}
11 | }
12 |
13 | pub fn collect_diagnostics(
14 | &self,
15 | protoc_path: &str,
16 | file_path: &str,
17 | include_paths: &[String],
18 | ) -> Vec {
19 | let mut cmd = Command::new(protoc_path);
20 |
21 | // Add include paths
22 | for path in include_paths {
23 | cmd.arg("-I").arg(path);
24 | }
25 |
26 | // Generate descriptor but discard its output
27 | cmd.arg("-o")
28 | .arg(if cfg!(windows) { "NUL" } else { "/dev/null" });
29 |
30 | // Add the file to check
31 | cmd.arg(file_path);
32 |
33 | // Run protoc and capture output
34 | match cmd.output() {
35 | Ok(output) => {
36 | if !output.status.success() {
37 | let error = String::from_utf8_lossy(&output.stderr);
38 | self.parse_protoc_output(&error)
39 | } else {
40 | Vec::new()
41 | }
42 | }
43 | Err(e) => {
44 | tracing::error!(error=%e, "failed to run protoc");
45 | Vec::new()
46 | }
47 | }
48 | }
49 |
50 | fn parse_protoc_output(&self, output: &str) -> Vec {
51 | let mut diagnostics = Vec::new();
52 |
53 | for line in output.lines() {
54 | // Parse protoc error format: file:line:column: message
55 | if let Some((file_info, message)) = line.split_once(": ") {
56 | let parts: Vec<&str> = file_info.split(':').collect();
57 | if parts.len() >= 3
58 | && let (Ok(line), Ok(col)) = (parts[1].parse::(), parts[2].parse::())
59 | {
60 | let point = Point {
61 | row: (line - 1) as usize,
62 | column: (col - 1) as usize,
63 | };
64 | let diagnostic = Diagnostic {
65 | range: Range {
66 | start: ts_to_lsp_position(&point),
67 | end: ts_to_lsp_position(&Point {
68 | row: point.row,
69 | column: point.column + 1,
70 | }),
71 | },
72 | severity: Some(DiagnosticSeverity::ERROR),
73 | source: Some("protoc".to_string()),
74 | message: message.to_string(),
75 | ..Default::default()
76 | };
77 | diagnostics.push(diagnostic);
78 | }
79 | }
80 | }
81 |
82 | diagnostics
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/workspace/workspace_symbol.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | mod test {
3 | use insta::assert_yaml_snapshot;
4 |
5 | use crate::config::Config;
6 | use crate::state::ProtoLanguageState;
7 |
8 | #[test]
9 | fn test_workspace_symbols() {
10 | let current_dir = std::env::current_dir().unwrap();
11 | let ipath = vec![current_dir.join("src/workspace/input")];
12 | let a_uri = format!(
13 | "file://{}/src/workspace/input/a.proto",
14 | current_dir.to_str().unwrap()
15 | )
16 | .parse()
17 | .unwrap();
18 | let b_uri = format!(
19 | "file://{}/src/workspace/input/b.proto",
20 | current_dir.to_str().unwrap()
21 | )
22 | .parse()
23 | .unwrap();
24 | let c_uri = format!(
25 | "file://{}/src/workspace/input/c.proto",
26 | current_dir.to_str().unwrap()
27 | )
28 | .parse()
29 | .unwrap();
30 |
31 | let a = include_str!("input/a.proto");
32 | let b = include_str!("input/b.proto");
33 | let c = include_str!("input/c.proto");
34 |
35 | let mut state: ProtoLanguageState = ProtoLanguageState::new();
36 | state.upsert_file(&a_uri, a.to_owned(), &ipath, 3, &Config::default(), false);
37 | state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false);
38 | state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false);
39 |
40 | // Test empty query - should return all symbols
41 | let all_symbols = state.find_workspace_symbols("");
42 | let cdir = current_dir.to_str().unwrap().to_string();
43 | assert_yaml_snapshot!(all_symbols, { "[].location.uri" => insta::dynamic_redaction(move |c, _| {
44 | assert!(
45 | c.as_str()
46 | .unwrap()
47 | .contains(&cdir)
48 | );
49 | format!(
50 | "file:///src/workspace/input/{}",
51 | c.as_str().unwrap().split('/').next_back().unwrap()
52 | )
53 |
54 | })});
55 |
56 | // Test query for "author" - should match Author and Address
57 | let author_symbols = state.find_workspace_symbols("author");
58 | let cdir = current_dir.to_str().unwrap().to_string();
59 | assert_yaml_snapshot!(author_symbols, {"[].location.uri" => insta::dynamic_redaction(move |c ,_|{
60 | assert!(
61 | c.as_str()
62 | .unwrap()
63 | .contains(&cdir)
64 | );
65 | format!(
66 | "file:///src/workspace/input/{}",
67 | c.as_str().unwrap().split('/').next_back().unwrap()
68 | )
69 | })});
70 |
71 | // Test query for "address" - should match Address
72 | let address_symbols = state.find_workspace_symbols("address");
73 | assert_yaml_snapshot!(address_symbols, {"[].location.uri" => insta::dynamic_redaction(move |c ,_|{
74 | assert!(
75 | c.as_str()
76 | .unwrap()
77 | .contains(current_dir.to_str().unwrap())
78 | );
79 | format!(
80 | "file:///src/workspace/input/{}",
81 | c.as_str().unwrap().split('/').next_back().unwrap()
82 | )
83 | })});
84 |
85 | // Test query that should not match anything
86 | let no_match = state.find_workspace_symbols("nonexistent");
87 | assert!(no_match.is_empty());
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/parser/hover.rs:
--------------------------------------------------------------------------------
1 | use tree_sitter::Node;
2 |
3 | use crate::nodekind::NodeKind;
4 |
5 | use super::ParsedTree;
6 |
7 | impl ParsedTree {
8 | pub(super) fn find_preceding_comments(
9 | &self,
10 | nid: usize,
11 | content: impl AsRef<[u8]>,
12 | ) -> Option {
13 | let root = self.tree.root_node();
14 | let mut cursor = root.walk();
15 |
16 | Self::advance_cursor_to(&mut cursor, nid);
17 | if !cursor.goto_parent() {
18 | return None;
19 | }
20 |
21 | if !cursor.goto_previous_sibling() {
22 | return None;
23 | }
24 |
25 | let mut comments = vec![];
26 | while cursor.node().kind() == "comment" {
27 | let node = cursor.node();
28 | let text = node
29 | .utf8_text(content.as_ref())
30 | .expect("utf-8 parser error")
31 | .trim()
32 | .trim_start_matches("//")
33 | .trim();
34 |
35 | comments.push(text);
36 |
37 | if !cursor.goto_previous_sibling() {
38 | break;
39 | }
40 | }
41 | if !comments.is_empty() {
42 | comments.reverse();
43 | Some(comments.join("\n"))
44 | } else {
45 | None
46 | }
47 | }
48 |
49 | pub fn hover(&self, identifier: &str, content: impl AsRef<[u8]>) -> Vec {
50 | let mut results = vec![];
51 | self.hover_impl(identifier, self.tree.root_node(), &mut results, content);
52 | results
53 | }
54 |
55 | fn hover_impl(
56 | &self,
57 | identifier: &str,
58 | n: Node,
59 | v: &mut Vec,
60 | content: impl AsRef<[u8]>,
61 | ) {
62 | if identifier.is_empty() {
63 | return;
64 | }
65 |
66 | match identifier.split_once('.') {
67 | Some((parent, child)) => {
68 | let child_node = self
69 | .find_all_nodes_from(n, NodeKind::is_userdefined)
70 | .into_iter()
71 | .find(|n| n.utf8_text(content.as_ref()).expect("utf8-parse error") == parent)
72 | .and_then(|n| n.parent());
73 |
74 | if let Some(inner) = child_node {
75 | self.hover_impl(child, inner, v, content);
76 | }
77 | }
78 | None => {
79 | let comments: Vec = self
80 | .find_all_nodes_from(n, NodeKind::is_userdefined)
81 | .into_iter()
82 | .filter(|n| {
83 | n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier
84 | })
85 | .filter_map(|n| self.find_preceding_comments(n.id(), content.as_ref()))
86 | .collect();
87 |
88 | v.extend(comments);
89 | }
90 | }
91 | }
92 | }
93 |
94 | #[cfg(test)]
95 | mod test {
96 | use async_lsp::lsp_types::Url;
97 | use insta::assert_yaml_snapshot;
98 |
99 | use crate::parser::ProtoParser;
100 |
101 | #[test]
102 | fn test_hover() {
103 | let uri: Url = "file://foo.bar/p.proto".parse().unwrap();
104 | let contents = include_str!("input/test_hover.proto");
105 | let parsed = ProtoParser::new().parse(uri.clone(), contents);
106 |
107 | assert!(parsed.is_some());
108 | let tree = parsed.unwrap();
109 |
110 | let res = tree.hover("Book", contents);
111 | assert_yaml_snapshot!(res);
112 |
113 | let res = tree.hover("", contents);
114 | assert_yaml_snapshot!(res);
115 |
116 | let res = tree.hover("Book.Author", contents);
117 | assert_yaml_snapshot!(res);
118 |
119 | let res = tree.hover("Comic.Author", contents);
120 | assert_yaml_snapshot!(res);
121 |
122 | let res = tree.hover("Author", contents);
123 | assert_yaml_snapshot!(res);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/server.rs:
--------------------------------------------------------------------------------
1 | use async_lsp::{
2 | ClientSocket, LanguageClient,
3 | lsp_types::{
4 | NumberOrString, ProgressParams, ProgressParamsValue,
5 | notification::{
6 | DidChangeTextDocument, DidCreateFiles, DidDeleteFiles, DidOpenTextDocument,
7 | DidRenameFiles, DidSaveTextDocument,
8 | },
9 | request::{
10 | Completion, DocumentSymbolRequest, Formatting, GotoDefinition, HoverRequest,
11 | Initialize, PrepareRenameRequest, RangeFormatting, References, Rename,
12 | WorkspaceSymbolRequest,
13 | },
14 | },
15 | router::Router,
16 | };
17 | use std::{
18 | ops::ControlFlow,
19 | path::PathBuf,
20 | sync::{mpsc, mpsc::Sender},
21 | thread,
22 | };
23 |
24 | use crate::{config::workspace::WorkspaceProtoConfigs, state::ProtoLanguageState};
25 |
26 | pub struct TickEvent;
27 | pub struct ProtoLanguageServer {
28 | pub client: ClientSocket,
29 | pub counter: i32,
30 | pub state: ProtoLanguageState,
31 | pub configs: WorkspaceProtoConfigs,
32 | }
33 |
34 | impl ProtoLanguageServer {
35 | pub fn new_router(
36 | client: ClientSocket,
37 | cli_include_paths: Vec,
38 | fallback_include_path: Option,
39 | ) -> Router {
40 | let mut router = Router::new(Self {
41 | client,
42 | counter: 0,
43 | state: ProtoLanguageState::new(),
44 | configs: WorkspaceProtoConfigs::new(cli_include_paths, fallback_include_path),
45 | });
46 |
47 | router.event::(|st, _| {
48 | st.counter += 1;
49 | ControlFlow::Continue(())
50 | });
51 |
52 | // Ignore any unknown notification.
53 | router.unhandled_notification(|_, notif| {
54 | tracing::info!(notif.method, "ignored unknown notification");
55 | ControlFlow::Continue(())
56 | });
57 |
58 | // Handling request
59 | router.request::(|st, params| st.initialize(params));
60 | router.request::(|st, params| st.hover(params));
61 | router.request::(|st, params| st.completion(params));
62 | router.request::(|st, params| st.prepare_rename(params));
63 | router.request::(|st, params| st.rename(params));
64 | router.request::(|st, params| st.references(params));
65 | router.request::(|st, params| st.definition(params));
66 | router.request::(|st, params| st.document_symbol(params));
67 | router.request::(|st, params| st.workspace_symbol(params));
68 | router.request::(|st, params| st.formatting(params));
69 | router.request::(|st, params| st.range_formatting(params));
70 |
71 | // Handling notification
72 | router.notification::(|st, params| st.did_save(params));
73 | router.notification::(|st, params| st.did_open(params));
74 | router.notification::(|st, params| st.did_change(params));
75 | router.notification::(|st, params| st.did_create_files(params));
76 | router.notification::(|st, params| st.did_rename_files(params));
77 | router.notification::(|st, params| st.did_delete_files(params));
78 |
79 | router
80 | }
81 |
82 | pub fn with_report_progress(&self, token: NumberOrString) -> Sender {
83 | let (tx, rx) = mpsc::channel();
84 | let mut socket = self.client.clone();
85 |
86 | thread::spawn(move || {
87 | while let Ok(value) = rx.recv() {
88 | if let Err(e) = socket.progress(ProgressParams {
89 | token: token.clone(),
90 | value,
91 | }) {
92 | tracing::error!(error=%e, "failed to report parse progress");
93 | }
94 | }
95 | });
96 |
97 | tx
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/parser/docsymbol.rs:
--------------------------------------------------------------------------------
1 | use async_lsp::lsp_types::{DocumentSymbol, Range};
2 | use tree_sitter::TreeCursor;
3 |
4 | use crate::{nodekind::NodeKind, utils::ts_to_lsp_position};
5 |
6 | use super::ParsedTree;
7 |
8 | #[derive(Default)]
9 | pub(super) struct DocumentSymbolTreeBuilder {
10 | // The stack are things we're still in the process of building/parsing.
11 | stack: Vec<(usize, DocumentSymbol)>,
12 | // The found are things we've finished processing/parsing, at the top level of the stack.
13 | found: Vec,
14 | }
15 |
16 | impl DocumentSymbolTreeBuilder {
17 | pub(super) fn push(&mut self, node: usize, symbol: DocumentSymbol) {
18 | self.stack.push((node, symbol));
19 | }
20 |
21 | pub(super) fn maybe_pop(&mut self, node: usize) {
22 | let should_pop = self.stack.last().is_some_and(|(n, _)| *n == node);
23 | if should_pop {
24 | let (_, explored) = self.stack.pop().unwrap();
25 | if let Some((_, parent)) = self.stack.last_mut() {
26 | parent.children.as_mut().unwrap().push(explored);
27 | } else {
28 | self.found.push(explored);
29 | }
30 | }
31 | }
32 |
33 | pub(super) fn build(self) -> Vec {
34 | self.found
35 | }
36 | }
37 |
38 | impl ParsedTree {
39 | pub fn find_document_locations(&self, content: impl AsRef<[u8]>) -> Vec {
40 | let mut builder = DocumentSymbolTreeBuilder::default();
41 | let content = content.as_ref();
42 |
43 | let mut cursor = self.tree.root_node().walk();
44 | self.find_document_locations_inner(&mut builder, &mut cursor, content);
45 |
46 | builder.build()
47 | }
48 |
49 | fn find_document_locations_inner(
50 | &self,
51 | builder: &mut DocumentSymbolTreeBuilder,
52 | cursor: &'_ mut TreeCursor,
53 | content: &[u8],
54 | ) {
55 | loop {
56 | let node = cursor.node();
57 |
58 | if NodeKind::is_userdefined(&node) {
59 | let name = node.utf8_text(content).unwrap();
60 | let kind = NodeKind::to_symbolkind(&node);
61 | let detail = self.find_preceding_comments(node.id(), content);
62 | let message = node.parent().unwrap();
63 |
64 | // https://github.com/rust-lang/rust/issues/102777
65 | #[allow(deprecated)]
66 | let new_symbol = DocumentSymbol {
67 | name: name.to_string(),
68 | detail,
69 | kind,
70 | tags: None,
71 | deprecated: None,
72 | range: Range {
73 | start: ts_to_lsp_position(&message.start_position()),
74 | end: ts_to_lsp_position(&message.end_position()),
75 | },
76 | selection_range: Range {
77 | start: ts_to_lsp_position(&node.start_position()),
78 | end: ts_to_lsp_position(&node.end_position()),
79 | },
80 | children: Some(vec![]),
81 | };
82 |
83 | builder.push(message.id(), new_symbol);
84 | }
85 |
86 | if cursor.goto_first_child() {
87 | self.find_document_locations_inner(builder, cursor, content);
88 | builder.maybe_pop(node.id());
89 | cursor.goto_parent();
90 | }
91 |
92 | if !cursor.goto_next_sibling() {
93 | break;
94 | }
95 | }
96 | }
97 | }
98 |
99 | #[cfg(test)]
100 | mod test {
101 | use async_lsp::lsp_types::Url;
102 | use insta::assert_yaml_snapshot;
103 |
104 | use crate::parser::ProtoParser;
105 |
106 | #[test]
107 | #[allow(deprecated)]
108 | fn test_document_symbols() {
109 | let uri: Url = "file://foo/bar/pro.proto".parse().unwrap();
110 | let contents = include_str!("input/test_document_symbols.proto");
111 |
112 | let parsed = ProtoParser::new().parse(uri.clone(), contents);
113 | assert!(parsed.is_some());
114 |
115 | let tree = parsed.unwrap();
116 | assert_yaml_snapshot!(tree.find_document_locations(contents));
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/workspace/definition.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use async_lsp::lsp_types::{Location, Range, Url};
4 |
5 | use crate::{
6 | context::jumpable::Jumpable, state::ProtoLanguageState, utils::split_identifier_package,
7 | };
8 |
9 | impl ProtoLanguageState {
10 | pub fn definition(
11 | &self,
12 | ipath: &[PathBuf],
13 | curr_package: &str,
14 | jump: Jumpable,
15 | ) -> Vec {
16 | match jump {
17 | Jumpable::Import(path) => {
18 | let Some(p) = ipath.iter().map(|p| p.join(&path)).find(|p| p.exists()) else {
19 | return vec![];
20 | };
21 |
22 | let Ok(uri) = Url::from_file_path(p) else {
23 | return vec![];
24 | };
25 |
26 | vec![Location {
27 | uri,
28 | range: Range::default(), // just start of the file
29 | }]
30 | }
31 | Jumpable::Identifier(identifier) => {
32 | let (mut package, identifier) = split_identifier_package(identifier.as_str());
33 | if package.is_empty() {
34 | package = curr_package;
35 | }
36 |
37 | let mut trees = vec![];
38 |
39 | // If package != curr_package, either identifier is from a completely new package
40 | // or relative package from within. As per name resolution first resolve relative
41 | // packages, add all relative trees in search list
42 | if curr_package != package {
43 | let fullpackage = format!("{curr_package}.{package}");
44 | trees.append(&mut self.get_trees_for_package(&fullpackage));
45 | }
46 |
47 | // Add all direct package trees
48 | trees.append(&mut self.get_trees_for_package(package));
49 | trees.into_iter().fold(vec![], |mut v, tree| {
50 | v.extend(tree.definition(identifier, self.get_content(&tree.uri)));
51 | v
52 | })
53 | }
54 | }
55 | }
56 | }
57 |
58 | #[cfg(test)]
59 | mod test {
60 | use crate::context::jumpable::Jumpable;
61 | use std::path::PathBuf;
62 |
63 | use insta::assert_yaml_snapshot;
64 |
65 | use crate::config::Config;
66 | use crate::state::ProtoLanguageState;
67 | #[test]
68 | fn workspace_test_definition() {
69 | let ipath = vec![PathBuf::from("src/workspace/input")];
70 | let a_uri = "file://input/a.proto".parse().unwrap();
71 | let b_uri = "file://input/b.proto".parse().unwrap();
72 | let c_uri = "file://input/c.proto".parse().unwrap();
73 |
74 | let a = include_str!("input/a.proto");
75 | let b = include_str!("input/b.proto");
76 | let c = include_str!("input/c.proto");
77 |
78 | let mut state: ProtoLanguageState = ProtoLanguageState::new();
79 | state.upsert_file(&a_uri, a.to_owned(), &ipath, 2, &Config::default(), false);
80 | state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false);
81 | state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false);
82 |
83 | assert_yaml_snapshot!(state.definition(
84 | &ipath,
85 | "com.workspace",
86 | Jumpable::Identifier("Author".to_owned())
87 | ));
88 | assert_yaml_snapshot!(state.definition(
89 | &ipath,
90 | "com.workspace",
91 | Jumpable::Identifier("Author.Address".to_owned())
92 | ));
93 | assert_yaml_snapshot!(state.definition(
94 | &ipath,
95 | "com.workspace",
96 | Jumpable::Identifier("com.utility.Foobar.Baz".to_owned())
97 | ));
98 | assert_yaml_snapshot!(state.definition(
99 | &ipath,
100 | "com.utility",
101 | Jumpable::Identifier("Baz".to_owned())
102 | ));
103 |
104 | let loc = state.definition(
105 | &[std::env::current_dir().unwrap().join(&ipath[0])],
106 | "com.workspace",
107 | Jumpable::Import("c.proto".to_owned()),
108 | );
109 |
110 | assert_yaml_snapshot!(loc, {"[0].uri" => insta::dynamic_redaction(|c, _| {
111 | assert!(c.as_str().unwrap().ends_with("c.proto"));
112 | "file:///c.proto".to_string()
113 | })});
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use async_lsp::client_monitor::ClientProcessMonitorLayer;
4 | use async_lsp::concurrency::ConcurrencyLayer;
5 | use async_lsp::panic::CatchUnwindLayer;
6 | use async_lsp::server::LifecycleLayer;
7 | use async_lsp::tracing::TracingLayer;
8 | use clap::Parser;
9 | use const_format::concatcp;
10 | use server::{ProtoLanguageServer, TickEvent};
11 | use tower::ServiceBuilder;
12 | use tracing::Level;
13 |
14 | mod config;
15 | mod context;
16 | mod docs;
17 | mod formatter;
18 | mod lsp;
19 | mod nodekind;
20 | mod parser;
21 | mod protoc;
22 | mod server;
23 | mod state;
24 | mod utils;
25 | mod workspace;
26 |
27 | /// Language server for proto3 files
28 | #[derive(Parser, Debug)]
29 | #[command(
30 | author,
31 | version = concatcp!(
32 | env!("CARGO_PKG_VERSION"),
33 | "\n",
34 | BUILD_INFO
35 | ),
36 | about,
37 | long_about = None,
38 | ignore_errors(true)
39 | )]
40 | struct Cli {
41 | /// Include paths for proto files
42 | #[arg(short, long, value_delimiter = ',')]
43 | include_paths: Option>,
44 | }
45 |
46 | const FALLBACK_INCLUDE_PATH: Option<&str> = option_env!("FALLBACK_INCLUDE_PATH");
47 | const BUILD_INFO: &str = concatcp!(
48 | "fallback include path: ",
49 | match FALLBACK_INCLUDE_PATH {
50 | Some(path) => path,
51 | None => "not set",
52 | }
53 | );
54 |
55 | #[tokio::main(flavor = "current_thread")]
56 | async fn main() {
57 | let cli = Cli::parse();
58 |
59 | let dir = std::env::temp_dir();
60 | eprintln!("file logging at directory: {dir:?}");
61 |
62 | let file_appender = tracing_appender::rolling::daily(dir.clone(), "protols.log");
63 | let file_appender = tracing_appender::non_blocking(file_appender);
64 |
65 | tracing_subscriber::fmt()
66 | .with_max_level(Level::INFO)
67 | .with_ansi(false)
68 | .with_writer(file_appender.0)
69 | .init();
70 |
71 | let fallback_include_path = FALLBACK_INCLUDE_PATH.map(Into::into);
72 |
73 | tracing::info!("server version: {}", env!("CARGO_PKG_VERSION"));
74 | let (server, _) = async_lsp::MainLoop::new_server(|client| {
75 | tracing::info!("Using CLI options: {:?}", cli);
76 | tracing::info!("Using fallback include path: {:?}", fallback_include_path);
77 | let router = ProtoLanguageServer::new_router(
78 | client.clone(),
79 | cli.include_paths
80 | .map(|ic| ic.into_iter().map(std::path::PathBuf::from).collect())
81 | .unwrap_or_default(),
82 | fallback_include_path,
83 | );
84 |
85 | tokio::spawn({
86 | let client = client.clone();
87 | async move {
88 | let mut interval = tokio::time::interval(Duration::from_secs(1));
89 | loop {
90 | interval.tick().await;
91 | if client.emit(TickEvent).is_err() {
92 | break;
93 | }
94 | }
95 | }
96 | });
97 |
98 | ServiceBuilder::new()
99 | .layer(TracingLayer::default())
100 | .layer(LifecycleLayer::default())
101 | .layer(CatchUnwindLayer::default())
102 | .layer(ConcurrencyLayer::default())
103 | .layer(ClientProcessMonitorLayer::new(client.clone()))
104 | .service(router)
105 | });
106 |
107 | // Prefer truly asynchronous piped stdin/stdout without blocking tasks.
108 | #[cfg(unix)]
109 | let (stdin, stdout) = (
110 | async_lsp::stdio::PipeStdin::lock_tokio().unwrap(),
111 | async_lsp::stdio::PipeStdout::lock_tokio().unwrap(),
112 | );
113 | // Fallback to spawn blocking read/write otherwise.
114 | #[cfg(not(unix))]
115 | let (stdin, stdout) = (
116 | tokio_util::compat::TokioAsyncReadCompatExt::compat(tokio::io::stdin()),
117 | tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(tokio::io::stdout()),
118 | );
119 |
120 | server.run_buffered(stdin, stdout).await.unwrap();
121 | }
122 |
123 | #[cfg(test)]
124 | mod tests {
125 | use super::*;
126 |
127 | #[test]
128 | fn test_cli_parsing() {
129 | // Test with no arguments
130 | let args = vec!["protols"];
131 | let cli = Cli::parse_from(args);
132 | assert!(cli.include_paths.is_none());
133 |
134 | // Test with include paths
135 | let args = vec!["protols", "--include-paths=/path1,/path2"];
136 | let cli = Cli::parse_from(args);
137 | assert!(cli.include_paths.is_some());
138 | let paths = cli.include_paths.unwrap();
139 | assert_eq!(paths.len(), 2);
140 | assert_eq!(paths[0], "/path1");
141 | assert_eq!(paths[1], "/path2");
142 |
143 | // Test with short form
144 | let args = vec!["protols", "-i", "/path1,/path2"];
145 | let cli = Cli::parse_from(args);
146 | assert!(cli.include_paths.is_some());
147 | let paths = cli.include_paths.unwrap();
148 | assert_eq!(paths.len(), 2);
149 | assert_eq!(paths[0], "/path1");
150 | assert_eq!(paths[1], "/path2");
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/formatter/clang.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::needless_late_init)]
2 | use std::{
3 | borrow::Cow,
4 | fs::File,
5 | io::Write,
6 | path::{Path, PathBuf},
7 | process::Command,
8 | };
9 |
10 | use async_lsp::lsp_types::{Position, Range, TextEdit};
11 | use hard_xml::XmlRead;
12 | use serde::Serialize;
13 | use tempfile::{TempDir, tempdir};
14 |
15 | use super::ProtoFormatter;
16 |
17 | pub struct ClangFormatter {
18 | pub path: String,
19 | working_dir: Option,
20 | temp_dir: TempDir,
21 | }
22 |
23 | #[derive(XmlRead, Serialize, PartialEq, Debug)]
24 | #[xml(tag = "replacements")]
25 | struct Replacements<'a> {
26 | #[xml(child = "replacement")]
27 | replacements: Vec>,
28 | }
29 |
30 | #[derive(XmlRead, Serialize, PartialEq, Debug)]
31 | #[xml(tag = "replacement")]
32 | struct Replacement<'a> {
33 | #[xml(attr = "offset")]
34 | offset: usize,
35 | #[xml(attr = "length")]
36 | length: usize,
37 | #[xml(text)]
38 | text: Cow<'a, str>,
39 | }
40 |
41 | impl Replacement<'_> {
42 | fn offset_to_position(offset: usize, content: &str) -> Option {
43 | if offset > content.len() {
44 | return None;
45 | }
46 | let up_to_offset = &content[..offset];
47 | let line = up_to_offset.matches('\n').count();
48 | let last_newline = up_to_offset.rfind('\n').map_or(0, |pos| pos + 1);
49 | let character = offset - last_newline;
50 |
51 | Some(Position {
52 | line: line as u32,
53 | character: character as u32,
54 | })
55 | }
56 |
57 | fn as_text_edit(&self, content: &str) -> Option {
58 | Some(TextEdit {
59 | range: Range {
60 | start: Self::offset_to_position(self.offset, content)?,
61 | end: Self::offset_to_position(self.offset + self.length, content)?,
62 | },
63 | new_text: self.text.to_string(),
64 | })
65 | }
66 | }
67 |
68 | impl ClangFormatter {
69 | pub fn new(cmd: &str, wdir: Option<&str>) -> Self {
70 | Self {
71 | temp_dir: tempdir().expect("faile to creat temp dir"),
72 | path: cmd.to_owned(),
73 | working_dir: wdir.map(ToOwned::to_owned),
74 | }
75 | }
76 |
77 | fn get_temp_file_path(&self, content: &str) -> Option {
78 | let p = self.temp_dir.path().join("format-temp.proto");
79 | let mut file = File::create(p.clone()).ok()?;
80 | file.write_all(content.as_ref()).ok()?;
81 | Some(p)
82 | }
83 |
84 | fn get_command(&self, f: &str, u: &Path) -> Option {
85 | let mut c = Command::new(self.path.as_str());
86 | if let Some(wd) = &self.working_dir {
87 | c.current_dir(wd.as_str());
88 | }
89 | c.stdin(File::open(u).ok()?);
90 | c.args([
91 | "--output-replacements-xml",
92 | format!("--assume-filename={f}").as_str(),
93 | ]);
94 | Some(c)
95 | }
96 |
97 | fn output_to_textedit(&self, output: &str, content: &str) -> Option> {
98 | let r = Replacements::from_str(output).ok()?;
99 | let edits = r
100 | .replacements
101 | .into_iter()
102 | .filter_map(|r| r.as_text_edit(content.as_ref()))
103 | .collect();
104 |
105 | Some(edits)
106 | }
107 | }
108 |
109 | impl ProtoFormatter for ClangFormatter {
110 | fn format_document(&self, filename: &str, content: &str) -> Option> {
111 | let p = self.get_temp_file_path(content)?;
112 | let mut cmd = self.get_command(filename, p.as_ref())?;
113 | let output = cmd.output().ok()?;
114 | if !output.status.success() {
115 | tracing::error!(
116 | status = output.status.code(),
117 | "failed to execute clang-format"
118 | );
119 | return None;
120 | }
121 | self.output_to_textedit(&String::from_utf8_lossy(&output.stdout), content)
122 | }
123 |
124 | fn format_document_range(
125 | &self,
126 | r: &Range,
127 | filename: &str,
128 | content: &str,
129 | ) -> Option> {
130 | let p = self.get_temp_file_path(content)?;
131 | let start = r.start.line + 1;
132 | let end = r.end.line + 1;
133 | let output = self
134 | .get_command(filename, p.as_ref())?
135 | .args(["--lines", format!("{start}:{end}").as_str()])
136 | .output()
137 | .ok()?;
138 |
139 | if !output.status.success() {
140 | tracing::error!(
141 | status = output.status.code(),
142 | "failed to execute clang-format"
143 | );
144 | return None;
145 | }
146 | self.output_to_textedit(&String::from_utf8_lossy(&output.stdout), content)
147 | }
148 | }
149 |
150 | #[cfg(test)]
151 | mod test {
152 | use hard_xml::XmlRead;
153 | use insta::{assert_yaml_snapshot, with_settings};
154 |
155 | use super::{Replacement, Replacements};
156 |
157 | #[test]
158 | fn test_reading_xml() {
159 | let c = include_str!("input/replacement.xml");
160 | let r = Replacements::from_str(c).unwrap();
161 | assert_yaml_snapshot!(r);
162 | }
163 |
164 | #[test]
165 | fn test_reading_empty_xml() {
166 | let c = include_str!("input/empty.xml");
167 | let r = Replacements::from_str(c).unwrap();
168 | assert_yaml_snapshot!(r);
169 | }
170 |
171 | #[test]
172 | fn test_offset_to_position() {
173 | let c = include_str!("input/test.proto");
174 | let pos = vec![0, 4, 22, 999];
175 | for i in pos {
176 | with_settings!({description => c, info => &i}, {
177 | assert_yaml_snapshot!(Replacement::offset_to_position(i, c));
178 | })
179 | }
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/workspace/hover.rs:
--------------------------------------------------------------------------------
1 | use std::path::{Path, PathBuf};
2 |
3 | use async_lsp::lsp_types::{MarkupContent, MarkupKind};
4 |
5 | use crate::{
6 | context::hoverable::Hoverables, docs, state::ProtoLanguageState,
7 | utils::split_identifier_package,
8 | };
9 |
10 | fn format_import_path_hover_text(path: &str, p: &Path) -> String {
11 | format!(
12 | r#"Import: `{path}` protobuf file,
13 | ---
14 | Included from {}"#,
15 | p.to_string_lossy()
16 | )
17 | }
18 |
19 | fn format_identifier_hover_text(identifier: &str, package: &str, result: &str) -> String {
20 | format!(
21 | r#"`{identifier}` message or enum type, package: `{package}`
22 | ---
23 | {result}"#
24 | )
25 | }
26 |
27 | impl ProtoLanguageState {
28 | pub fn hover(
29 | &self,
30 | ipath: &[PathBuf],
31 | curr_package: &str,
32 | hv: Hoverables,
33 | ) -> Option {
34 | let v = match hv {
35 | Hoverables::FieldType(field) => docs::BUITIN
36 | .get(field.as_str())
37 | .map(ToString::to_string)
38 | .unwrap_or_default(),
39 |
40 | Hoverables::ImportPath(path) => ipath
41 | .iter()
42 | .map(|p| p.join(&path))
43 | .find(|p| p.exists())
44 | .map(|p| format_import_path_hover_text(&path, &p))
45 | .unwrap_or_default(),
46 |
47 | Hoverables::Identifier(identifier) => {
48 | let (mut package, identifier) = split_identifier_package(identifier.as_str());
49 | if package.is_empty() {
50 | package = curr_package;
51 | }
52 |
53 | // Identifier is user defined type or well known type
54 |
55 | // If well known types, check in wellknown docs,
56 | // otherwise check in trees
57 | match docs::WELLKNOWN
58 | .get(format!("{package}.{identifier}").as_str())
59 | .map(|&s| s.to_string())
60 | {
61 | Some(res) => res,
62 | None => {
63 | let mut trees = vec![];
64 |
65 | // If package != curr_package, either identifier is from a completely new package
66 | // or relative package from within. As per name resolution first resolve relative
67 | // packages, add all relative trees in search list
68 | if curr_package != package {
69 | let fullpackage = format!("{curr_package}.{package}");
70 | trees.append(&mut self.get_trees_for_package(&fullpackage));
71 | }
72 |
73 | // Add all direct package trees
74 | trees.append(&mut self.get_trees_for_package(package));
75 |
76 | // Find the first field hovered in the trees
77 | let res = trees.iter().find_map(|tree| {
78 | let content = self.get_content(&tree.uri);
79 | let res = tree.hover(identifier, content);
80 | if res.is_empty() {
81 | None
82 | } else {
83 | Some(res[0].clone())
84 | }
85 | });
86 |
87 | // Format the hover text and return
88 | // TODO: package here is literally what was hovered, incase of
89 | // relative it is only the relative part, should be full path, should
90 | // probably figure out the package from the tree which provides hover and
91 | // pass here.
92 | res.map(|r| format_identifier_hover_text(identifier, package, &r))
93 | .unwrap_or_default()
94 | }
95 | }
96 | }
97 | };
98 |
99 | match v {
100 | v if v.is_empty() => None,
101 | v => Some(MarkupContent {
102 | kind: MarkupKind::Markdown,
103 | value: v,
104 | }),
105 | }
106 | }
107 | }
108 |
109 | #[cfg(test)]
110 | mod test {
111 | use insta::assert_yaml_snapshot;
112 |
113 | use crate::config::Config;
114 | use crate::context::hoverable::Hoverables;
115 | use crate::state::ProtoLanguageState;
116 | #[test]
117 | fn workspace_test_hover() {
118 | let ipath = vec![std::env::current_dir().unwrap().join("src/workspace/input")];
119 | let a_uri = "file://input/a.proto".parse().unwrap();
120 | let b_uri = "file://input/b.proto".parse().unwrap();
121 | let c_uri = "file://input/c.proto".parse().unwrap();
122 |
123 | let a = include_str!("input/a.proto");
124 | let b = include_str!("input/b.proto");
125 | let c = include_str!("input/c.proto");
126 |
127 | let mut state: ProtoLanguageState = ProtoLanguageState::new();
128 | state.upsert_file(&a_uri, a.to_owned(), &ipath, 3, &Config::default(), false);
129 | state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false);
130 | state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false);
131 |
132 | assert_yaml_snapshot!(state.hover(
133 | &ipath,
134 | "com.workspace",
135 | Hoverables::Identifier("google.protobuf.Any".to_string())
136 | ));
137 | assert_yaml_snapshot!(state.hover(
138 | &ipath,
139 | "com.workspace",
140 | Hoverables::Identifier("Author".to_string())
141 | ));
142 | assert_yaml_snapshot!(state.hover(
143 | &ipath,
144 | "com.workspace",
145 | Hoverables::FieldType("int64".to_string())
146 | ));
147 | assert_yaml_snapshot!(state.hover(
148 | &ipath,
149 | "com.workspace",
150 | Hoverables::Identifier("Author.Address".to_string())
151 | ));
152 | assert_yaml_snapshot!(state.hover(
153 | &ipath,
154 | "com.workspace",
155 | Hoverables::Identifier("com.utility.Foobar.Baz".to_string())
156 | ));
157 | assert_yaml_snapshot!(state.hover(
158 | &ipath,
159 | "com.utility",
160 | Hoverables::Identifier("Baz".to_string())
161 | ));
162 | assert_yaml_snapshot!(state.hover(
163 | &ipath,
164 | "com.workspace",
165 | Hoverables::Identifier("com.inner.Why".to_string())
166 | ));
167 | assert_yaml_snapshot!(state.hover(
168 | &ipath,
169 | "com.inner",
170 | Hoverables::Identifier(".com.inner.secret.SomeSecret".to_string())
171 | ));
172 | // relative path hover
173 | assert_yaml_snapshot!(state.hover(
174 | &ipath,
175 | "com.inner",
176 | Hoverables::Identifier("secret.SomeSecret".to_string())
177 | ))
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/parser/tree.rs:
--------------------------------------------------------------------------------
1 | use async_lsp::lsp_types::{Position, Range};
2 | use tree_sitter::{Node, TreeCursor};
3 |
4 | use crate::{
5 | context::{hoverable::Hoverables, jumpable::Jumpable},
6 | nodekind::NodeKind,
7 | utils::{lsp_to_ts_point, ts_to_lsp_position},
8 | };
9 |
10 | use super::ParsedTree;
11 |
12 | impl ParsedTree {
13 | pub(super) fn walk_and_filter<'a>(
14 | cursor: &mut TreeCursor<'a>,
15 | f: fn(&Node) -> bool,
16 | early: bool,
17 | ) -> Vec> {
18 | let mut v = vec![];
19 |
20 | loop {
21 | let node = cursor.node();
22 |
23 | if f(&node) {
24 | v.push(node);
25 | if early {
26 | break;
27 | }
28 | }
29 |
30 | if cursor.goto_first_child() {
31 | v.extend(Self::walk_and_filter(cursor, f, early));
32 | cursor.goto_parent();
33 | }
34 |
35 | if !cursor.goto_next_sibling() {
36 | break;
37 | }
38 | }
39 |
40 | v
41 | }
42 |
43 | pub(super) fn advance_cursor_to(cursor: &mut TreeCursor<'_>, nid: usize) -> bool {
44 | loop {
45 | let node = cursor.node();
46 | if node.id() == nid {
47 | return true;
48 | }
49 | if cursor.goto_first_child() {
50 | if Self::advance_cursor_to(cursor, nid) {
51 | return true;
52 | }
53 | cursor.goto_parent();
54 | }
55 | if !cursor.goto_next_sibling() {
56 | return false;
57 | }
58 | }
59 | }
60 |
61 | pub fn get_user_defined_text<'a>(
62 | &'a self,
63 | pos: &Position,
64 | content: &'a [u8],
65 | ) -> Option<&'a str> {
66 | self.get_user_defined_node(pos)
67 | .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error"))
68 | }
69 |
70 | pub fn get_jumpable_at_position(&self, pos: &Position, content: &[u8]) -> Option {
71 | let n = self.get_node_at_position(pos)?;
72 |
73 | // If node is import path. return the whole path, removing the quotes
74 | if n.parent().filter(NodeKind::is_import_path).is_some() {
75 | return Some(Jumpable::Import(
76 | n.utf8_text(content)
77 | .expect("utf-8 parse error")
78 | .trim_matches('"')
79 | .to_string(),
80 | ));
81 | }
82 |
83 | // If node is user defined enum/message
84 | if let Some(identifier) = self.get_user_defined_text(pos, content) {
85 | return Some(Jumpable::Identifier(identifier.to_string()));
86 | }
87 |
88 | None
89 | }
90 |
91 | pub fn get_hoverable_at_position<'a>(
92 | &'a self,
93 | pos: &Position,
94 | content: &'a [u8],
95 | ) -> Option {
96 | let n = self.get_node_at_position(pos)?;
97 |
98 | // If node is import path. return the whole path, removing the quotes
99 | if n.parent().filter(NodeKind::is_import_path).is_some() {
100 | return Some(Hoverables::ImportPath(
101 | n.utf8_text(content)
102 | .expect("utf-8 parse error")
103 | .trim_matches('"')
104 | .to_string(),
105 | ));
106 | }
107 |
108 | // If node is user defined enum/message
109 | if let Some(identifier) = self.get_user_defined_text(pos, content) {
110 | return Some(Hoverables::Identifier(identifier.to_string()));
111 | }
112 |
113 | // Lastly; fallback to either wellknown or builtin types
114 | Some(Hoverables::FieldType(n.kind().to_string()))
115 | }
116 |
117 | pub fn get_ancestor_nodes_at_position<'a>(&'a self, pos: &Position) -> Vec> {
118 | let Some(mut n) = self.get_user_defined_node(pos) else {
119 | return vec![];
120 | };
121 |
122 | let mut nodes = vec![];
123 | while let Some(p) = n.parent() {
124 | if NodeKind::is_message(&p) {
125 | for i in 0..p.child_count() {
126 | let t = p.child(i).unwrap();
127 | if NodeKind::is_message_name(&t) {
128 | nodes.push(t);
129 | }
130 | }
131 | }
132 | n = p;
133 | }
134 | nodes
135 | }
136 |
137 | pub fn get_user_defined_node<'a>(&'a self, pos: &Position) -> Option> {
138 | self.get_node_at_position(pos)
139 | .map(|n| {
140 | if NodeKind::is_actionable(&n) {
141 | n
142 | } else {
143 | n.parent().unwrap()
144 | }
145 | })
146 | .filter(NodeKind::is_actionable)
147 | }
148 |
149 | pub fn get_node_at_position<'a>(&'a self, pos: &Position) -> Option> {
150 | let pos = lsp_to_ts_point(pos);
151 | self.tree.root_node().descendant_for_point_range(pos, pos)
152 | }
153 |
154 | pub fn find_all_nodes(&self, f: fn(&Node) -> bool) -> Vec> {
155 | self.find_all_nodes_from(self.tree.root_node(), f)
156 | }
157 |
158 | pub fn find_all_nodes_from<'a>(&self, n: Node<'a>, f: fn(&Node) -> bool) -> Vec> {
159 | let mut cursor = n.walk();
160 | Self::walk_and_filter(&mut cursor, f, false)
161 | }
162 |
163 | pub fn find_first_node(&self, f: fn(&Node) -> bool) -> Vec> {
164 | self.find_node_from(self.tree.root_node(), f)
165 | }
166 |
167 | pub fn find_node_from<'a>(&self, n: Node<'a>, f: fn(&Node) -> bool) -> Vec> {
168 | let mut cursor = n.walk();
169 | Self::walk_and_filter(&mut cursor, f, true)
170 | }
171 |
172 | pub fn get_package_name<'a>(&self, content: &'a [u8]) -> Option<&'a str> {
173 | self.find_first_node(NodeKind::is_package_name)
174 | .first()
175 | .map(|n| n.utf8_text(content).expect("utf-8 parse error"))
176 | }
177 |
178 | pub fn get_import_node(&self) -> Vec> {
179 | self.find_all_nodes(NodeKind::is_import_path)
180 | .into_iter()
181 | .filter_map(|n| n.child_by_field_name("path"))
182 | .collect()
183 | }
184 |
185 | pub fn get_import_paths<'a>(&self, content: &'a [u8]) -> Vec<&'a str> {
186 | self.get_import_node()
187 | .into_iter()
188 | .map(|n| {
189 | n.utf8_text(content)
190 | .expect("utf-8 parse error")
191 | .trim_matches('"')
192 | })
193 | .collect()
194 | }
195 |
196 | pub fn get_import_path_range(&self, content: &[u8], import: Vec) -> Vec {
197 | self.get_import_node()
198 | .into_iter()
199 | .filter(|n| {
200 | let t = n
201 | .utf8_text(content)
202 | .expect("utf8-parse error")
203 | .trim_matches('"');
204 | import.iter().any(|i| i == t)
205 | })
206 | .map(|n| Range {
207 | start: ts_to_lsp_position(&n.start_position()),
208 | end: ts_to_lsp_position(&n.end_position()),
209 | })
210 | .collect()
211 | }
212 | }
213 |
214 | #[cfg(test)]
215 | mod test {
216 | use async_lsp::lsp_types::Url;
217 | use insta::assert_yaml_snapshot;
218 |
219 | use crate::{nodekind::NodeKind, parser::ProtoParser};
220 |
221 | #[test]
222 | fn test_filter() {
223 | let uri: Url = "file://foo/bar/test.proto".parse().unwrap();
224 | let contents = include_str!("input/test_filter.proto");
225 | let parsed = ProtoParser::new().parse(uri, contents);
226 |
227 | assert!(parsed.is_some());
228 | let tree = parsed.unwrap();
229 | let nodes = tree.find_all_nodes(NodeKind::is_message_name);
230 |
231 | assert_eq!(nodes.len(), 2);
232 |
233 | let names: Vec<_> = nodes
234 | .into_iter()
235 | .map(|n| n.utf8_text(contents.as_ref()).unwrap())
236 | .collect();
237 |
238 | assert_yaml_snapshot!(names);
239 |
240 | let package_name = tree.get_package_name(contents.as_ref());
241 | assert_yaml_snapshot!(package_name);
242 | let imports = tree.get_import_paths(contents.as_ref());
243 | assert_yaml_snapshot!(imports);
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/src/workspace/rename.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::split_identifier_package;
2 | use std::collections::HashMap;
3 | use std::path::PathBuf;
4 |
5 | use async_lsp::lsp_types::{Location, TextEdit, Url};
6 |
7 | use crate::state::ProtoLanguageState;
8 | use async_lsp::lsp_types::ProgressParamsValue;
9 | use std::sync::mpsc::Sender;
10 |
11 | impl ProtoLanguageState {
12 | pub fn rename_fields(
13 | &mut self,
14 | current_package: &str,
15 | identifier: &str,
16 | new_text: &str,
17 | workspace: PathBuf,
18 | progress_sender: Option>,
19 | ) -> HashMap> {
20 | self.parse_all_from_workspace(workspace, progress_sender);
21 | let (_, identifier) = split_identifier_package(identifier);
22 | self.get_trees()
23 | .into_iter()
24 | .fold(HashMap::new(), |mut h, tree| {
25 | let content = self.get_content(&tree.uri);
26 | let package = tree.get_package_name(content.as_ref()).unwrap_or(".");
27 | let mut old = identifier.to_string();
28 | let mut new = new_text.to_string();
29 | let mut v = vec![];
30 |
31 | // Global scope: Reference by only . or within global directly
32 | if current_package == "." {
33 | if package == "." {
34 | v.extend(tree.rename_field(&old, &new, content.as_str()));
35 | }
36 |
37 | old = format!(".{old}");
38 | new = format!(".{new}");
39 |
40 | v.extend(tree.rename_field(&old, &new, content.as_str()));
41 |
42 | if !v.is_empty() {
43 | h.insert(tree.uri.clone(), v);
44 | }
45 | return h;
46 | }
47 |
48 | let full_old = format!("{current_package}.{old}");
49 | let full_new = format!("{current_package}.{new}");
50 | let global_full_old = format!(".{current_package}.{old}");
51 | let global_full_new = format!(".{current_package}.{new}");
52 |
53 | // Current package: Reference by full or relative name or directly
54 | if current_package == package {
55 | v.extend(tree.rename_field(&old, &new, content.as_str()));
56 | } else if current_package.starts_with(package) {
57 | // Safety: prefix check already done
58 | // get the relative part of the package
59 | let packagepart = current_package
60 | .strip_prefix(package)
61 | .unwrap()
62 | .trim_start_matches('.');
63 | let relative_old = format!("{packagepart}.{old}");
64 | let relative_new = format!("{packagepart}.{new}");
65 | v.extend(tree.rename_field(&relative_old, &relative_new, content.as_str()));
66 | }
67 |
68 | // Otherwise, full reference
69 | v.extend(tree.rename_field(&full_old, &full_new, content.as_str()));
70 | v.extend(tree.rename_field(&global_full_old, &global_full_new, content.as_str()));
71 |
72 | if !v.is_empty() {
73 | h.insert(tree.uri.clone(), v);
74 | }
75 | h
76 | })
77 | }
78 |
79 | pub fn reference_fields(
80 | &mut self,
81 | current_package: &str,
82 | identifier: &str,
83 | workspace: PathBuf,
84 | progress_sender: Option>,
85 | ) -> Option> {
86 | self.parse_all_from_workspace(workspace, progress_sender);
87 | let (_, identifier) = split_identifier_package(identifier);
88 | let r = self
89 | .get_trees()
90 | .into_iter()
91 | .fold(Vec::::new(), |mut v, tree| {
92 | let content = self.get_content(&tree.uri);
93 | let package = tree.get_package_name(content.as_ref()).unwrap_or(".");
94 | let mut ident = identifier.to_owned();
95 | // Global scope: Reference by only . or within global directly
96 | if current_package == "." {
97 | if package == "." {
98 | v.extend(tree.reference_field(&ident, content.as_str()));
99 | }
100 |
101 | ident = format!(".{ident}");
102 | v.extend(tree.reference_field(&ident, content.as_str()));
103 |
104 | return v;
105 | }
106 |
107 | let full_ident = format!("{current_package}.{ident}");
108 | let global_full_ident = format!(".{current_package}.{ident}");
109 |
110 | // Current package: Reference by full or relative name or directly
111 | if current_package == package {
112 | v.extend(tree.reference_field(&ident, content.as_str()));
113 | } else if current_package.starts_with(package) {
114 | // Safety: prefix check already done
115 | // get the relative part of the package
116 | let packagepart = current_package
117 | .strip_prefix(package)
118 | .unwrap()
119 | .trim_start_matches('.');
120 | let relative = format!("{packagepart}.{ident}");
121 | v.extend(tree.reference_field(&relative, content.as_str()));
122 | }
123 |
124 | // Otherwise, full reference
125 | v.extend(tree.reference_field(&full_ident, content.as_str()));
126 | v.extend(tree.reference_field(&global_full_ident, content.as_str()));
127 | v
128 | });
129 | if r.is_empty() { None } else { Some(r) }
130 | }
131 | }
132 |
133 | #[cfg(test)]
134 | mod test {
135 | use std::path::PathBuf;
136 |
137 | use insta::assert_yaml_snapshot;
138 |
139 | use crate::config::Config;
140 | use crate::state::ProtoLanguageState;
141 |
142 | #[test]
143 | fn test_rename() {
144 | let ipath = vec![PathBuf::from("src/workspace/input")];
145 | let a_uri = "file://input/a.proto".parse().unwrap();
146 | let b_uri = "file://input/b.proto".parse().unwrap();
147 | let c_uri = "file://input/c.proto".parse().unwrap();
148 |
149 | let a = include_str!("input/a.proto");
150 | let b = include_str!("input/b.proto");
151 | let c = include_str!("input/c.proto");
152 |
153 | let mut state: ProtoLanguageState = ProtoLanguageState::new();
154 | state.upsert_file(&a_uri, a.to_owned(), &ipath, 2, &Config::default(), false);
155 | state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false);
156 | state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false);
157 |
158 | assert_yaml_snapshot!(state.rename_fields(
159 | "com.workspace",
160 | "Author",
161 | "Writer",
162 | PathBuf::from("src/workspace/input"),
163 | None
164 | ));
165 | assert_yaml_snapshot!(state.rename_fields(
166 | "com.workspace",
167 | "Author.Address",
168 | "Author.Location",
169 | PathBuf::from("src/workspace/input"),
170 | None
171 | ));
172 | assert_yaml_snapshot!(state.rename_fields(
173 | "com.utility",
174 | "Foobar.Baz",
175 | "Foobar.Baaz",
176 | PathBuf::from("src/workspace/input"),
177 | None
178 | ));
179 | }
180 |
181 | #[test]
182 | fn test_reference() {
183 | let ipath = vec![PathBuf::from("src/workspace/input")];
184 | let a_uri = "file://input/a.proto".parse().unwrap();
185 | let b_uri = "file://input/b.proto".parse().unwrap();
186 | let c_uri = "file://input/c.proto".parse().unwrap();
187 |
188 | let a = include_str!("input/a.proto");
189 | let b = include_str!("input/b.proto");
190 | let c = include_str!("input/c.proto");
191 |
192 | let mut state: ProtoLanguageState = ProtoLanguageState::new();
193 | state.upsert_file(&a_uri, a.to_owned(), &ipath, 2, &Config::default(), false);
194 | state.upsert_file(&b_uri, b.to_owned(), &ipath, 2, &Config::default(), false);
195 | state.upsert_file(&c_uri, c.to_owned(), &ipath, 2, &Config::default(), false);
196 |
197 | assert_yaml_snapshot!(state.reference_fields(
198 | "com.workspace",
199 | "Author",
200 | PathBuf::from("src/workspace/input"),
201 | None
202 | ));
203 | assert_yaml_snapshot!(state.reference_fields(
204 | "com.workspace",
205 | "Author.Address",
206 | PathBuf::from("src/workspace/input"),
207 | None
208 | ));
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/src/parser/rename.rs:
--------------------------------------------------------------------------------
1 | use async_lsp::lsp_types::{Location, Position, Range, TextEdit};
2 | use tree_sitter::Node;
3 |
4 | use crate::{nodekind::NodeKind, utils::ts_to_lsp_position};
5 |
6 | use super::ParsedTree;
7 |
8 | impl ParsedTree {
9 | pub fn can_rename(&self, pos: &Position) -> Option {
10 | self.get_node_at_position(pos)
11 | .filter(NodeKind::is_identifier)
12 | .and_then(|n| {
13 | if n.parent().is_some() && NodeKind::is_userdefined(&n.parent().unwrap()) {
14 | Some(Range {
15 | start: ts_to_lsp_position(&n.start_position()),
16 | end: ts_to_lsp_position(&n.end_position()),
17 | })
18 | } else {
19 | None
20 | }
21 | })
22 | }
23 |
24 | fn nodes_within<'a>(
25 | &self,
26 | n: Node<'a>,
27 | identifier: &str,
28 | content: impl AsRef<[u8]>,
29 | ) -> Option>> {
30 | n.parent().map(|p| {
31 | self.find_all_nodes_from(p, NodeKind::is_field_name)
32 | .into_iter()
33 | .filter(|i| i.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier)
34 | .collect()
35 | })
36 | }
37 |
38 | pub fn reference_tree(
39 | &self,
40 | pos: &Position,
41 | content: impl AsRef<[u8]>,
42 | ) -> Option<(Vec, String)> {
43 | let rename_range = self.can_rename(pos)?;
44 |
45 | let mut res = vec![Location {
46 | uri: self.uri.clone(),
47 | range: rename_range,
48 | }];
49 |
50 | let nodes = self.get_ancestor_nodes_at_position(pos);
51 | let mut i = 1;
52 | let mut otext = nodes.first()?.utf8_text(content.as_ref()).ok()?.to_owned();
53 | while nodes.len() > i {
54 | let id = nodes[i].utf8_text(content.as_ref()).ok()?;
55 | if let Some(inodes) = self.nodes_within(nodes[i], &otext, content.as_ref()) {
56 | res.extend(inodes.into_iter().map(|n| Location {
57 | uri: self.uri.clone(),
58 | range: Range {
59 | start: ts_to_lsp_position(&n.start_position()),
60 | end: ts_to_lsp_position(&n.end_position()),
61 | },
62 | }))
63 | }
64 | otext = format!("{id}.{otext}");
65 | i += 1
66 | }
67 | Some((res, otext))
68 | }
69 |
70 | pub fn rename_tree(
71 | &self,
72 | pos: &Position,
73 | new_name: &str,
74 | content: impl AsRef<[u8]>,
75 | ) -> Option<(Vec, String, String)> {
76 | let rename_range = self.can_rename(pos)?;
77 |
78 | let mut v = vec![TextEdit {
79 | range: rename_range,
80 | new_text: new_name.to_owned(),
81 | }];
82 |
83 | let nodes = self.get_ancestor_nodes_at_position(pos);
84 |
85 | let mut i = 1;
86 | let mut otext = nodes.first()?.utf8_text(content.as_ref()).ok()?.to_owned();
87 | let mut ntext = new_name.to_owned();
88 |
89 | while nodes.len() > i {
90 | let id = nodes[i].utf8_text(content.as_ref()).ok()?;
91 |
92 | if let Some(inodes) = self.nodes_within(nodes[i], &otext, content.as_ref()) {
93 | v.extend(inodes.into_iter().map(|n| TextEdit {
94 | range: Range {
95 | start: ts_to_lsp_position(&n.start_position()),
96 | end: ts_to_lsp_position(&n.end_position()),
97 | },
98 | new_text: ntext.to_owned(),
99 | }));
100 | }
101 |
102 | otext = format!("{id}.{otext}");
103 | ntext = format!("{id}.{ntext}");
104 |
105 | i += 1
106 | }
107 |
108 | Some((v, otext, ntext))
109 | }
110 |
111 | pub fn rename_field(
112 | &self,
113 | old_identifier: &str,
114 | new_identifier: &str,
115 | content: impl AsRef<[u8]>,
116 | ) -> Vec {
117 | self.find_all_nodes(NodeKind::is_field_name)
118 | .into_iter()
119 | .filter(|n| {
120 | let ntext = n.utf8_text(content.as_ref()).expect("utf-8 parse error");
121 | let sc = format!("{old_identifier}.");
122 | ntext == old_identifier || ntext.starts_with(&sc)
123 | })
124 | .map(|n| {
125 | let text = n.utf8_text(content.as_ref()).expect("utf-8 parse error");
126 | TextEdit {
127 | new_text: text.replace(old_identifier, new_identifier),
128 | range: Range {
129 | start: ts_to_lsp_position(&n.start_position()),
130 | end: ts_to_lsp_position(&n.end_position()),
131 | },
132 | }
133 | })
134 | .collect()
135 | }
136 |
137 | pub fn reference_field(&self, id: &str, content: impl AsRef<[u8]>) -> Vec {
138 | self.find_all_nodes(NodeKind::is_field_name)
139 | .into_iter()
140 | .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == id)
141 | .map(|n| Location {
142 | uri: self.uri.clone(),
143 | range: Range {
144 | start: ts_to_lsp_position(&n.start_position()),
145 | end: ts_to_lsp_position(&n.end_position()),
146 | },
147 | })
148 | .collect()
149 | }
150 | }
151 |
152 | #[cfg(test)]
153 | mod test {
154 | use async_lsp::lsp_types::{Position, Url};
155 | use insta::assert_yaml_snapshot;
156 |
157 | use crate::parser::ProtoParser;
158 |
159 | #[test]
160 | fn test_rename() {
161 | let uri: Url = "file://foo/bar.proto".parse().unwrap();
162 | let pos_book = Position {
163 | line: 5,
164 | character: 9,
165 | };
166 | let pos_author = Position {
167 | line: 11,
168 | character: 14,
169 | };
170 | let pos_non_rename = Position {
171 | line: 21,
172 | character: 5,
173 | };
174 | let contents = include_str!("input/test_rename.proto");
175 |
176 | let parsed = ProtoParser::new().parse(uri.clone(), contents);
177 | assert!(parsed.is_some());
178 | let tree = parsed.unwrap();
179 |
180 | let rename_fn = |nt: &str, pos: &Position| match tree.rename_tree(pos, nt, contents) {
181 | Some(k) => {
182 | let mut v = tree.rename_field(&k.1, &k.2, contents);
183 | v.extend(k.0);
184 | v
185 | }
186 | _ => {
187 | vec![]
188 | }
189 | };
190 |
191 | assert_yaml_snapshot!(rename_fn("Kitab", &pos_book));
192 | assert_yaml_snapshot!(rename_fn("Writer", &pos_author));
193 | assert_yaml_snapshot!(rename_fn("xyx", &pos_non_rename));
194 | }
195 |
196 | #[test]
197 | fn test_reference() {
198 | let uri: Url = "file://foo/bar.proto".parse().unwrap();
199 | let pos_book = Position {
200 | line: 5,
201 | character: 9,
202 | };
203 | let pos_author = Position {
204 | line: 11,
205 | character: 14,
206 | };
207 | let pos_non_ref = Position {
208 | line: 21,
209 | character: 5,
210 | };
211 | let contents = include_str!("input/test_reference.proto");
212 |
213 | let parsed = ProtoParser::new().parse(uri.clone(), contents);
214 | assert!(parsed.is_some());
215 | let tree = parsed.unwrap();
216 |
217 | let reference_fn = |pos: &Position| match tree.reference_tree(pos, contents) {
218 | Some(k) => {
219 | let mut v = tree.reference_field(&k.1, contents);
220 | v.extend(k.0);
221 | v
222 | }
223 | _ => {
224 | vec![]
225 | }
226 | };
227 |
228 | assert_yaml_snapshot!(reference_fn(&pos_book));
229 | assert_yaml_snapshot!(reference_fn(&pos_author));
230 | assert_yaml_snapshot!(reference_fn(&pos_non_ref));
231 | }
232 |
233 | #[test]
234 | fn test_can_rename() {
235 | let uri: Url = "file://foo/bar/test.proto".parse().unwrap();
236 | let pos_rename = Position {
237 | line: 5,
238 | character: 9,
239 | };
240 | let pos_non_rename = Position {
241 | line: 2,
242 | character: 2,
243 | };
244 | let pos_inner_type = Position {
245 | line: 19,
246 | character: 11,
247 | };
248 | let pos_outer_type = Position {
249 | line: 19,
250 | character: 5,
251 | };
252 |
253 | let contents = include_str!("input/test_can_rename.proto");
254 | let parsed = ProtoParser::new().parse(uri.clone(), contents);
255 | assert!(parsed.is_some());
256 |
257 | let tree = parsed.unwrap();
258 | assert_yaml_snapshot!(tree.can_rename(&pos_rename));
259 | assert_yaml_snapshot!(tree.can_rename(&pos_non_rename));
260 | assert_yaml_snapshot!(tree.can_rename(&pos_inner_type));
261 | assert_yaml_snapshot!(tree.can_rename(&pos_outer_type));
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | # Protols - Protocol Buffers Language Server
2 |
3 | Protols is an open-source Language Server Protocol (LSP) implementation for Protocol Buffers (proto) files, written in Rust. It provides intelligent code assistance for protobuf development, including auto-completion, diagnostics, formatting, go-to-definition, hover information, and more.
4 |
5 | Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
6 |
7 | ## Working Effectively
8 |
9 | ### Bootstrap and Build
10 | - Install dependencies and build the project:
11 | - Rust toolchain is already available (cargo 1.89.0, rustc 1.89.0)
12 | - Install protoc: `sudo apt update && sudo apt install -y protobuf-compiler` -- takes 2-3 minutes. NEVER CANCEL. Installs libprotoc 3.21.12.
13 | - clang-format is already installed and available at `/usr/bin/clang-format` (Ubuntu clang-format version 18.1.3)
14 | - `cargo build --verbose` -- takes about 1 minute to complete. NEVER CANCEL. Set timeout to 90+ minutes for safety.
15 | - `cargo test --verbose` -- takes about 6 seconds, runs 22 tests. NEVER CANCEL. Set timeout to 30+ minutes.
16 |
17 | ### Essential Commands
18 | - Check code formatting: `cargo fmt --check` -- takes under 1 second
19 | - Run linter: `cargo clippy` -- takes about 15 seconds. NEVER CANCEL. Set timeout to 30+ minutes.
20 | - Run the binary: `./target/debug/protols --help` or `./target/debug/protols --version`
21 | - Build release version: `cargo build --release` -- takes about 1 minute. NEVER CANCEL. Set timeout to 90+ minutes.
22 | - Test specific functionality: `cargo test ` for individual tests
23 |
24 | ### External Dependencies Verification
25 | - **protoc (Protocol Buffers Compiler)**: Required for advanced diagnostics. Install with `sudo apt install -y protobuf-compiler`. Verify with `protoc --version`.
26 | - **clang-format**: Required for code formatting. Already available. Verify with `clang-format --version`.
27 |
28 | ## Validation and Testing
29 |
30 | ### Manual Validation Scenarios
31 | After making changes to the LSP functionality, ALWAYS test these scenarios:
32 |
33 | 1. **Basic Build and Test Validation**:
34 | - `cargo build` -- should complete in ~1 minute without errors
35 | - `cargo test --verbose` -- should pass all 22 tests in ~6 seconds
36 | - `cargo fmt --check` -- should pass formatting check
37 | - `cargo clippy` -- should pass linting with no warnings
38 | - `./target/debug/protols --help` -- should show help message
39 | - `./target/debug/protols --version` -- should show version 0.12.8
40 |
41 | 2. **External Dependencies Validation**:
42 | - `protoc --version` -- should show libprotoc 3.21.12
43 | - `clang-format --version` -- should show Ubuntu clang-format version 18.1.3
44 | - Test protoc with sample file: `protoc sample/simple.proto --descriptor_set_out=/tmp/test.desc`
45 | - Test clang-format with sample file: `clang-format sample/simple.proto`
46 |
47 | 3. **LSP Functionality Testing**:
48 | - Test specific LSP features: `cargo test parser::hover::test::test_hover`
49 | - Test workspace functionality: `cargo test workspace`
50 | - Test with include paths: `./target/debug/protols --include-paths=/tmp,/home` (will start LSP server)
51 | - Verify LSP server starts correctly (shows logging directory and waits for input)
52 |
53 | 4. **Sample File Validation**:
54 | - Ensure sample proto files in `/sample/` directory are valid
55 | - Test parsing with `sample/simple.proto`, `sample/everything.proto`, `sample/test.proto`
56 | - Verify protoc can process sample files without errors
57 |
58 | ### CRITICAL Build and Test Timing
59 | - **NEVER CANCEL builds or tests** - they may take longer than expected
60 | - **cargo build**: 1 minute typical, set timeout to 90+ minutes
61 | - **cargo test**: 6 seconds typical, set timeout to 30+ minutes
62 | - **cargo clippy**: 15 seconds typical, set timeout to 30+ minutes
63 | - **External dependency installation**: 2-3 minutes, set timeout to 30+ minutes
64 |
65 | ### CI Validation Requirements
66 | Always run these commands before committing changes:
67 | - `cargo fmt --check` -- validates code formatting
68 | - `cargo clippy` -- validates code quality and catches common issues
69 | - `cargo test --verbose` -- runs full test suite
70 | - `cargo build --release` -- ensures release build works
71 |
72 | ## Key Project Structure
73 |
74 | ### Root Directory
75 | ```
76 | ├── Cargo.toml # Main project configuration
77 | ├── Cargo.lock # Dependency lock file
78 | ├── README.md # Project documentation
79 | ├── protols.toml # LSP configuration file
80 | ├── .clang-format # Formatting configuration for proto files
81 | ├── src/ # Main source code
82 | ├── sample/ # Sample proto files for testing
83 | └── .github/workflows/ # CI/CD pipelines
84 | ```
85 |
86 | ### Important Source Files
87 | - `src/main.rs` - Entry point, command-line argument parsing, LSP server setup
88 | - `src/server.rs` - Core LSP server implementation
89 | - `src/lsp.rs` - LSP message handling and protocol implementation
90 | - `src/parser/` - Tree-sitter based proto file parsing
91 | - `src/formatter/` - Code formatting using clang-format
92 | - `src/workspace/` - Workspace and multi-file support
93 | - `src/config/` - Configuration management
94 |
95 | ### Key Features to Test
96 | When modifying functionality, always validate:
97 | - **Code Completion**: Auto-complete messages, enums, keywords
98 | - **Diagnostics**: Syntax errors from tree-sitter and protoc
99 | - **Document Symbols**: Navigate symbols in proto files
100 | - **Code Formatting**: Format proto files using clang-format
101 | - **Go to Definition**: Jump to symbol definitions
102 | - **Hover Information**: Documentation on hover
103 | - **Rename Symbols**: Rename and propagate changes
104 | - **Find References**: Find symbol usage across files
105 |
106 | ## Configuration Details
107 |
108 | ### protols.toml Example
109 | ```toml
110 | [config]
111 | include_paths = ["src/workspace/input"]
112 |
113 | [config.path]
114 | clang_format = "clang-format"
115 | protoc = "protoc"
116 | ```
117 |
118 | ### Command Line Options
119 | - `-i, --include-paths `: Comma-separated include paths for proto files
120 | - `-V, --version`: Print version information
121 | - `-h, --help`: Print help information
122 |
123 | ## Common Development Tasks
124 |
125 | ### Common Development Tasks
126 |
127 | ### Adding New Features
128 | 1. Write tests first in appropriate `src/*/test/` directories or `src/*/input/` test data
129 | 2. Implement feature in relevant module (`src/parser/`, `src/workspace/`, `src/formatter/`, etc.)
130 | 3. Update LSP message handlers in `src/lsp.rs` if needed
131 | 4. Test with sample proto files in `sample/` directory
132 | 5. Run all validation commands before committing
133 |
134 | ### Debugging Issues
135 | - Check logs in system temp directory (output shows location on startup: "file logging at directory: /tmp")
136 | - Use `cargo test ` to run specific test modules
137 | - Use `cargo test --verbose` for detailed test output
138 | - Test with sample files: `sample/simple.proto`, `sample/everything.proto`, `sample/test.proto`
139 | - Test files available in `src/parser/input/` and `src/workspace/input/` for unit tests
140 | - Verify external dependencies: `protoc --version`, `clang-format --version`
141 |
142 | ### Working with Proto Files
143 | - Sample files available in `sample/` directory for testing
144 | - Test input files in `src/parser/input/test_*.proto` for specific functionality
145 | - Test workspace files in `src/workspace/input/` for multi-file scenarios
146 | - Always test with various proto3 syntax features: messages, enums, services, imports
147 | - Use `protoc --descriptor_set_out=/tmp/test.desc` to validate proto syntax
148 |
149 | ### Performance and Timing Expectations
150 | - Small project: ~1400 lines of Rust code
151 | - Fast incremental builds after first build
152 | - Test suite is comprehensive but fast (22 tests in 6 seconds)
153 | - LSP server starts quickly but will wait for client input (normal behavior)
154 |
155 | ## Environment Notes
156 | - This is a Rust project using edition 2024
157 | - Uses tree-sitter for parsing proto files
158 | - Integrates with external tools (protoc, clang-format) for enhanced functionality
159 | - Logging goes to system temp directory with daily rotation
160 | - Supports both Unix pipes and fallback I/O for cross-platform compatibility
161 |
162 | ## Common Command Outputs (for reference)
163 |
164 | ### Repository Structure
165 | ```
166 | ├── Cargo.toml # Main project configuration
167 | ├── Cargo.lock # Dependency lock file
168 | ├── README.md # Project documentation
169 | ├── protols.toml # LSP configuration file
170 | ├── .clang-format # Formatting configuration for proto files
171 | ├── LICENSE # MIT license
172 | ├── .gitignore # Git ignore rules
173 | ├── src/ # Main source code (~1400 lines)
174 | │ ├── main.rs # Entry point (3956 bytes)
175 | │ ├── lsp.rs # LSP implementation (19116 bytes)
176 | │ ├── server.rs # Server logic (3561 bytes)
177 | │ ├── state.rs # State management (9991 bytes)
178 | │ ├── parser/ # Tree-sitter parsing
179 | │ ├── workspace/ # Multi-file support
180 | │ ├── formatter/ # Code formatting
181 | │ ├── config/ # Configuration management
182 | │ └── docs/ # Documentation generation
183 | ├── sample/ # Sample proto files
184 | │ ├── simple.proto # Basic examples
185 | │ ├── everything.proto # Comprehensive features
186 | │ └── test.proto # Test scenarios
187 | └── .github/workflows/ # CI/CD pipelines
188 | ├── ci.yml # Build and test
189 | └── release.yml # Release automation
190 | ```
191 |
192 | ### Key Project Metadata
193 | ```
194 | name = "protols"
195 | description = "Language server for proto3 files"
196 | version = "0.12.8"
197 | edition = "2024"
198 | license = "MIT"
199 | ```
200 |
201 | ### Test Output Summary
202 | - **Total tests**: 22 tests across parser, workspace, config, and formatter modules
203 | - **Test categories**: hover, definition, rename, document symbols, diagnostics, workspace operations
204 | - **Performance**: All tests complete in under 6 seconds
205 | - **Coverage**: Core LSP features, configuration, and multi-file workspace scenarios
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Protols - Protobuf Language Server
2 |
3 | [](https://crates.io/crates/protols)
4 | [](https://github.com/coder3101/protols/actions/workflows/ci.yml)
5 |
6 | **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.
7 |
8 | ## ✨ Features
9 |
10 | - ✅ **Code Completion**: Auto-complete messages, enums, and keywords in your `.proto` files.
11 | - ✅ **Diagnostics**: Syntax errors, import error with tree-sitter and advanced diagnostics from `protoc`.
12 | - ✅ **Workspace Symbols**: Search and view all symbols across workspaces.
13 | - ✅ **Document Symbols**: Navigate and view all symbols, including nested messages and enums.
14 | - ✅ **Code Formatting**: Format `.proto` files using `clang-format` for a consistent style.
15 | - ✅ **Go to Definition**: Jump to the definition of symbols like messages or enums and imports.
16 | - ✅ **Hover Information**: Get detailed information and documentation on hover.
17 | - ✅ **Rename Symbols**: Rename protobuf symbols and propagate changes across the codebase.
18 | - ✅ **Find References**: Find where messages, enums, and fields are used throughout the codebase.
19 |
20 | ---
21 |
22 | ## Table of Contents
23 |
24 | - [Installation](#-installation)
25 | - [For Neovim](#for-neovim)
26 | - [Setting Include Paths in Neovim](#setting-include-paths-in-neovim)
27 | - [Command Line Options](#command-line-options)
28 | - [For Visual Studio Code](#for-visual-studio-code)
29 | - [Configuration](#%EF%B8%8Fconfiguration)
30 | - [Sample `protols.toml`](#sample-protolstoml)
31 | - [Configuration Sections](#configuration-sections)
32 | - [Basic Configuration](#basic-configuration)
33 | - [Path Configuration](#path-configuration)
34 | - [Usage](#-usage)
35 | - [Code Completion](#code-completion)
36 | - [Diagnostics](#diagnostics)
37 | - [Code Formatting](#code-formatting)
38 | - [Workspace Symbols](#workspace-symbols)
39 | - [Document Symbols](#document-symbols)
40 | - [Go to Definition](#go-to-definition)
41 | - [Hover Information](#hover-information)
42 | - [Rename Symbols](#rename-symbols)
43 | - [Find References](#find-references)
44 | - [Protocol Buffers Well-Known Types](#protocol-buffers-well-known-types)
45 | - [Packaging](#-packaging)
46 | - [Contributing](#-contributing)
47 | - [Setting Up Locally](#setting-up-locally)
48 | - [License](#-license)
49 |
50 | ---
51 |
52 | ## 🚀 Installation
53 |
54 | ### For Neovim
55 |
56 | You can install **Protols** via [mason.nvim](https://github.com/mason-org/mason-registry/blob/main/packages/protols/package.yaml), or install it directly from [crates.io](https://crates.io/crates/protols):
57 |
58 | ```bash
59 | cargo install protols
60 | ```
61 |
62 | Then, configure it in your `init.lua` using [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig):
63 |
64 | ```lua
65 | require'lspconfig'.protols.setup{}
66 | ```
67 |
68 | #### Setting Include Paths in Neovim
69 |
70 | For dynamic configuration of include paths, you can use the `before_init` callback to set them via `initializationParams`:
71 |
72 | ```lua
73 | require'lspconfig'.protols.setup{
74 | before_init = function(_, config)
75 | config.init_options = {
76 | include_paths = {
77 | "/usr/local/include/protobuf",
78 | "vendor/protos",
79 | "../shared-protos"
80 | }
81 | }
82 | end
83 | }
84 | ```
85 |
86 | ### Command Line Options
87 |
88 | Protols supports various command line options to customize its behavior:
89 |
90 | ```
91 | protols [OPTIONS]
92 |
93 | Options:
94 | -i, --include-paths Include paths for proto files, comma-separated
95 | -V, --version Print version information
96 | -h, --help Print help information
97 | ```
98 |
99 | For example, to specify include paths when starting the language server:
100 |
101 | ```bash
102 | protols --include-paths=/path/to/protos,/another/path/to/protos
103 | ```
104 |
105 | ### For Visual Studio Code
106 |
107 | 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.
108 |
109 | > **Note**: This extension is [open source](https://github.com/ianandhum/vscode-protobuf-support), but is not officially maintained by us.
110 |
111 | ---
112 |
113 | ## ⚙️Configuration
114 |
115 | Protols can be configured using a `protols.toml` file, which you can place in root of your project directory.
116 |
117 | ### Sample `protols.toml`
118 |
119 | ```toml
120 | [config]
121 | include_paths = ["foobar", "bazbaaz"] # Include paths to look for protofiles during parsing
122 |
123 | [config.path]
124 | clang_format = "clang-format"
125 | protoc = "protoc"
126 | ```
127 |
128 | ### Configuration Sections
129 |
130 | #### Basic Configuration
131 |
132 | The `[config]` section contains stable settings that should generally remain unchanged.
133 |
134 | - `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:
135 | - **Configuration file**: Workspace-specific paths defined in `protols.toml`
136 | - **Command line**: Global paths using `--include-paths` flag that apply to all workspaces
137 | - **Initialization parameters**: Dynamic paths set via LSP `initializationParams` (useful for editors like Neovim)
138 |
139 | When a file is not found in any of the paths above, the following directories are searched:
140 | - **Protobuf Include Path**: the path containing the [Protocol Buffers Well-Known Types](https://protobuf.dev/reference/protobuf/google.protobuf/) as detected by [`pkg-config`](https://www.freedesktop.org/wiki/Software/pkg-config/) (requires `pkg-config` present in environment and capable of finding the installation of `protobuf`)
141 | - **Fallback Include Path**: the fallback include path configured at compile time
142 |
143 |
144 | All include paths from these sources are combined when resolving proto imports.
145 |
146 | #### Path Configuration
147 |
148 | The `[config.path]` section contains path for various tools used by LSP.
149 |
150 | - `clang_format`: Uses clang_format from this path for formatting
151 | - `protoc`: Uses protoc from this path for diagnostics
152 |
153 | ---
154 |
155 | ## 🛠 Usage
156 |
157 | Protols offers a rich set of features to enhance your `.proto` file editing experience.
158 |
159 | ### Code Completion
160 |
161 | **Protols** offers intelligent autocompletion for messages, enums, and proto3 keywords within the current package. Simply start typing, and Protols will suggest valid completions.
162 |
163 | ### Diagnostics
164 |
165 | Syntax errors are caught by the tree-sitter parser, which highlights issues directly in your editor. More advanced error reporting, is done by `protoc` which runs after a file saved. You must have `protoc` installed and added to your path or you can specify its path in the configuration above
166 |
167 | ### Code Formatting
168 |
169 | Format your `.proto` files using `clang-format`. To customize the formatting style, add a `.clang-format` file to the root of your project. Both document and range formatting are supported.
170 |
171 | ### Workspace Symbols
172 |
173 | Protols implements workspace symbol capabilities allowing you to search for symbols across workspace or list them, including nested symbols such as messages and enums. This allows for easy navigation and reference across workspace.
174 |
175 | ### Document Symbols
176 |
177 | Protols provides a list of symbols in the current document, including nested symbols such as messages and enums. This allows for easy navigation and reference.
178 |
179 | ### Go to Definition
180 |
181 | Jump directly to the definition of any custom symbol or imports, including those in other files or packages. This feature works across package boundaries.
182 |
183 | ### Hover Information
184 |
185 | Hover over any symbol or imports to get detailed documentation and comments associated with it. This works seamlessly across different packages and namespaces.
186 |
187 | ### Rename Symbols
188 |
189 | Rename symbols like messages or enums, and Propagate the changes throughout the codebase. Currently, field renaming within symbols is not supported.
190 |
191 | ### Find References
192 |
193 | Find all references to user-defined types like messages or enums. Nested fields are fully supported, making it easier to track symbol usage across your project.
194 |
195 | ## Protocol Buffers Well-Known Types
196 |
197 | Protols does not ship with the [Protocol Buffers Well-Known Types](https://protobuf.dev/reference/protobuf/google.protobuf/) unless configured to do so by a distribution.
198 | In order for features above to work for the well-known types, the well-known imports must either resolve against one of the configured import paths or the environment must contain in `PATH` a `pkg-config` executable capable of resolving the package `protobuf`.
199 | You can verify this by running
200 |
201 | ```bash
202 | pkg-config --modversion protobuf
203 | ```
204 |
205 | in protols' environment.
206 |
207 | ---
208 |
209 | ## 📦 Packaging
210 |
211 | Distributions may set an absolute include path which contains the Protocol Buffers Well-Known Types,
212 | for example pointing to the files provided by the `protobuf` package, by compiling protols with the
213 | environment variable `FALLBACK_INCLUDE_PATH` set to the desired path. This path will be used by the
214 | compiled executable for resolution of any proto files that could not be resolved otherwise.
215 |
216 | ## 🤝 Contributing
217 |
218 | We welcome contributions from developers of all experience levels! To get started:
219 |
220 | 1. **Fork** the repository and clone it to your local machine.
221 | 2. Create a **new branch** for your feature or fix.
222 | 3. Run the tests to ensure everything works as expected.
223 | 4. **Open a pull request** with a detailed description of your changes.
224 |
225 | ### Setting Up Locally
226 |
227 | 1. Clone the repository:
228 |
229 | ```bash
230 | git clone https://github.com/coder3101/protols.git
231 | cd protols
232 | ```
233 |
234 | 2. Build and test the project:
235 |
236 | ```bash
237 | cargo build
238 | cargo test
239 | ```
240 |
241 | ---
242 |
243 | ## 📄 License
244 |
245 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
246 |
247 | ---
248 |
--------------------------------------------------------------------------------
/src/state.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::{HashMap, HashSet},
3 | path::PathBuf,
4 | sync::{Arc, Mutex, RwLock},
5 | };
6 | use tracing::info;
7 |
8 | use async_lsp::lsp_types::{
9 | CompletionItem, CompletionItemKind, Location, OneOf, PublishDiagnosticsParams, Url,
10 | WorkspaceSymbol,
11 | };
12 | use async_lsp::lsp_types::{DocumentSymbol, ProgressParamsValue};
13 | use std::sync::mpsc::Sender;
14 | use tree_sitter::Node;
15 | use walkdir::WalkDir;
16 |
17 | use crate::{
18 | config::Config,
19 | nodekind::NodeKind,
20 | parser::{ParsedTree, ProtoParser},
21 | };
22 |
23 | use crate::protoc::ProtocDiagnostics;
24 |
25 | pub struct ProtoLanguageState {
26 | documents: Arc>>,
27 | trees: Arc>>,
28 | parser: Arc>,
29 | parsed_workspaces: Arc>>,
30 | protoc_diagnostics: Arc>,
31 | }
32 |
33 | impl ProtoLanguageState {
34 | pub fn new() -> Self {
35 | ProtoLanguageState {
36 | documents: Default::default(),
37 | trees: Default::default(),
38 | parser: Arc::new(Mutex::new(ProtoParser::new())),
39 | parsed_workspaces: Arc::new(RwLock::new(HashSet::new())),
40 | protoc_diagnostics: Arc::new(Mutex::new(ProtocDiagnostics::new())),
41 | }
42 | }
43 |
44 | pub fn get_content(&self, uri: &Url) -> String {
45 | self.documents
46 | .read()
47 | .expect("poison")
48 | .get(uri)
49 | .map(|s| s.to_string())
50 | .unwrap_or_default()
51 | }
52 |
53 | pub fn get_tree(&self, uri: &Url) -> Option {
54 | self.trees.read().expect("poison").get(uri).cloned()
55 | }
56 |
57 | pub fn get_trees(&self) -> Vec {
58 | self.trees
59 | .read()
60 | .expect("poison")
61 | .values()
62 | .map(ToOwned::to_owned)
63 | .collect()
64 | }
65 |
66 | pub fn get_trees_for_package(&self, package: &str) -> Vec {
67 | self.trees
68 | .read()
69 | .expect("poison")
70 | .values()
71 | .filter(|tree| {
72 | let content = self.get_content(&tree.uri);
73 | tree.get_package_name(content.as_bytes()).unwrap_or(".") == package
74 | })
75 | .map(ToOwned::to_owned)
76 | .collect()
77 | }
78 |
79 | pub fn find_workspace_symbols(&self, query: &str) -> Vec {
80 | let mut symbols = Vec::new();
81 |
82 | for tree in self.get_trees() {
83 | let content = self.get_content(&tree.uri);
84 | let doc_symbols = tree.find_document_locations(content.as_bytes());
85 |
86 | for doc_symbol in doc_symbols {
87 | Self::find_workspace_symbols_impl(
88 | &doc_symbol,
89 | &tree.uri,
90 | query,
91 | None,
92 | &mut symbols,
93 | );
94 | }
95 | }
96 |
97 | // Sort symbols by name and then by URI for consistent ordering
98 | symbols.sort_by(|a, b| {
99 | let name_cmp = a.name.cmp(&b.name);
100 | if name_cmp != std::cmp::Ordering::Equal {
101 | return name_cmp;
102 | }
103 | // Extract URI from location
104 | match (&a.location, &b.location) {
105 | (OneOf::Left(loc_a), OneOf::Left(loc_b)) => {
106 | loc_a.uri.as_str().cmp(loc_b.uri.as_str())
107 | }
108 | _ => std::cmp::Ordering::Equal,
109 | }
110 | });
111 |
112 | symbols
113 | }
114 |
115 | fn find_workspace_symbols_impl(
116 | doc_symbol: &DocumentSymbol,
117 | uri: &Url,
118 | query: &str,
119 | container_name: Option,
120 | symbols: &mut Vec,
121 | ) {
122 | let symbol_name_lower = doc_symbol.name.to_lowercase();
123 |
124 | if query.is_empty() || symbol_name_lower.contains(query) {
125 | symbols.push(WorkspaceSymbol {
126 | name: doc_symbol.name.clone(),
127 | kind: doc_symbol.kind,
128 | tags: doc_symbol.tags.clone(),
129 | container_name: container_name.clone(),
130 | location: OneOf::Left(Location {
131 | uri: uri.clone(),
132 | range: doc_symbol.range,
133 | }),
134 | data: None,
135 | });
136 | }
137 |
138 | if let Some(children) = &doc_symbol.children {
139 | for child in children {
140 | Self::find_workspace_symbols_impl(
141 | child,
142 | uri,
143 | query,
144 | Some(doc_symbol.name.clone()),
145 | symbols,
146 | );
147 | }
148 | }
149 | }
150 |
151 | fn upsert_content_impl(
152 | &mut self,
153 | uri: &Url,
154 | content: String,
155 | ipath: &[PathBuf],
156 | depth: usize,
157 | parse_session: &mut HashSet,
158 | ) {
159 | // Safety: to not cause stack overflow
160 | if depth == 0 {
161 | return;
162 | }
163 |
164 | // avoid re-parsing same file incase of circular dependencies
165 | if parse_session.contains(uri) {
166 | return;
167 | }
168 |
169 | let Some(parsed) = self
170 | .parser
171 | .lock()
172 | .expect("poison")
173 | .parse(uri.clone(), content.as_bytes())
174 | else {
175 | return;
176 | };
177 |
178 | self.trees
179 | .write()
180 | .expect("posion")
181 | .insert(uri.clone(), parsed);
182 |
183 | self.documents
184 | .write()
185 | .expect("poison")
186 | .insert(uri.clone(), content.clone());
187 |
188 | parse_session.insert(uri.clone());
189 | let imports = self.get_owned_imports(uri, content.as_str());
190 |
191 | for import in imports.iter() {
192 | if let Some(p) = ipath.iter().map(|p| p.join(import)).find(|p| p.exists())
193 | && let Ok(uri) = Url::from_file_path(p.clone())
194 | && let Ok(content) = std::fs::read_to_string(p)
195 | {
196 | self.upsert_content_impl(&uri, content, ipath, depth - 1, parse_session);
197 | }
198 | }
199 | }
200 |
201 | fn get_owned_imports(&self, uri: &Url, content: &str) -> Vec {
202 | self.get_tree(uri)
203 | .map(|t| t.get_import_paths(content.as_ref()))
204 | .unwrap_or_default()
205 | .into_iter()
206 | .map(ToOwned::to_owned)
207 | .collect()
208 | }
209 |
210 | pub fn upsert_content(
211 | &mut self,
212 | uri: &Url,
213 | content: String,
214 | ipath: &[PathBuf],
215 | depth: usize,
216 | ) -> Vec {
217 | let mut session = HashSet::new();
218 | self.upsert_content_impl(uri, content.clone(), ipath, depth, &mut session);
219 |
220 | // After content is upserted, those imports which couldn't be located
221 | // are flagged as import error
222 | self.get_tree(uri)
223 | .map(|t| t.get_import_paths(content.as_ref()))
224 | .unwrap_or_default()
225 | .into_iter()
226 | .map(ToOwned::to_owned)
227 | .filter(|import| !ipath.iter().any(|p| p.join(import.as_str()).exists()))
228 | .collect()
229 | }
230 |
231 | pub fn parse_all_from_workspace(
232 | &mut self,
233 | workspace: PathBuf,
234 | progress_sender: Option>,
235 | ) {
236 | if self
237 | .parsed_workspaces
238 | .read()
239 | .expect("poison")
240 | .contains(workspace.to_str().unwrap_or_default())
241 | {
242 | return;
243 | }
244 |
245 | let files: Vec<_> = WalkDir::new(workspace.to_str().unwrap_or_default())
246 | .into_iter()
247 | .filter_map(|e| e.ok())
248 | .filter(|e| e.path().extension().is_some())
249 | .filter(|e| e.path().extension().unwrap() == "proto")
250 | .collect();
251 |
252 | let total_files = files.len();
253 |
254 | for (idx, file) in files.into_iter().enumerate() {
255 | let path = file.path();
256 | if path.is_absolute()
257 | && path.is_file()
258 | && let Ok(content) = std::fs::read_to_string(path)
259 | && let Ok(uri) = Url::from_file_path(path)
260 | {
261 | if self.documents.read().expect("poison").contains_key(&uri) {
262 | continue;
263 | }
264 | self.upsert_content(&uri, content, &[], 1);
265 |
266 | if let Some(sender) = &progress_sender {
267 | let percentage = ((idx + 1) as f64 / total_files as f64 * 100.0) as u32;
268 | let _ = sender.send(ProgressParamsValue::WorkDone(
269 | async_lsp::lsp_types::WorkDoneProgress::Report(
270 | async_lsp::lsp_types::WorkDoneProgressReport {
271 | cancellable: None,
272 | message: Some(format!(
273 | "Parsing file {} of {}",
274 | idx + 1,
275 | total_files
276 | )),
277 | percentage: Some(percentage),
278 | },
279 | ),
280 | ));
281 | }
282 | }
283 | }
284 |
285 | self.parsed_workspaces
286 | .write()
287 | .expect("poison")
288 | .insert(workspace.to_str().unwrap_or_default().to_string());
289 | }
290 |
291 | pub fn upsert_file(
292 | &mut self,
293 | uri: &Url,
294 | content: String,
295 | ipath: &[PathBuf],
296 | depth: usize,
297 | config: &Config,
298 | protoc_diagnostics: bool,
299 | ) -> Option {
300 | info!(%uri, %depth, "upserting file");
301 | let diag = self.upsert_content(uri, content.clone(), ipath, depth);
302 | self.get_tree(uri).map(|tree| {
303 | let mut d = vec![];
304 | d.extend(tree.collect_parse_diagnostics());
305 | d.extend(tree.collect_import_diagnostics(content.as_ref(), diag));
306 |
307 | // Add protoc diagnostics if enabled
308 | if protoc_diagnostics
309 | && let Ok(protoc_diagnostics) = self.protoc_diagnostics.lock()
310 | && let Ok(file_path) = uri.to_file_path()
311 | {
312 | let protoc_diags = protoc_diagnostics.collect_diagnostics(
313 | &config.path.protoc,
314 | file_path.to_str().unwrap_or_default(),
315 | &ipath
316 | .iter()
317 | .map(|p| p.to_str().unwrap_or_default().to_string())
318 | .collect::>(),
319 | );
320 | d.extend(protoc_diags);
321 | }
322 |
323 | PublishDiagnosticsParams {
324 | uri: tree.uri.clone(),
325 | diagnostics: d,
326 | version: None,
327 | }
328 | })
329 | }
330 |
331 | pub fn delete_file(&mut self, uri: &Url) {
332 | info!(%uri, "deleting file");
333 | self.documents.write().expect("poison").remove(uri);
334 | self.trees.write().expect("poison").remove(uri);
335 | }
336 |
337 | pub fn rename_file(&mut self, new_uri: &Url, old_uri: &Url) {
338 | info!(%new_uri, %new_uri, "renaming file");
339 |
340 | if let Some(v) = self.documents.write().expect("poison").remove(old_uri) {
341 | self.documents
342 | .write()
343 | .expect("poison")
344 | .insert(new_uri.clone(), v);
345 | }
346 |
347 | if let Some(mut v) = self.trees.write().expect("poison").remove(old_uri) {
348 | v.uri = new_uri.clone();
349 | self.trees
350 | .write()
351 | .expect("poison")
352 | .insert(new_uri.clone(), v);
353 | }
354 | }
355 |
356 | pub fn completion_items(&self, package: &str) -> Vec {
357 | let collector = |f: fn(&Node) -> bool, k: CompletionItemKind| {
358 | self.get_trees_for_package(package)
359 | .into_iter()
360 | .fold(vec![], |mut v, tree| {
361 | let content = self.get_content(&tree.uri);
362 | let t = tree.find_all_nodes(f).into_iter().map(|n| CompletionItem {
363 | label: n.utf8_text(content.as_bytes()).unwrap().to_string(),
364 | kind: Some(k),
365 | ..Default::default()
366 | });
367 | v.extend(t);
368 | v
369 | })
370 | };
371 |
372 | let mut result = collector(NodeKind::is_enum_name, CompletionItemKind::ENUM);
373 | result.extend(collector(
374 | NodeKind::is_message_name,
375 | CompletionItemKind::STRUCT,
376 | ));
377 | // Better ways to dedup, but who cares?...
378 | result.sort_by_key(|k| k.label.clone());
379 | result.dedup_by_key(|k| k.label.clone());
380 | result
381 | }
382 | }
383 |
--------------------------------------------------------------------------------
/src/config/workspace.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::{HashMap, HashSet},
3 | env,
4 | path::{Path, PathBuf},
5 | };
6 |
7 | use async_lsp::lsp_types::{Url, WorkspaceFolder};
8 | use pkg_config::Config;
9 |
10 | use crate::formatter::clang::ClangFormatter;
11 |
12 | use super::ProtolsConfig;
13 |
14 | const CONFIG_FILE_NAMES: [&str; 2] = [".protols.toml", "protols.toml"];
15 |
16 | pub struct WorkspaceProtoConfigs {
17 | workspaces: HashSet,
18 | configs: HashMap,
19 | formatters: HashMap,
20 | protoc_include_prefix: Vec,
21 | cli_include_paths: Vec,
22 | init_include_paths: Vec,
23 | fallback_include_path: Option,
24 | }
25 |
26 | impl WorkspaceProtoConfigs {
27 | pub fn new(cli_include_paths: Vec, fallback_include_path: Option) -> Self {
28 | // Try to find protobuf library and get its include paths
29 | // Do not emit metadata on stdout as LSP programs can consider
30 | // it part of spec
31 | let protoc_include_prefix = Config::new()
32 | .atleast_version("3.0.0")
33 | .env_metadata(false)
34 | .cargo_metadata(false)
35 | .probe("protobuf")
36 | .map(|lib| lib.include_paths)
37 | .unwrap_or_default();
38 |
39 | Self {
40 | workspaces: HashSet::new(),
41 | formatters: HashMap::new(),
42 | configs: HashMap::new(),
43 | fallback_include_path,
44 | protoc_include_prefix,
45 | cli_include_paths,
46 | init_include_paths: Vec::new(),
47 | }
48 | }
49 |
50 | fn get_config_file_path(wpath: &PathBuf) -> Option {
51 | for file in CONFIG_FILE_NAMES {
52 | let p = Path::new(&wpath).join(file);
53 | match std::fs::exists(&p) {
54 | Ok(exists) if exists => return Some(p),
55 | _ => continue,
56 | }
57 | }
58 | None
59 | }
60 |
61 | pub fn add_workspace(&mut self, w: &WorkspaceFolder) {
62 | let Ok(wpath) = w.uri.to_file_path() else {
63 | return;
64 | };
65 |
66 | let path = Self::get_config_file_path(&wpath).unwrap_or_default();
67 | let content = std::fs::read_to_string(path).unwrap_or_default();
68 |
69 | let wr: ProtolsConfig = basic_toml::from_str(&content).unwrap_or_default();
70 | let fmt = ClangFormatter::new(
71 | &wr.config.path.clang_format,
72 | Some(wpath.to_str().expect("non-utf8 path")),
73 | );
74 |
75 | self.workspaces.insert(w.uri.clone());
76 | self.configs.insert(w.uri.clone(), wr);
77 | self.formatters.insert(w.uri.clone(), fmt);
78 | }
79 |
80 | pub fn get_config_for_uri(&self, u: &Url) -> Option<&ProtolsConfig> {
81 | self.get_workspace_for_uri(u)
82 | .and_then(|w| self.configs.get(w))
83 | }
84 |
85 | pub fn get_formatter_for_uri(&self, u: &Url) -> Option<&ClangFormatter> {
86 | self.get_workspace_for_uri(u)
87 | .and_then(|w| self.formatters.get(w))
88 | }
89 |
90 | pub fn get_workspace_for_uri(&self, u: &Url) -> Option<&Url> {
91 | let upath = u.to_file_path().ok()?;
92 | self.workspaces
93 | .iter()
94 | .find(|&k| upath.starts_with(k.to_file_path().unwrap()))
95 | }
96 |
97 | pub fn set_init_include_paths(&mut self, paths: Vec) {
98 | self.init_include_paths = paths;
99 | }
100 |
101 | pub fn get_include_paths(&self, uri: &Url) -> Option> {
102 | let cfg = self.get_config_for_uri(uri)?;
103 | let w = self.get_workspace_for_uri(uri)?.to_file_path().ok()?;
104 |
105 | let mut ipath: Vec = cfg
106 | .config
107 | .include_paths
108 | .iter()
109 | .map(PathBuf::from)
110 | .map(|p| if p.is_relative() { w.join(p) } else { p })
111 | .collect();
112 |
113 | // Add CLI include paths
114 | for path in &self.cli_include_paths {
115 | if path.is_relative() {
116 | ipath.push(w.join(path));
117 | } else {
118 | ipath.push(path.clone());
119 | }
120 | }
121 |
122 | // Add initialization include paths
123 | for path in &self.init_include_paths {
124 | if path.is_relative() {
125 | ipath.push(w.join(path));
126 | } else {
127 | ipath.push(path.clone());
128 | }
129 | }
130 |
131 | ipath.push(w.to_path_buf());
132 | ipath.extend_from_slice(&self.protoc_include_prefix);
133 | ipath.extend_from_slice(self.fallback_include_path.as_slice());
134 | Some(ipath)
135 | }
136 |
137 | pub fn get_workspaces(&self) -> Vec<&Url> {
138 | self.workspaces.iter().collect()
139 | }
140 |
141 | pub fn no_workspace_mode(&mut self) {
142 | let wr = ProtolsConfig::default();
143 | let rp = if cfg!(target_os = "windows") {
144 | let mut d = String::from("C");
145 | if let Ok(cdir) = env::current_dir()
146 | && let Some(drive) = cdir.components().next()
147 | {
148 | d = drive.as_os_str().to_string_lossy().to_string()
149 | }
150 | format!("{d}://")
151 | } else {
152 | String::from("/")
153 | };
154 | let uri = match Url::from_file_path(&rp) {
155 | Err(err) => {
156 | tracing::error!(?err, "failed to convert path: {rp} to Url");
157 | return;
158 | }
159 | Ok(uri) => uri,
160 | };
161 |
162 | let fmt = ClangFormatter::new(&wr.config.path.clang_format, None);
163 |
164 | self.workspaces.insert(uri.clone());
165 | self.configs.insert(uri.clone(), wr);
166 | self.formatters.insert(uri.clone(), fmt);
167 | }
168 | }
169 |
170 | #[cfg(test)]
171 | mod test {
172 | use async_lsp::lsp_types::{Url, WorkspaceFolder};
173 | use insta::assert_yaml_snapshot;
174 | use std::path::PathBuf;
175 | use tempfile::tempdir;
176 |
177 | use super::{CONFIG_FILE_NAMES, WorkspaceProtoConfigs};
178 |
179 | #[test]
180 | fn test_get_for_workspace() {
181 | let tmpdir = tempdir().expect("failed to create temp directory");
182 | let tmpdir2 = tempdir().expect("failed to create temp2 directory");
183 | let f = tmpdir.path().join("protols.toml");
184 | std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap();
185 |
186 | let mut ws = WorkspaceProtoConfigs::new(vec![], None);
187 | ws.add_workspace(&WorkspaceFolder {
188 | uri: Url::from_directory_path(tmpdir.path()).unwrap(),
189 | name: "Test".to_string(),
190 | });
191 | ws.add_workspace(&WorkspaceFolder {
192 | uri: Url::from_directory_path(tmpdir2.path()).unwrap(),
193 | name: "Test2".to_string(),
194 | });
195 |
196 | let inworkspace = Url::from_file_path(tmpdir.path().join("foobar.proto")).unwrap();
197 | let outworkspace =
198 | Url::from_file_path(tempdir().unwrap().path().join("out.proto")).unwrap();
199 | let inworkspace2 = Url::from_file_path(tmpdir2.path().join("foobar.proto")).unwrap();
200 |
201 | assert!(ws.get_config_for_uri(&inworkspace).is_some());
202 | assert!(ws.get_config_for_uri(&inworkspace2).is_some());
203 | assert!(ws.get_config_for_uri(&outworkspace).is_none());
204 |
205 | assert!(ws.get_workspace_for_uri(&inworkspace).is_some());
206 | assert!(ws.get_workspace_for_uri(&inworkspace2).is_some());
207 | assert!(ws.get_workspace_for_uri(&outworkspace).is_none());
208 |
209 | assert_yaml_snapshot!(ws.get_config_for_uri(&inworkspace).unwrap());
210 | assert_yaml_snapshot!(ws.get_config_for_uri(&inworkspace2).unwrap());
211 | }
212 |
213 | #[test]
214 | fn test_get_formatter_for_uri() {
215 | let tmpdir = tempdir().expect("failed to create temp directory");
216 | let tmpdir2 = tempdir().expect("failed to create temp2 directory");
217 | let f = tmpdir.path().join("protols.toml");
218 | std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap();
219 |
220 | let mut ws = WorkspaceProtoConfigs::new(vec![], None);
221 | ws.add_workspace(&WorkspaceFolder {
222 | uri: Url::from_directory_path(tmpdir.path()).unwrap(),
223 | name: "Test".to_string(),
224 | });
225 |
226 | ws.add_workspace(&WorkspaceFolder {
227 | uri: Url::from_directory_path(tmpdir2.path()).unwrap(),
228 | name: "Test2".to_string(),
229 | });
230 |
231 | let inworkspace = Url::from_file_path(tmpdir.path().join("foobar.proto")).unwrap();
232 | let outworkspace =
233 | Url::from_file_path(tempdir().unwrap().path().join("out.proto")).unwrap();
234 | let inworkspace2 = Url::from_file_path(tmpdir2.path().join("foobar.proto")).unwrap();
235 |
236 | assert!(ws.get_formatter_for_uri(&outworkspace).is_none());
237 | assert_eq!(
238 | ws.get_formatter_for_uri(&inworkspace).unwrap().path,
239 | "/usr/bin/clang-format"
240 | );
241 | assert_eq!(
242 | ws.get_formatter_for_uri(&inworkspace2).unwrap().path,
243 | "clang-format"
244 | );
245 | }
246 |
247 | #[test]
248 | fn test_loading_different_config_files() {
249 | let tmpdir = tempdir().expect("failed to create temp directory");
250 |
251 | for file in CONFIG_FILE_NAMES {
252 | let f = tmpdir.path().join(file);
253 | std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap();
254 |
255 | let mut ws = WorkspaceProtoConfigs::new(vec![], None);
256 | ws.add_workspace(&WorkspaceFolder {
257 | uri: Url::from_directory_path(tmpdir.path()).unwrap(),
258 | name: "Test".to_string(),
259 | });
260 |
261 | // check we really loaded the config file
262 | let workspace = Url::from_file_path(tmpdir.path().join("foobar.proto")).unwrap();
263 | assert!(ws.get_workspace_for_uri(&workspace).is_some());
264 | }
265 | }
266 |
267 | #[test]
268 | fn test_cli_include_paths() {
269 | let tmpdir = tempdir().expect("failed to create temp directory");
270 | let f = tmpdir.path().join("protols.toml");
271 | std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap();
272 |
273 | // Set CLI include paths
274 | let cli_paths = vec![
275 | PathBuf::from("/path/to/protos"),
276 | PathBuf::from("relative/path"),
277 | ];
278 | let mut ws = WorkspaceProtoConfigs::new(cli_paths, None);
279 | ws.add_workspace(&WorkspaceFolder {
280 | uri: Url::from_directory_path(tmpdir.path()).unwrap(),
281 | name: "Test".to_string(),
282 | });
283 |
284 | let inworkspace = Url::from_file_path(tmpdir.path().join("foobar.proto")).unwrap();
285 | let include_paths = ws.get_include_paths(&inworkspace).unwrap();
286 |
287 | // Check that CLI paths are included in the result
288 | assert!(
289 | include_paths
290 | .iter()
291 | .any(|p| p.ends_with("relative/path") || p == &PathBuf::from("/path/to/protos"))
292 | );
293 |
294 | // The relative path should be resolved relative to the workspace
295 | let resolved_relative_path = tmpdir.path().join("relative/path");
296 | assert!(include_paths.contains(&resolved_relative_path));
297 |
298 | // The absolute path should be included as is
299 | assert!(include_paths.contains(&PathBuf::from("/path/to/protos")));
300 | }
301 |
302 | #[test]
303 | fn test_init_include_paths() {
304 | let tmpdir = tempdir().expect("failed to create temp directory");
305 | let f = tmpdir.path().join("protols.toml");
306 | std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap();
307 |
308 | // Set both CLI and initialization include paths
309 | let cli_paths = vec![PathBuf::from("/cli/path")];
310 | let init_paths = vec![
311 | PathBuf::from("/init/path1"),
312 | PathBuf::from("relative/init/path"),
313 | ];
314 |
315 | let mut ws = WorkspaceProtoConfigs::new(cli_paths, None);
316 | ws.set_init_include_paths(init_paths);
317 | ws.add_workspace(&WorkspaceFolder {
318 | uri: Url::from_directory_path(tmpdir.path()).unwrap(),
319 | name: "Test".to_string(),
320 | });
321 |
322 | let inworkspace = Url::from_file_path(tmpdir.path().join("foobar.proto")).unwrap();
323 | let include_paths = ws.get_include_paths(&inworkspace).unwrap();
324 |
325 | // Check that initialization paths are included
326 | assert!(include_paths.contains(&PathBuf::from("/init/path1")));
327 |
328 | // The relative path should be resolved relative to the workspace
329 | let resolved_relative_path = tmpdir.path().join("relative/init/path");
330 | assert!(include_paths.contains(&resolved_relative_path));
331 |
332 | // CLI paths should still be included
333 | assert!(include_paths.contains(&PathBuf::from("/cli/path")));
334 | }
335 |
336 | #[test]
337 | fn test_fallback_include_path() {
338 | let tmpdir = tempdir().expect("failed to create temp directory");
339 | let f = tmpdir.path().join("protols.toml");
340 | std::fs::write(f, include_str!("input/protols-valid.toml")).unwrap();
341 |
342 | // Set both CLI and initialization include paths
343 | let cli_paths = vec![PathBuf::from("/cli/path")];
344 | let init_paths = vec![
345 | PathBuf::from("/init/path1"),
346 | PathBuf::from("relative/init/path"),
347 | ];
348 |
349 | let mut ws = WorkspaceProtoConfigs::new(cli_paths, Some("fallback_path".into()));
350 | ws.set_init_include_paths(init_paths);
351 | ws.add_workspace(&WorkspaceFolder {
352 | uri: Url::from_directory_path(tmpdir.path()).unwrap(),
353 | name: "Test".to_string(),
354 | });
355 |
356 | let inworkspace = Url::from_file_path(tmpdir.path().join("foobar.proto")).unwrap();
357 | let include_paths = ws.get_include_paths(&inworkspace).unwrap();
358 |
359 | // Fallback path should be included and on the last position
360 | assert_eq!(
361 | include_paths
362 | .iter()
363 | .rev()
364 | .position(|p| p == "fallback_path"),
365 | Some(0)
366 | );
367 | }
368 | }
369 |
--------------------------------------------------------------------------------