from typing import Optional from docutils import nodes from docutils.nodes import NodeVisitor from lsprotocol.types import DocumentSymbol from lsprotocol.types import Position from lsprotocol.types import Range from lsprotocol.types import SymbolKind class SymbolVisitor(NodeVisitor): """A visitor used to build the hierarchy we return from a ``textDocument/documentSymbol`` request. """ def __init__(self, rst, *args, **kwargs): super().__init__(*args, **kwargs) self.logger = rst.logger self.symbols = [] self.symbol_stack = [] @property def current_symbol(self) -> Optional[DocumentSymbol]: if len(self.symbol_stack) == 0: return None return self.symbol_stack[-1] def push_symbol(self): symbol = DocumentSymbol( name="", kind=SymbolKind.String, range=Range( start=Position(line=1, character=0), end=Position(line=1, character=10), ), selection_range=Range( start=Position(line=1, character=0), end=Position(line=1, character=10), ), children=[], ) current_symbol = self.current_symbol if not current_symbol: self.symbols.append(symbol) else: if current_symbol.children is None: current_symbol.children = [symbol] else: current_symbol.children.append(symbol) self.symbol_stack.append(symbol) return symbol def pop_symbol(self): self.symbol_stack.pop() def visit_section(self, node: nodes.Node) -> None: self.push_symbol() def depart_section(self, node: nodes.Node) -> None: self.pop_symbol() def visit_title(self, node: nodes.Node) -> None: symbol = self.current_symbol has_parent = True if symbol is None: has_parent = False symbol = self.push_symbol() name = node.astext() line = (node.line or 1) - 1 symbol.name = name symbol.range.start.line = line symbol.range.end.line = line symbol.range.end.character = len(name) - 1 symbol.selection_range.start.line = line symbol.selection_range.end.line = line symbol.selection_range.end.character = len(name) - 1 if not has_parent: self.pop_symbol() def depart_title(self, node: nodes.Node) -> None: pass def visit_a_directive(self, node: nodes.Element): symbol = self.push_symbol() name = node["text"] # type: ignore line = (node.line or 1) - 1 symbol.name = name symbol.kind = SymbolKind.Class symbol.range.start.line = line symbol.range.end.line = line symbol.range.end.character = len(name) - 1 symbol.selection_range.start.line = line symbol.selection_range.end.line = line symbol.selection_range.end.character = len(name) - 1 def depart_a_directive(self, node: nodes.Node): self.pop_symbol() # TODO: Enable symbols for roles # However the reported line numbers can be inaccurate... def visit_a_role(self, node: nodes.Node) -> None: ... def depart_a_role(self, node: nodes.Node) -> None: ... # TODO: Enable symbols for definition list items # However the reported line numbers appear to be inaccurate... def visit_Text(self, node: nodes.Node) -> None: pass def depart_Text(self, node: nodes.Node) -> None: pass def unknown_visit(self, node: nodes.Node) -> None: pass def unknown_departure(self, node: nodes.Node) -> None: pass