Skip to content

Commit 98dfb24

Browse files
authored
Merge pull request #64 from hit9/dev1
Editor: A simple language server for bitproto
2 parents 36d9eed + 3399e9e commit 98dfb24

32 files changed

+5757
-291
lines changed

changes.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,40 @@
11
.. currentmodule:: bitproto
22

3+
Version 1.2.0
4+
-------------
5+
6+
.. _version-1.2.0:
7+
8+
- Feature: Add a simple language-server, tested on neovim and vscode.
9+
- Editor: Upgrade vscode extenions to support the ``bitproto-language-server``.
10+
311
Version 1.1.2
412
-------------
513

6-
.. _version-1.1.2
14+
.. _version-1.1.2:
715

816
- Feature: Allow using constants as option values. ISSUE #61, PR #63
917

1018
Version 1.1.1
1119
-------------
1220

13-
.. _version-1.1.1
21+
.. _version-1.1.1:
1422

1523
- Fix bug: enum importing other bitproto's field name generation bug. #53 #52
1624
- Fix bug: import statements of bitprotos should be placed ahead of other declarations. #53
1725

1826
Version 1.1.0
1927
-------------
2028

21-
.. _version-1.1.0
29+
.. _version-1.1.0:
2230

2331
- Performance improvements for C bitprotolib, 40~60us improvement per call on stm32. PR #48.
2432
- Fix Python nested message ``__post_init___`` function code generation. PR #48, commit 73f4b01.
2533

2634
Version 1.0.1
2735
-------------
2836

29-
.. _version-1.0.1
37+
.. _version-1.0.1:
3038

3139
- Add support for Python 3.11
3240

compiler/MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ recursive-exclude *.egg-info *
77
recursive-exclude .git *
88
recursive-exclude .mypy_cache *
99
recursive-exclude __pycache__ *
10+
recursive-exclude .ruff_cache *

compiler/bitproto/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
99
"""
1010

11-
__version__ = "1.1.2"
11+
__version__ = "1.2.0"
1212
__description__ = "bit level data interchange format."

compiler/bitproto/_ast.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
| | |- Enum :Scope:Definition:Node
4040
| | |- Message :Scope:Definition:Node
4141
| | |- Proto :Scope:Definition:Node
42+
|- Reference :Node
4243
"""
4344

4445
from collections import OrderedDict as dict_
@@ -130,11 +131,18 @@ def cache_if_frozen_condition(
130131
class Node:
131132
token: str = ""
132133
lineno: int = 0
134+
# column start and end of this token in the line, starting from 0.
135+
# to get the col end: col_start + len(token)
136+
token_col_start: int = 0
133137
filepath: str = ""
134138

135139
# cheat mypy: override by @frozen
136140
__frozen__: ClassVar[bool] = False
137141

142+
@property
143+
def token_col_end(self) -> int:
144+
return self.token_col_start + len(self.token)
145+
138146
def is_frozen(self) -> bool:
139147
return self.__frozen__
140148

@@ -192,6 +200,18 @@ def __repr__(self) -> str:
192200
return f"<{class_repr} {self.name}>"
193201

194202

203+
@dataclass
204+
class Reference(Node):
205+
"""
206+
Reference to a definition.
207+
"""
208+
209+
referenced_definition: Optional[Definition] = None
210+
211+
def __repr__(self) -> str:
212+
return f"<{self.token}>"
213+
214+
195215
@dataclass
196216
class BoundDefinition(Definition):
197217
"""BoundDefinition is definitions bound to a proto,
@@ -368,6 +388,12 @@ class Scope(Definition):
368388

369389
members: "dict_[str, Definition]" = dataclass_field(default_factory=dict_)
370390

391+
# scope's start and end (normally, the '{' and '}' positions)
392+
scope_start_lineno: int = 0
393+
scope_start_col: int = 0
394+
scope_end_lineno: int = 0
395+
scope_end_col: int = 0
396+
371397
def push_member(self, member: Definition, name: Optional[str] = None) -> None:
372398
"""Push a definition `member`, and run hook functions around.
373399
Raises error if given member's name already taken by another member.
@@ -1037,6 +1063,8 @@ def validate_message_field_on_push(self, field: MessageField) -> None:
10371063
class Proto(ScopeWithOptions):
10381064
__option_descriptors__: ClassVar[OptionDescriptors] = PROTO_OPTTIONS
10391065

1066+
references: List[Reference] = dataclass_field(default_factory=list)
1067+
10401068
def set_name(self, name: str) -> None:
10411069
if self.is_frozen():
10421070
raise InternalError("set_name on frozen proto")

compiler/bitproto/parser.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
MessageField,
2727
Option,
2828
Proto,
29+
Reference,
2930
Scope,
3031
Type,
3132
)
@@ -189,12 +190,9 @@ def util_parse_sequence(self, p: P) -> None:
189190
p[0] = []
190191

191192
def copy_p_tracking(self, p: P, from_: int = 1, to: int = 0) -> None:
192-
"""Don't know why P's tracking info (lexpos and lineno) sometimes missing.
193-
Particular in recursion grammar situation. We have to copy it manually.
194-
195-
Add this function in a p_xxx function when:
196-
1. the p[0] is gona to be used in another parsing target.
197-
2. and the tracking information is gona to be used there.
193+
"""
194+
Ply's position tracking works only for lexing SYMBOLS (not for all grammer symbols) by default.
195+
We either enable parse(tracking=True), or copy them on need manually.
198196
"""
199197
p.set_lexpos(to, p.lexpos(from_))
200198
p.set_lineno(to, p.lineno(from_))
@@ -213,13 +211,20 @@ def p_open_global_scope(self, p: P) -> None:
213211
filepath=self.current_filepath(),
214212
_bound=None,
215213
scope_stack=self.current_scope_stack(),
214+
scope_start_lineno=1,
215+
scope_start_col=1,
216216
)
217217
self.push_scope(proto)
218218

219219
@override_docstring(r_close_global_scope)
220220
def p_close_global_scope(self, p: P) -> None:
221221
scope = self.pop_scope()
222222
proto = cast_or_raise(Proto, scope)
223+
224+
proto.scope_end_lineno = p.lexer.lexdata.count("\n") # FIXME: slow?
225+
lexpos = len(p.lexer.lexdata)
226+
proto.scope_end_col = lexpos - p.lexer.lexdata.rfind("\n", 0, lexpos)
227+
223228
if not proto.name:
224229
raise ProtoNameUndefined(filepath=self.current_filepath())
225230
proto.freeze()
@@ -334,6 +339,7 @@ def p_option(self, p: P) -> None:
334339
filepath=self.current_filepath(),
335340
lineno=p.lineno(2),
336341
token=p[2],
342+
token_col_start=self._get_col(p, 2),
337343
)
338344
self.current_scope().push_member(option)
339345

@@ -357,6 +363,7 @@ def p_alias(self, p: P) -> None:
357363
filepath=self.current_filepath(),
358364
lineno=lineno,
359365
token=token,
366+
token_col_start=self._get_col(p, 2) if len(p) == 6 else self._get_col(p, 3),
360367
indent=self.current_indent(p),
361368
scope_stack=self.current_scope_stack(),
362369
comment_block=self.collect_comment_block(),
@@ -384,6 +391,7 @@ def p_const(self, p: P) -> None:
384391
_bound=self.current_proto(),
385392
filepath=self.current_filepath(),
386393
token=p[2],
394+
token_col_start=self._get_col(p, 2),
387395
lineno=p.lineno(2),
388396
)
389397
self.current_scope().push_member(constant)
@@ -466,6 +474,15 @@ def p_constant_reference(self, p: P) -> None:
466474
p[0] = d
467475
self.copy_p_tracking(p)
468476

477+
reference = Reference(
478+
token=p[1],
479+
lineno=p.lineno(1),
480+
token_col_start=self._get_col(p, 1),
481+
filepath=self.current_filepath(),
482+
referenced_definition=d,
483+
)
484+
self.current_proto().references.append(reference)
485+
469486
@override_docstring(r_type)
470487
def p_type(self, p: P) -> None:
471488
p[0] = p[1]
@@ -498,9 +515,19 @@ def p_type_reference(self, p: P) -> None:
498515
token=p[1],
499516
lineno=p.lineno(1),
500517
)
518+
501519
p[0] = d
502520
self.copy_p_tracking(p)
503521

522+
reference = Reference(
523+
token=p[1],
524+
lineno=p.lineno(1),
525+
token_col_start=self._get_col(p, 1),
526+
filepath=self.current_filepath(),
527+
referenced_definition=d,
528+
)
529+
self.current_proto().references.append(reference)
530+
504531
@override_docstring(r_optional_extensible_flag)
505532
def p_optional_extensible_flag(self, p: P) -> None:
506533
extensible = len(p) == 2
@@ -517,6 +544,7 @@ def p_array_type(self, p: P) -> None:
517544
cap=p[3],
518545
extensible=p[5],
519546
token="{0}[{1}]".format(p[1], p[3]),
547+
token_col_start=self._get_col(p, 1),
520548
lineno=p.lineno(2),
521549
filepath=self.current_filepath(),
522550
)
@@ -552,12 +580,15 @@ def p_open_enum_scope(self, p: P) -> None:
552580
name=p[2],
553581
type=p[4],
554582
token=p[2],
583+
token_col_start=self._get_col(p, 2),
555584
lineno=p.lineno(2),
556585
filepath=self.current_filepath(),
557586
indent=self.current_indent(p),
558587
comment_block=self.collect_comment_block(),
559588
scope_stack=self.current_scope_stack(),
560589
_bound=self.current_proto(),
590+
scope_start_lineno=p.lineno(5), # '{'
591+
scope_start_col=self._get_col(p, 5), # '{'
561592
)
562593
self.push_scope(enum)
563594

@@ -567,7 +598,10 @@ def p_enum_scope(self, p: P) -> None:
567598

568599
@override_docstring(r_close_enum_scope)
569600
def p_close_enum_scope(self, p: P) -> None:
570-
self.pop_scope().freeze()
601+
enum = self.pop_scope()
602+
enum.scope_end_lineno = p.lineno(1)
603+
enum.scope_end_col = self._get_col(p, 1)
604+
enum.freeze()
571605

572606
@override_docstring(r_enum_items)
573607
def p_enum_items(self, p: P) -> None:
@@ -605,6 +639,7 @@ def p_enum_field(self, p: P) -> None:
605639
name=name,
606640
value=value,
607641
token=p[1],
642+
token_col_start=self._get_col(p, 1),
608643
lineno=p.lineno(1),
609644
indent=self.current_indent(p),
610645
filepath=self.current_filepath(),
@@ -626,18 +661,24 @@ def p_open_message_scope(self, p: P) -> None:
626661
name=p[2],
627662
extensible=p[3],
628663
token=p[2],
664+
token_col_start=self._get_col(p, 2),
629665
lineno=p.lineno(2),
630666
filepath=self.current_filepath(),
631667
indent=self.current_indent(p),
632668
comment_block=self.collect_comment_block(),
633669
scope_stack=self.current_scope_stack(),
634670
_bound=self.current_proto(),
671+
scope_start_lineno=p.lineno(4), # '{'
672+
scope_start_col=self._get_col(p, 4), # '{'
635673
)
636674
self.push_scope(message)
637675

638676
@override_docstring(r_close_message_scope)
639677
def p_close_message_scope(self, p: P) -> None:
640-
self.pop_scope().freeze()
678+
message = self.pop_scope()
679+
message.scope_end_lineno = p.lineno(1) # '}'
680+
message.scope_end_col = self._get_col(p, 1) # '}'
681+
message.freeze()
641682

642683
@override_docstring(r_message_scope)
643684
def p_message_scope(self, p: P) -> None:
@@ -673,6 +714,7 @@ def p_message_field(self, p: P) -> None:
673714
type=type,
674715
number=field_number,
675716
token=p[2],
717+
token_col_start=self._get_col(p, 2),
676718
lineno=p.lineno(2),
677719
filepath=self.current_filepath(),
678720
comment_block=self.collect_comment_block(),
@@ -685,6 +727,7 @@ def p_message_field(self, p: P) -> None:
685727
@override_docstring(r_message_field_name)
686728
def p_message_field_name(self, p: P) -> None:
687729
p[0] = p[1]
730+
self.copy_p_tracking(p) # from 1 to 0
688731

689732
@override_docstring(r_boolean_literal)
690733
def p_boolean_literal(self, p: P) -> None:
@@ -700,7 +743,7 @@ def p_string_literal(self, p: P) -> None:
700743

701744
@override_docstring(r_dotted_identifier)
702745
def p_dotted_identifier(self, p: P) -> None:
703-
self.copy_p_tracking(p)
746+
self.copy_p_tracking(p) # from 1 => 0
704747
if len(p) == 4:
705748
p[0] = ".".join([p[1], p[3]])
706749
elif len(p) == 2:
@@ -716,6 +759,13 @@ def p_error(self, p: P) -> None:
716759
raise GrammarError(filepath=filepath, token=p.value(1), lineno=p.lineno(1))
717760
raise GrammarError()
718761

762+
def _get_col(self, p: P, k: int) -> int:
763+
lexpos = p.lexpos(k)
764+
# we dont use `last_newline_pos` here,
765+
# because the recursive parsing may result a deeper `last_newline_pos`.
766+
last_newline = p.lexer.lexdata.rfind("\n", 0, lexpos)
767+
return lexpos - max(last_newline, 0)
768+
719769

720770
def parse(filepath: str, traditional_mode: bool = False) -> Proto:
721771
"""Parse a bitproto from given filepath.
@@ -726,3 +776,14 @@ def parse(filepath: str, traditional_mode: bool = False) -> Proto:
726776
extensible grammar is used in traditional mode.
727777
"""
728778
return Parser(traditional_mode=traditional_mode).parse(filepath)
779+
780+
781+
def parse_string(
782+
content: str, traditional_mode: bool = False, filepath: str = ""
783+
) -> Proto:
784+
"""
785+
Parse a bitproto from string.
786+
"""
787+
return Parser(traditional_mode=traditional_mode).parse_string(
788+
content, filepath=filepath
789+
)

docs/language.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,13 @@ The bitproto naming guidelines are introduced in following code example:
663663
Editor Integration
664664
^^^^^^^^^^^^^^^^^^
665665

666+
Language Server
667+
""""""""""""""""
668+
669+
A simple language server for bitproto: `bitproto-language-server github <https://github.com/hit9/bitproto/tree/master/editors/language_server>`_.
670+
671+
This language_server is tested on neovim and vscode.
672+
666673
Vim
667674
"""
668675
A syntax plugin for `vim <https://www.vim.org/>`_ is available from

0 commit comments

Comments
 (0)