[tui][tsmin] added value search & fixed get_usages
:Release Notes: - fixed get_usages bug. query awaited python, why value was installed to Python - added new method in minimal single file example, to search every value installed to some field - in TUI removed redundant input handler :Detailed Notes: - :Testing Performed: - manual testing :QA Notes: - :Issues Addressed: -
This commit is contained in:
parent
5088ccb03c
commit
00ddf1ee33
26
README.md
26
README.md
@ -14,3 +14,29 @@ $ pip install -r requirements.txt
|
||||
$ source .venv/bin/activate
|
||||
$ python3 src/ts-example
|
||||
```
|
||||
|
||||
## TUI usage
|
||||
|
||||
Start the TUI (the required `--file` argument can be the file you intend to
|
||||
inspect):
|
||||
|
||||
```bash
|
||||
$ python3 src/ts-example --tui minimal --file path/to/file.py
|
||||
```
|
||||
|
||||
At the prompt, load a file and then inspect grammar fields:
|
||||
|
||||
```text
|
||||
> read_file path/to/file.py
|
||||
> find_field_values function_definition name
|
||||
```
|
||||
|
||||
`find_field_values <node_type> <field_name>` walks every matching node and
|
||||
prints the named field and its location. Node and field names are
|
||||
language-grammar-specific. For example, C++ class names can be found with:
|
||||
|
||||
```text
|
||||
> find_field_values class_specifier name
|
||||
```
|
||||
|
||||
Other available commands include `dump_tree` and `get_usages <function>`.
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import tempfile
|
||||
from typing import Any
|
||||
from command_runner import ICommandRunner
|
||||
from arg_conf import parser as arg_parser
|
||||
from log_conf import logger
|
||||
import tree_sitter_python as tspython
|
||||
|
||||
import tree_sitter_cpp as tscpp
|
||||
from tree_sitter import Language, Parser, Tree
|
||||
import tree_sitter_python as tspython
|
||||
from arg_conf import parser as arg_parser
|
||||
from command_runner import ICommandRunner
|
||||
from log_conf import logger
|
||||
from tree_sitter import Language, Parser, Query, QueryCursor, Tree
|
||||
|
||||
PY_LANGUAGE = Language(tspython.language())
|
||||
CPP_LANGUAGE = Language(tscpp.language())
|
||||
|
||||
|
||||
def arguments():
|
||||
global arg_parser
|
||||
subparsers = arg_parser.add_subparsers(
|
||||
@ -19,31 +21,147 @@ def arguments():
|
||||
|
||||
module_parser = subparsers.add_parser("minimal")
|
||||
|
||||
module_parser.add_argument("--language", help="Setup parsing language", default="Python")
|
||||
module_parser.add_argument(
|
||||
"--language", help="Setup parsing language", default="Python"
|
||||
)
|
||||
module_parser.add_argument("--file", "-f", help="File to parse", required=True)
|
||||
logger.debug("Added Tree Sitter Minimal Example Arguments to the list")
|
||||
|
||||
|
||||
class SingleFileParser(ICommandRunner):
|
||||
def __init__ (self, args):
|
||||
def __init__(self, args):
|
||||
self.normalized_language: Language
|
||||
self.tree: Tree
|
||||
self.parser: Parser
|
||||
if args.language == 'Python':
|
||||
self.parser = Parser(PY_LANGUAGE)
|
||||
elif args.language == 'C++':
|
||||
language = args.language.casefold()
|
||||
if language in {"c++", "cpp"}:
|
||||
self.parser = Parser(CPP_LANGUAGE)
|
||||
self.normalized_language = CPP_LANGUAGE
|
||||
self.language_name = "cpp"
|
||||
else:
|
||||
self.parser = Parser(PY_LANGUAGE)
|
||||
args.language = 'Python'
|
||||
logger.info(f"Parser is installed to {args.language}")
|
||||
self.normalized_language = PY_LANGUAGE
|
||||
self.language_name = "python"
|
||||
logger.info(f"Parser is installed to {self.language_name}")
|
||||
|
||||
def node_text(self, node, source: bytes) -> str:
|
||||
return source[node.start_byte : node.end_byte].decode(
|
||||
"utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
def get_usages(self):
|
||||
pass
|
||||
def final_callee_name(self, callee) -> str:
|
||||
"""Return the last named component of a call expression.
|
||||
|
||||
Selecting this from the syntax tree is more reliable than splitting
|
||||
source text: template arguments (``obj.foo<int>()``) and Python
|
||||
subscriptions (``obj.foo[T]()``) may themselves contain ``.`` or
|
||||
``::``.
|
||||
"""
|
||||
# These are grammar field names used by Python attributes/subscripts
|
||||
# and C++ field expressions, qualified identifiers, and templates.
|
||||
for field_name in ("attribute", "field", "name", "value", "argument"):
|
||||
child = callee.child_by_field_name(field_name)
|
||||
if child is not None:
|
||||
return self.final_callee_name(child)
|
||||
return self.node_text(callee, self.source_code)
|
||||
|
||||
def get_usages(self, function: str):
|
||||
query_source: str = ""
|
||||
if self.language_name == "python":
|
||||
query_source = """
|
||||
(call
|
||||
function: (_) @callee
|
||||
arguments: (argument_list) @arguments
|
||||
) @call
|
||||
"""
|
||||
elif self.language_name == "cpp":
|
||||
query_source = """
|
||||
(call_expression
|
||||
function: (_) @callee
|
||||
arguments: (argument_list) @arguments
|
||||
) @call
|
||||
"""
|
||||
|
||||
query = Query(
|
||||
self.normalized_language,
|
||||
query_source,
|
||||
)
|
||||
|
||||
cursor = QueryCursor(query)
|
||||
found = False
|
||||
for _, captures in cursor.matches(self.tree.root_node):
|
||||
call = captures["call"][0]
|
||||
callee = captures["callee"][0]
|
||||
arguments = captures["arguments"][0]
|
||||
|
||||
callee_text = self.node_text(callee, self.source_code)
|
||||
|
||||
final_name = self.final_callee_name(callee)
|
||||
|
||||
if function not in {callee_text, final_name}:
|
||||
continue
|
||||
|
||||
found = True
|
||||
|
||||
logger.info(
|
||||
"Call found: %s",
|
||||
{
|
||||
"call": self.node_text(call, self.source_code),
|
||||
"callee": callee_text,
|
||||
"arguments": self.node_text(
|
||||
arguments,
|
||||
self.source_code,
|
||||
),
|
||||
"line": call.start_point.row + 1,
|
||||
"column": call.start_point.column + 1,
|
||||
},
|
||||
)
|
||||
|
||||
if not found:
|
||||
logger.info("No usages found for %r", function)
|
||||
|
||||
def find_field_values(self, node_type: str, field_name: str):
|
||||
"""Find the value of ``field_name`` on every node of ``node_type``.
|
||||
|
||||
Unlike a query capture, this demonstrates navigating a grammar field
|
||||
directly with Node.child_by_field_name().
|
||||
"""
|
||||
found = False
|
||||
nodes = [self.tree.root_node]
|
||||
|
||||
while nodes:
|
||||
node = nodes.pop()
|
||||
nodes.extend(reversed(node.children))
|
||||
|
||||
if node.type != node_type:
|
||||
continue
|
||||
|
||||
field_value = node.child_by_field_name(field_name)
|
||||
if field_value is None:
|
||||
continue
|
||||
|
||||
found = True
|
||||
logger.info(
|
||||
"%s.%s: %s (line %d, column %d)",
|
||||
node_type,
|
||||
field_name,
|
||||
self.node_text(field_value, self.source_code),
|
||||
field_value.start_point.row + 1,
|
||||
field_value.start_point.column + 1,
|
||||
)
|
||||
|
||||
if not found:
|
||||
logger.info(
|
||||
"No %r fields found on %r nodes",
|
||||
field_name,
|
||||
node_type,
|
||||
)
|
||||
|
||||
def read_file(self, filename: str):
|
||||
with open(filename, 'rb') as file:
|
||||
source_code = file.read()
|
||||
self.tree = self.parser.parse(source_code)
|
||||
with open(filename, "rb") as file:
|
||||
self.source_code = file.read()
|
||||
self.tree = self.parser.parse(self.source_code)
|
||||
|
||||
def dump_tree(self):
|
||||
with tempfile.TemporaryFile(mode="w+b", buffering=0) as output:
|
||||
@ -61,26 +179,34 @@ class SingleFileParser(ICommandRunner):
|
||||
self.read_file(args.file)
|
||||
self.dump_tree()
|
||||
tokens = command.split()
|
||||
if tokens[0] == 'read_file':
|
||||
if tokens[0] == "read_file":
|
||||
logger.debug(f"{tokens[0]} called with {tokens[1]}")
|
||||
self.read_file(tokens[1])
|
||||
if tokens[0] == 'dump_tree':
|
||||
if tokens[0] == "dump_tree":
|
||||
self.dump_tree()
|
||||
|
||||
if tokens[0] == "get_usages":
|
||||
self.get_usages(tokens[1])
|
||||
if tokens[0] == "find_field_values":
|
||||
self.find_field_values(tokens[1], tokens[2])
|
||||
|
||||
def get_commands(self) -> dict[str, Any]:
|
||||
return {"simple_file":{}, "read_file": {}, "dump_tree":{}}
|
||||
return {
|
||||
"simple_file": {},
|
||||
"read_file": {},
|
||||
"dump_tree": {},
|
||||
"get_usages": {},
|
||||
"find_field_values": {},
|
||||
}
|
||||
|
||||
def get_positional_path_args(self) -> dict[str, set[int]]:
|
||||
return {
|
||||
"read_file": {0}
|
||||
}
|
||||
return {"read_file": {0}}
|
||||
|
||||
def get_path_options(self) -> set[str]:
|
||||
return {
|
||||
"read_file",
|
||||
}
|
||||
|
||||
|
||||
def simple_file():
|
||||
global arg_parser
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
@ -4,6 +4,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from command_runner import ICommandRunner
|
||||
from log_conf import logger
|
||||
from textual import on
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.geometry import Offset, Region, Spacing
|
||||
@ -248,7 +249,7 @@ class CommandAutoComplete(AutoCompleteAbove):
|
||||
state = self._get_target_state()
|
||||
|
||||
if self._is_path_context(state):
|
||||
text_before_cursor = state.text[:state.cursor_position]
|
||||
text_before_cursor = state.text[: state.cursor_position]
|
||||
|
||||
# After "src/", show the contents of src even though
|
||||
# the current path segment is empty.
|
||||
@ -257,15 +258,11 @@ class CommandAutoComplete(AutoCompleteAbove):
|
||||
|
||||
return super().should_show_dropdown(search_string)
|
||||
|
||||
|
||||
def post_completion(self) -> None:
|
||||
state = self._get_target_state()
|
||||
text_before_cursor = state.text[:state.cursor_position]
|
||||
text_before_cursor = state.text[: state.cursor_position]
|
||||
|
||||
if (
|
||||
self._is_path_context(state)
|
||||
and text_before_cursor.endswith(("/", "\\"))
|
||||
):
|
||||
if self._is_path_context(state) and text_before_cursor.endswith(("/", "\\")):
|
||||
# apply_completion suppresses Input.Changed, so explicitly
|
||||
# rebuild candidates for the newly selected directory.
|
||||
search_string = self.get_search_string(state)
|
||||
@ -318,6 +315,7 @@ class CommandAutoComplete(AutoCompleteAbove):
|
||||
|
||||
self.target.cursor_position = start + len(new_token)
|
||||
|
||||
|
||||
class LogLine(Message):
|
||||
"""A line to display in the TUI output pane."""
|
||||
|
||||
@ -401,35 +399,10 @@ class ConsoleApp(App[None]):
|
||||
self.runner = runner
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._log_handler = RichLogHandler(self)
|
||||
self._log_handler.setFormatter(
|
||||
logging.Formatter(
|
||||
"%(asctime)s %(levelname)s %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
)
|
||||
|
||||
# Replace terminal-based logging handlers while the TUI is running.
|
||||
self._root_logger = logging.getLogger()
|
||||
self._previous_handlers = self._root_logger.handlers[:]
|
||||
self._previous_level = self._root_logger.level
|
||||
|
||||
for handler in self._previous_handlers:
|
||||
self._root_logger.removeHandler(handler)
|
||||
|
||||
self._root_logger.addHandler(self._log_handler)
|
||||
self._root_logger.setLevel(logging.INFO)
|
||||
|
||||
logging.info("Application started")
|
||||
logger.info("Application started")
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
# Restore the application's previous logging configuration.
|
||||
self._root_logger.removeHandler(self._log_handler)
|
||||
|
||||
for handler in self._previous_handlers:
|
||||
self._root_logger.addHandler(handler)
|
||||
|
||||
self._root_logger.setLevel(self._previous_level)
|
||||
logger.info("on_unmount")
|
||||
|
||||
@on(LogLine)
|
||||
def display_log_line(self, message: LogLine) -> None:
|
||||
@ -452,7 +425,7 @@ class ConsoleApp(App[None]):
|
||||
try:
|
||||
self.run_command(command)
|
||||
except Exception:
|
||||
logging.exception("Command failed")
|
||||
logger.exception("Command failed")
|
||||
|
||||
def get_commands(self) -> dict[str, Any]:
|
||||
return {"help": {}, "error": {}, "quit": {}, "exit": {}}
|
||||
@ -461,11 +434,11 @@ class ConsoleApp(App[None]):
|
||||
"""Replace this with your application logic."""
|
||||
|
||||
if command == "help":
|
||||
logging.info("Commands: help, error, quit")
|
||||
logger.info("Commands: help, error, quit")
|
||||
|
||||
elif command == "error":
|
||||
raise RuntimeError("Example failure")
|
||||
|
||||
else:
|
||||
logging.debug(f"Delegated {command} to runner")
|
||||
logger.debug(f"Delegated {command} to runner")
|
||||
self.runner.run_command(command)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user