lsp: Add document outline

This commit is contained in:
James Westman 2023-07-23 21:11:00 -05:00
parent 950b141d26
commit e087aeb44f
24 changed files with 469 additions and 28 deletions

View file

@ -20,7 +20,6 @@
""" Utilities for parsing an AST from a token stream. """
import typing as T
from collections import defaultdict
from enum import Enum
from .ast_utils import AstNode
@ -31,7 +30,7 @@ from .errors import (
UnexpectedTokenError,
assert_true,
)
from .tokenizer import Token, TokenType
from .tokenizer import Range, Token, TokenType
SKIP_TOKENS = [TokenType.COMMENT, TokenType.WHITESPACE]
@ -63,14 +62,16 @@ class ParseGroup:
be converted to AST nodes by passing the children and key=value pairs to
the AST node constructor."""
def __init__(self, ast_type: T.Type[AstNode], start: int):
def __init__(self, ast_type: T.Type[AstNode], start: int, text: str):
self.ast_type = ast_type
self.children: T.List[ParseGroup] = []
self.keys: T.Dict[str, T.Any] = {}
self.tokens: T.Dict[str, T.Optional[Token]] = {}
self.ranges: T.Dict[str, Range] = {}
self.start = start
self.end: T.Optional[int] = None
self.incomplete = False
self.text = text
def add_child(self, child: "ParseGroup"):
self.children.append(child)
@ -81,6 +82,10 @@ class ParseGroup:
self.keys[key] = val
self.tokens[key] = token
def set_range(self, key: str, range: Range):
assert_true(key not in self.ranges)
self.ranges[key] = range
def to_ast(self):
"""Creates an AST node from the match group."""
children = [child.to_ast() for child in self.children]
@ -104,8 +109,9 @@ class ParseGroup:
class ParseContext:
"""Contains the state of the parser."""
def __init__(self, tokens: T.List[Token], index=0):
def __init__(self, tokens: T.List[Token], text: str, index=0):
self.tokens = tokens
self.text = text
self.binding_power = 0
self.index = index
@ -113,6 +119,7 @@ class ParseContext:
self.group: T.Optional[ParseGroup] = None
self.group_keys: T.Dict[str, T.Tuple[T.Any, T.Optional[Token]]] = {}
self.group_children: T.List[ParseGroup] = []
self.group_ranges: T.Dict[str, Range] = {}
self.last_group: T.Optional[ParseGroup] = None
self.group_incomplete = False
@ -124,7 +131,7 @@ class ParseContext:
context will be used to parse one node. If parsing is successful, the
new context will be applied to "self". If parsing fails, the new
context will be discarded."""
ctx = ParseContext(self.tokens, self.index)
ctx = ParseContext(self.tokens, self.text, self.index)
ctx.errors = self.errors
ctx.warnings = self.warnings
ctx.binding_power = self.binding_power
@ -140,6 +147,8 @@ class ParseContext:
other.group.set_val(key, val, token)
for child in other.group_children:
other.group.add_child(child)
for key, range in other.group_ranges.items():
other.group.set_range(key, range)
other.group.end = other.tokens[other.index - 1].end
other.group.incomplete = other.group_incomplete
self.group_children.append(other.group)
@ -148,6 +157,7 @@ class ParseContext:
# its matched values
self.group_keys = {**self.group_keys, **other.group_keys}
self.group_children += other.group_children
self.group_ranges = {**self.group_ranges, **other.group_ranges}
self.group_incomplete |= other.group_incomplete
self.index = other.index
@ -161,13 +171,19 @@ class ParseContext:
def start_group(self, ast_type: T.Type[AstNode]):
"""Sets this context to have its own match group."""
assert_true(self.group is None)
self.group = ParseGroup(ast_type, self.tokens[self.index].start)
self.group = ParseGroup(ast_type, self.tokens[self.index].start, self.text)
def set_group_val(self, key: str, value: T.Any, token: T.Optional[Token]):
"""Sets a matched key=value pair on the current match group."""
assert_true(key not in self.group_keys)
self.group_keys[key] = (value, token)
def set_mark(self, key: str):
"""Sets a zero-length range on the current match group at the current position."""
self.group_ranges[key] = Range(
self.tokens[self.index].start, self.tokens[self.index].start, self.text
)
def set_group_incomplete(self):
"""Marks the current match group as incomplete (it could not be fully
parsed, but the parser recovered)."""
@ -604,6 +620,15 @@ class Keyword(ParseNode):
return str(token) == self.kw
class Mark(ParseNode):
def __init__(self, key: str):
self.key = key
def _parse(self, ctx: ParseContext):
ctx.set_mark(self.key)
return True
def to_parse_node(value) -> ParseNode:
if isinstance(value, str):
return Match(value)