├── .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 | [![Crates.io](https://img.shields.io/crates/v/protols.svg)](https://crates.io/crates/protols) 4 | [![Build and Test](https://github.com/coder3101/protols/actions/workflows/ci.yml/badge.svg)](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 | --------------------------------------------------------------------------------