import tree_sitter import tree_sitter_python from pathlib import Path class CodeOutliner: def __init__(self): self.language = tree_sitter.Language(tree_sitter_python.language()) self.parser = tree_sitter.Parser(self.language) def outline(self, code: str) -> str: tree = self.parser.parse(bytes(code, "utf8")) lines = code.splitlines() output = [] def get_docstring(node): # In Python, docstring is usually the first expression statement in a block body = node.child_by_field_name("body") if body and body.type == "block": for child in body.children: if child.type == "comment": continue if child.type == "expression_statement": expr = child.children[0] if expr.type == "string": doc = code[expr.start_byte:expr.end_byte].strip() # Strip quotes if doc.startswith(('"""', "'''")): doc = doc[3:-3] elif doc.startswith(('"', "'")): doc = doc[1:-1] return doc.splitlines()[0] if doc else "" break return None def walk(node, indent=0): node_type = node.type name = None if node_type == "class_definition": name_node = node.child_by_field_name("name") if name_node: name = code[name_node.start_byte:name_node.end_byte] start_line = node.start_point.row + 1 end_line = node.end_point.row + 1 output.append(f"{' ' * indent}[Class] {name} (Lines {start_line}-{end_line})") doc = get_docstring(node) if doc: output.append(f"{' ' * (indent + 1)}\"\"\"{doc}\"\"\"") elif node_type in ("function_definition", "async_function_definition"): name_node = node.child_by_field_name("name") if name_node: name = code[name_node.start_byte:name_node.end_byte] start_line = node.start_point.row + 1 end_line = node.end_point.row + 1 prefix = "[Async Func]" if node_type == "async_function_definition" else "[Func]" # Check if it's a method (parent is a class body) parent = node.parent while parent and parent.type != "class_definition": if parent.type == "module": break parent = parent.parent if parent and parent.type == "class_definition": prefix = "[Method]" output.append(f"{' ' * indent}{prefix} {name} (Lines {start_line}-{end_line})") doc = get_docstring(node) if doc: output.append(f"{' ' * (indent + 1)}\"\"\"{doc}\"\"\"") for child in node.children: # Don't recurse into function bodies for outlining functions, # but we DO want to recurse into classes to find methods. if node_type == "class_definition": if child.type == "block": walk(child, indent + 1) elif node_type == "module": walk(child, indent) elif node_type == "block": walk(child, indent) walk(tree.root_node) return "\n".join(output) def get_outline(path: Path, code: str) -> str: suffix = path.suffix.lower() if suffix == ".py": outliner = CodeOutliner() return outliner.outline(code) else: return f"Outlining not supported for {suffix} files yet."