|
1 |
| -import subprocess |
| 1 | +import functools |
| 2 | +import os |
2 | 3 |
|
3 | 4 | import gdb
|
| 5 | +import pygments |
| 6 | +import pygments.lexers |
| 7 | +import pygments.formatters |
4 | 8 |
|
| 9 | +import libpython |
| 10 | +import libpython_extensions |
| 11 | +import tui_windows |
5 | 12 |
|
6 |
| -def highlight_python(text): |
| 13 | + |
| 14 | +def highlight_python(content: bytes) -> bytes: |
7 | 15 | """
|
8 |
| - Pipe bytes through the highlight program to add Python syntax highlighting. |
| 16 | + Applies Python syntax highlighting and prepends line numbers to provided content. |
9 | 17 | """
|
10 |
| - if getattr(highlight_python, "failed", False): |
11 |
| - return text |
12 |
| - result = subprocess.run( |
13 |
| - ["highlight", "--syntax=python", "--out-format=ansi"], |
14 |
| - stdout=subprocess.PIPE, |
15 |
| - input=text, |
16 |
| - check=False, |
| 18 | + return pygments.highlight( |
| 19 | + content, pygments.lexers.PythonLexer(), pygments.formatters.TerminalFormatter(linenos=True) |
17 | 20 | )
|
18 |
| - if result.returncode: |
19 |
| - print("Failed to provide syntax highlighting for Python.") |
20 |
| - print("Please install the `highlight` program.") |
21 |
| - highlight_python.failed = True |
22 |
| - return text |
23 |
| - return result.stdout |
24 | 21 |
|
25 | 22 |
|
26 |
| -def register_window(name): |
| 23 | +@functools.cache |
| 24 | +def get_highlighted_file_content(filename: str) -> str: |
27 | 25 | """
|
28 |
| - Register a TUI window and define a new layout for it. |
| 26 | + Returns the content of the Python source file with syntax highlighting. |
29 | 27 | """
|
| 28 | + with libpython_extensions.PythonSubstitutePath.open(os.fsencode(filename), "r") as f: |
| 29 | + content = f.read() |
| 30 | + return highlight_python(content) |
30 | 31 |
|
31 |
| - def decorator(cls): |
32 |
| - gdb.register_window_type(name, cls) |
33 |
| - gdb.execute(f"tui new-layout {name} {name} 1 status 1 cmd 1") |
34 |
| - return cls |
35 |
| - |
36 |
| - return decorator |
37 |
| - |
38 |
| - |
39 |
| -class Window: |
40 |
| - title: str | None = None |
41 |
| - |
42 |
| - def __init__(self, tui_window): |
43 |
| - self._tui_window = tui_window |
44 |
| - self._tui_window.title = self.title |
45 |
| - gdb.events.before_prompt.connect(self.render) |
46 | 32 |
|
47 |
| - def get_lines(self): |
48 |
| - raise NotImplementedError() |
| 33 | +def get_filename_and_line() -> tuple[str, int]: |
| 34 | + """ |
| 35 | + Returns the path to the current Python source file and the current line number. |
| 36 | + """ |
| 37 | + # py-list requires an actual PyEval_EvalFrameEx frame: |
| 38 | + frame = libpython.Frame.get_selected_bytecode_frame() |
| 39 | + if not frame: |
| 40 | + raise gdb.error("Unable to locate gdb frame for python bytecode interpreter") |
49 | 41 |
|
50 |
| - def render(self): |
51 |
| - if not self._tui_window.is_valid(): |
52 |
| - return |
| 42 | + pyop = frame.get_pyop() |
| 43 | + if not pyop or pyop.is_optimized_out(): |
| 44 | + raise gdb.error(libpython.UNABLE_READ_INFO_PYTHON_FRAME) |
53 | 45 |
|
54 |
| - # Truncate output |
55 |
| - lines = self.get_lines()[:self._tui_window.height] |
56 |
| - lines = (line[:self._tui_window.width - 1] for line in lines) |
| 46 | + filename = pyop.filename() |
| 47 | + lineno = pyop.current_line_num() |
| 48 | + if lineno is None: |
| 49 | + raise gdb.error("Unable to read python frame line number") |
| 50 | + return filename, lineno |
57 | 51 |
|
58 |
| - output = "\n".join(lines) |
59 |
| - self._tui_window.write(output, True) |
60 | 52 |
|
61 |
| - def close(self): |
62 |
| - gdb.events.before_prompt.disconnect(self.render) |
| 53 | +@tui_windows.register_window("python-source") |
| 54 | +class PythonSourceWindow(tui_windows.ScrollableWindow): |
| 55 | + title = "Python Source" |
63 | 56 |
|
| 57 | + def get_content(self): |
| 58 | + filename, line = get_filename_and_line() |
| 59 | + lines = get_highlighted_file_content(filename).splitlines() |
| 60 | + prefixed_lines = [(" > " if i == line else " ") + l for i, l in enumerate(lines, start=1)] |
64 | 61 |
|
65 |
| -@register_window("python-source") |
66 |
| -class PythonSourceWindow(Window): |
67 |
| - title = "Python Source" |
| 62 | + # Set vertical scroll offset to center the current line |
| 63 | + half_window_height = self._tui_window.height // 2 |
| 64 | + self.vscroll_offset = line - half_window_height |
68 | 65 |
|
69 |
| - def get_lines(self): |
70 |
| - python_source = gdb.execute("py-list", to_string=True).encode("utf-8") |
71 |
| - return highlight_python(python_source).decode("utf-8").splitlines() |
| 66 | + return "\n".join(prefixed_lines) |
72 | 67 |
|
73 | 68 |
|
74 |
| -@register_window("python-backtrace") |
75 |
| -class PythonBacktraceWindow(Window): |
| 69 | +@tui_windows.register_window("python-backtrace") |
| 70 | +class PythonBacktraceWindow(tui_windows.ScrollableWindow): |
76 | 71 | title = "Python Backtrace"
|
77 | 72 |
|
78 |
| - def get_lines(self): |
79 |
| - return gdb.execute("py-bt", to_string=True).splitlines() |
| 73 | + def get_content(self): |
| 74 | + return gdb.execute("py-bt", to_string=True) |
80 | 75 |
|
81 | 76 |
|
82 |
| -@register_window("python-locals") |
83 |
| -class PythonLocalsWindow(Window): |
| 77 | +@tui_windows.register_window("python-locals") |
| 78 | +class PythonLocalsWindow(tui_windows.ScrollableWindow): |
84 | 79 | title = "Local Python Variables"
|
85 | 80 |
|
86 |
| - def get_lines(self): |
87 |
| - return gdb.execute("py-locals", to_string=True).splitlines() |
| 81 | + def get_content(self): |
| 82 | + return gdb.execute("py-locals", to_string=True) |
88 | 83 |
|
89 | 84 |
|
90 |
| -@register_window("python-bytecode") |
91 |
| -class PythonBytecodeWindow(Window): |
| 85 | +@tui_windows.register_window("python-bytecode") |
| 86 | +class PythonBytecodeWindow(tui_windows.ScrollableWindow): |
92 | 87 | title = "Python Bytecode"
|
93 | 88 |
|
94 |
| - def get_lines(self): |
| 89 | + def get_lines(self) -> list[str]: |
95 | 90 | lines = gdb.execute("py-dis", to_string=True).splitlines()
|
96 |
| - total_lines = len(lines) |
97 |
| - height = self._tui_window.height |
98 |
| - if total_lines < height: |
99 |
| - return lines |
100 | 91 |
|
101 |
| - current_line = None |
102 |
| - for index, line in enumerate(lines, 1): |
| 92 | + # Set vertical scroll offset to center the current line |
| 93 | + for index, line in enumerate(lines, start=1): |
103 | 94 | if "-->" in line:
|
104 |
| - current_line = index |
105 |
| - break |
106 |
| - else: |
107 |
| - return lines[:height] |
| 95 | + half_window_height = self._tui_window.height // 2 |
| 96 | + self.vscroll_offset = index - half_window_height |
108 | 97 |
|
109 |
| - first_half = height // 2 |
110 |
| - second_half = height - first_half |
111 |
| - if current_line < first_half: |
112 |
| - return lines[:height] |
113 |
| - if current_line + second_half > total_lines: |
114 |
| - return lines[-height:] |
115 |
| - return lines[current_line - first_half : current_line + second_half] |
| 98 | + return lines |
116 | 99 |
|
117 | 100 |
|
118 | 101 | # Define a layout with all Python windows
|
119 | 102 | gdb.execute(
|
120 | 103 | " ".join(
|
121 | 104 | (
|
122 | 105 | "tui new-layout python",
|
123 |
| - "python-backtrace 2", |
124 |
| - "{-horizontal python-bytecode 1 python-locals 1} 2", |
125 |
| - "python-source 2", |
126 |
| - "status 1 cmd 1", |
| 106 | + "{-horizontal {python-source 2 status 1 cmd 1} 3", |
| 107 | + "{python-locals 1 python-backtrace 1 python-bytecode 1 timeline 1} 2} 1", |
127 | 108 | )
|
128 | 109 | )
|
129 | 110 | )
|
0 commit comments