[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
|
$ source .venv/bin/activate
|
||||||
$ python3 src/ts-example
|
$ 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
|
import tempfile
|
||||||
from typing import Any
|
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
|
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())
|
PY_LANGUAGE = Language(tspython.language())
|
||||||
CPP_LANGUAGE = Language(tscpp.language())
|
CPP_LANGUAGE = Language(tscpp.language())
|
||||||
|
|
||||||
|
|
||||||
def arguments():
|
def arguments():
|
||||||
global arg_parser
|
global arg_parser
|
||||||
subparsers = arg_parser.add_subparsers(
|
subparsers = arg_parser.add_subparsers(
|
||||||
@ -19,31 +21,147 @@ def arguments():
|
|||||||
|
|
||||||
module_parser = subparsers.add_parser("minimal")
|
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)
|
module_parser.add_argument("--file", "-f", help="File to parse", required=True)
|
||||||
logger.debug("Added Tree Sitter Minimal Example Arguments to the list")
|
logger.debug("Added Tree Sitter Minimal Example Arguments to the list")
|
||||||
|
|
||||||
|
|
||||||
class SingleFileParser(ICommandRunner):
|
class SingleFileParser(ICommandRunner):
|
||||||
def __init__ (self, args):
|
def __init__(self, args):
|
||||||
|
self.normalized_language: Language
|
||||||
self.tree: Tree
|
self.tree: Tree
|
||||||
self.parser: Parser
|
self.parser: Parser
|
||||||
if args.language == 'Python':
|
language = args.language.casefold()
|
||||||
self.parser = Parser(PY_LANGUAGE)
|
if language in {"c++", "cpp"}:
|
||||||
elif args.language == 'C++':
|
|
||||||
self.parser = Parser(CPP_LANGUAGE)
|
self.parser = Parser(CPP_LANGUAGE)
|
||||||
|
self.normalized_language = CPP_LANGUAGE
|
||||||
|
self.language_name = "cpp"
|
||||||
else:
|
else:
|
||||||
self.parser = Parser(PY_LANGUAGE)
|
self.parser = Parser(PY_LANGUAGE)
|
||||||
args.language = 'Python'
|
self.normalized_language = PY_LANGUAGE
|
||||||
logger.info(f"Parser is installed to {args.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):
|
def final_callee_name(self, callee) -> str:
|
||||||
pass
|
"""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):
|
def read_file(self, filename: str):
|
||||||
with open(filename, 'rb') as file:
|
with open(filename, "rb") as file:
|
||||||
source_code = file.read()
|
self.source_code = file.read()
|
||||||
self.tree = self.parser.parse(source_code)
|
self.tree = self.parser.parse(self.source_code)
|
||||||
|
|
||||||
def dump_tree(self):
|
def dump_tree(self):
|
||||||
with tempfile.TemporaryFile(mode="w+b", buffering=0) as output:
|
with tempfile.TemporaryFile(mode="w+b", buffering=0) as output:
|
||||||
@ -61,26 +179,34 @@ class SingleFileParser(ICommandRunner):
|
|||||||
self.read_file(args.file)
|
self.read_file(args.file)
|
||||||
self.dump_tree()
|
self.dump_tree()
|
||||||
tokens = command.split()
|
tokens = command.split()
|
||||||
if tokens[0] == 'read_file':
|
if tokens[0] == "read_file":
|
||||||
logger.debug(f"{tokens[0]} called with {tokens[1]}")
|
logger.debug(f"{tokens[0]} called with {tokens[1]}")
|
||||||
self.read_file(tokens[1])
|
self.read_file(tokens[1])
|
||||||
if tokens[0] == 'dump_tree':
|
if tokens[0] == "dump_tree":
|
||||||
self.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]:
|
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]]:
|
def get_positional_path_args(self) -> dict[str, set[int]]:
|
||||||
return {
|
return {"read_file": {0}}
|
||||||
"read_file": {0}
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_path_options(self) -> set[str]:
|
def get_path_options(self) -> set[str]:
|
||||||
return {
|
return {
|
||||||
"read_file",
|
"read_file",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def simple_file():
|
def simple_file():
|
||||||
global arg_parser
|
global arg_parser
|
||||||
args = arg_parser.parse_args()
|
args = arg_parser.parse_args()
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from command_runner import ICommandRunner
|
from command_runner import ICommandRunner
|
||||||
|
from log_conf import logger
|
||||||
from textual import on
|
from textual import on
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.geometry import Offset, Region, Spacing
|
from textual.geometry import Offset, Region, Spacing
|
||||||
@ -248,7 +249,7 @@ class CommandAutoComplete(AutoCompleteAbove):
|
|||||||
state = self._get_target_state()
|
state = self._get_target_state()
|
||||||
|
|
||||||
if self._is_path_context(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
|
# After "src/", show the contents of src even though
|
||||||
# the current path segment is empty.
|
# the current path segment is empty.
|
||||||
@ -257,15 +258,11 @@ class CommandAutoComplete(AutoCompleteAbove):
|
|||||||
|
|
||||||
return super().should_show_dropdown(search_string)
|
return super().should_show_dropdown(search_string)
|
||||||
|
|
||||||
|
|
||||||
def post_completion(self) -> None:
|
def post_completion(self) -> None:
|
||||||
state = self._get_target_state()
|
state = self._get_target_state()
|
||||||
text_before_cursor = state.text[:state.cursor_position]
|
text_before_cursor = state.text[: state.cursor_position]
|
||||||
|
|
||||||
if (
|
if self._is_path_context(state) and text_before_cursor.endswith(("/", "\\")):
|
||||||
self._is_path_context(state)
|
|
||||||
and text_before_cursor.endswith(("/", "\\"))
|
|
||||||
):
|
|
||||||
# apply_completion suppresses Input.Changed, so explicitly
|
# apply_completion suppresses Input.Changed, so explicitly
|
||||||
# rebuild candidates for the newly selected directory.
|
# rebuild candidates for the newly selected directory.
|
||||||
search_string = self.get_search_string(state)
|
search_string = self.get_search_string(state)
|
||||||
@ -318,6 +315,7 @@ class CommandAutoComplete(AutoCompleteAbove):
|
|||||||
|
|
||||||
self.target.cursor_position = start + len(new_token)
|
self.target.cursor_position = start + len(new_token)
|
||||||
|
|
||||||
|
|
||||||
class LogLine(Message):
|
class LogLine(Message):
|
||||||
"""A line to display in the TUI output pane."""
|
"""A line to display in the TUI output pane."""
|
||||||
|
|
||||||
@ -401,35 +399,10 @@ class ConsoleApp(App[None]):
|
|||||||
self.runner = runner
|
self.runner = runner
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self._log_handler = RichLogHandler(self)
|
logger.info("Application started")
|
||||||
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")
|
|
||||||
|
|
||||||
def on_unmount(self) -> None:
|
def on_unmount(self) -> None:
|
||||||
# Restore the application's previous logging configuration.
|
logger.info("on_unmount")
|
||||||
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)
|
|
||||||
|
|
||||||
@on(LogLine)
|
@on(LogLine)
|
||||||
def display_log_line(self, message: LogLine) -> None:
|
def display_log_line(self, message: LogLine) -> None:
|
||||||
@ -452,7 +425,7 @@ class ConsoleApp(App[None]):
|
|||||||
try:
|
try:
|
||||||
self.run_command(command)
|
self.run_command(command)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Command failed")
|
logger.exception("Command failed")
|
||||||
|
|
||||||
def get_commands(self) -> dict[str, Any]:
|
def get_commands(self) -> dict[str, Any]:
|
||||||
return {"help": {}, "error": {}, "quit": {}, "exit": {}}
|
return {"help": {}, "error": {}, "quit": {}, "exit": {}}
|
||||||
@ -461,11 +434,11 @@ class ConsoleApp(App[None]):
|
|||||||
"""Replace this with your application logic."""
|
"""Replace this with your application logic."""
|
||||||
|
|
||||||
if command == "help":
|
if command == "help":
|
||||||
logging.info("Commands: help, error, quit")
|
logger.info("Commands: help, error, quit")
|
||||||
|
|
||||||
elif command == "error":
|
elif command == "error":
|
||||||
raise RuntimeError("Example failure")
|
raise RuntimeError("Example failure")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.debug(f"Delegated {command} to runner")
|
logger.debug(f"Delegated {command} to runner")
|
||||||
self.runner.run_command(command)
|
self.runner.run_command(command)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user