src/eric7/UI/PythonAstViewer.py

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9413
80c06d472826
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
11 import ast 11 import ast
12 12
13 from PyQt6.QtCore import pyqtSlot, Qt, QTimer 13 from PyQt6.QtCore import pyqtSlot, Qt, QTimer
14 from PyQt6.QtGui import QBrush 14 from PyQt6.QtGui import QBrush
15 from PyQt6.QtWidgets import ( 15 from PyQt6.QtWidgets import (
16 QTreeWidget, QTreeWidgetItem, QAbstractItemView, QWidget, QVBoxLayout 16 QTreeWidget,
17 QTreeWidgetItem,
18 QAbstractItemView,
19 QWidget,
20 QVBoxLayout,
17 ) 21 )
18 22
19 from asttokens import ASTTokens 23 from asttokens import ASTTokens
20 24
21 from EricGui.EricOverrideCursor import EricOverrideCursor 25 from EricGui.EricOverrideCursor import EricOverrideCursor
26 class PythonAstViewer(QWidget): 30 class PythonAstViewer(QWidget):
27 """ 31 """
28 Class implementing a widget to visualize the Python AST for some Python 32 Class implementing a widget to visualize the Python AST for some Python
29 sources. 33 sources.
30 """ 34 """
35
31 StartLineRole = Qt.ItemDataRole.UserRole 36 StartLineRole = Qt.ItemDataRole.UserRole
32 StartIndexRole = Qt.ItemDataRole.UserRole + 1 37 StartIndexRole = Qt.ItemDataRole.UserRole + 1
33 EndLineRole = Qt.ItemDataRole.UserRole + 2 38 EndLineRole = Qt.ItemDataRole.UserRole + 2
34 EndIndexRole = Qt.ItemDataRole.UserRole + 3 39 EndIndexRole = Qt.ItemDataRole.UserRole + 3
35 40
36 def __init__(self, viewmanager, parent=None): 41 def __init__(self, viewmanager, parent=None):
37 """ 42 """
38 Constructor 43 Constructor
39 44
40 @param viewmanager reference to the viewmanager object 45 @param viewmanager reference to the viewmanager object
41 @type ViewManager 46 @type ViewManager
42 @param parent reference to the parent widget 47 @param parent reference to the parent widget
43 @type QWidget 48 @type QWidget
44 """ 49 """
45 super().__init__(parent) 50 super().__init__(parent)
46 51
47 self.__layout = QVBoxLayout(self) 52 self.__layout = QVBoxLayout(self)
48 self.setLayout(self.__layout) 53 self.setLayout(self.__layout)
49 self.__astWidget = QTreeWidget(self) 54 self.__astWidget = QTreeWidget(self)
50 self.__layout.addWidget(self.__astWidget) 55 self.__layout.addWidget(self.__astWidget)
51 self.__layout.setContentsMargins(0, 0, 0, 0) 56 self.__layout.setContentsMargins(0, 0, 0, 0)
52 57
53 self.__vm = viewmanager 58 self.__vm = viewmanager
54 self.__vmConnected = False 59 self.__vmConnected = False
55 60
56 self.__editor = None 61 self.__editor = None
57 self.__source = "" 62 self.__source = ""
58 63
59 self.__astWidget.setHeaderLabels([self.tr("Node"), 64 self.__astWidget.setHeaderLabels([self.tr("Node"), self.tr("Code Range")])
60 self.tr("Code Range")])
61 self.__astWidget.setSortingEnabled(False) 65 self.__astWidget.setSortingEnabled(False)
62 self.__astWidget.setSelectionBehavior( 66 self.__astWidget.setSelectionBehavior(
63 QAbstractItemView.SelectionBehavior.SelectRows) 67 QAbstractItemView.SelectionBehavior.SelectRows
68 )
64 self.__astWidget.setSelectionMode( 69 self.__astWidget.setSelectionMode(
65 QAbstractItemView.SelectionMode.SingleSelection) 70 QAbstractItemView.SelectionMode.SingleSelection
71 )
66 self.__astWidget.setAlternatingRowColors(True) 72 self.__astWidget.setAlternatingRowColors(True)
67 73
68 self.__errorColor = QBrush( 74 self.__errorColor = QBrush(Preferences.getPython("ASTViewerErrorColor"))
69 Preferences.getPython("ASTViewerErrorColor")) 75
70
71 self.__astWidget.itemClicked.connect(self.__astItemClicked) 76 self.__astWidget.itemClicked.connect(self.__astItemClicked)
72 77
73 self.__vm.astViewerStateChanged.connect(self.__astViewerStateChanged) 78 self.__vm.astViewerStateChanged.connect(self.__astViewerStateChanged)
74 79
75 self.hide() 80 self.hide()
76 81
77 def __editorChanged(self, editor): 82 def __editorChanged(self, editor):
78 """ 83 """
79 Private slot to handle a change of the current editor. 84 Private slot to handle a change of the current editor.
80 85
81 @param editor reference to the current editor 86 @param editor reference to the current editor
82 @type Editor 87 @type Editor
83 """ 88 """
84 if editor is not self.__editor: 89 if editor is not self.__editor:
85 if self.__editor: 90 if self.__editor:
86 self.__editor.clearAllHighlights() 91 self.__editor.clearAllHighlights()
87 self.__editor = editor 92 self.__editor = editor
88 if self.__editor: 93 if self.__editor:
89 self.__loadAST() 94 self.__loadAST()
90 95
91 def __editorSaved(self, editor): 96 def __editorSaved(self, editor):
92 """ 97 """
93 Private slot to reload the AST after the connected editor was saved. 98 Private slot to reload the AST after the connected editor was saved.
94 99
95 @param editor reference to the editor that performed a save action 100 @param editor reference to the editor that performed a save action
96 @type Editor 101 @type Editor
97 """ 102 """
98 if editor and editor is self.__editor: 103 if editor and editor is self.__editor:
99 self.__loadAST() 104 self.__loadAST()
100 105
101 def __editorDoubleClicked(self, editor, pos, buttons): 106 def __editorDoubleClicked(self, editor, pos, buttons):
102 """ 107 """
103 Private slot to handle a mouse button double click in the editor. 108 Private slot to handle a mouse button double click in the editor.
104 109
105 @param editor reference to the editor, that emitted the signal 110 @param editor reference to the editor, that emitted the signal
106 @type Editor 111 @type Editor
107 @param pos position of the double click 112 @param pos position of the double click
108 @type QPoint 113 @type QPoint
109 @param buttons mouse buttons that were double clicked 114 @param buttons mouse buttons that were double clicked
111 """ 116 """
112 if editor is self.__editor and buttons == Qt.MouseButton.LeftButton: 117 if editor is self.__editor and buttons == Qt.MouseButton.LeftButton:
113 if editor.isModified(): 118 if editor.isModified():
114 # reload the source 119 # reload the source
115 QTimer.singleShot(0, self.__loadAST) 120 QTimer.singleShot(0, self.__loadAST)
116 121
117 # highlight the corresponding entry 122 # highlight the corresponding entry
118 QTimer.singleShot(0, self.__selectItemForEditorSelection) 123 QTimer.singleShot(0, self.__selectItemForEditorSelection)
119 QTimer.singleShot(0, self.__grabFocus) 124 QTimer.singleShot(0, self.__grabFocus)
120 125
121 def __editorLanguageChanged(self, editor): 126 def __editorLanguageChanged(self, editor):
122 """ 127 """
123 Private slot to handle a change of the editor language. 128 Private slot to handle a change of the editor language.
124 129
125 @param editor reference to the editor which changed language 130 @param editor reference to the editor which changed language
126 @type Editor 131 @type Editor
127 """ 132 """
128 if editor is self.__editor: 133 if editor is self.__editor:
129 QTimer.singleShot(0, self.__loadDIS) 134 QTimer.singleShot(0, self.__loadDIS)
130 135
131 def __lastEditorClosed(self): 136 def __lastEditorClosed(self):
132 """ 137 """
133 Private slot to handle the last editor closed signal of the view 138 Private slot to handle the last editor closed signal of the view
134 manager. 139 manager.
135 """ 140 """
136 self.hide() 141 self.hide()
137 142
138 def show(self): 143 def show(self):
139 """ 144 """
140 Public slot to show the AST viewer. 145 Public slot to show the AST viewer.
141 """ 146 """
142 super().show() 147 super().show()
143 148
144 if not self.__vmConnected: 149 if not self.__vmConnected:
145 self.__vm.editorChangedEd.connect(self.__editorChanged) 150 self.__vm.editorChangedEd.connect(self.__editorChanged)
146 self.__vm.editorSavedEd.connect(self.__editorSaved) 151 self.__vm.editorSavedEd.connect(self.__editorSaved)
147 self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked) 152 self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked)
148 self.__vm.editorLanguageChanged.connect( 153 self.__vm.editorLanguageChanged.connect(self.__editorLanguageChanged)
149 self.__editorLanguageChanged)
150 self.__vmConnected = True 154 self.__vmConnected = True
151 155
152 def hide(self): 156 def hide(self):
153 """ 157 """
154 Public slot to hide the AST viewer. 158 Public slot to hide the AST viewer.
155 """ 159 """
156 super().hide() 160 super().hide()
157 161
158 if self.__editor: 162 if self.__editor:
159 self.__editor.clearAllHighlights() 163 self.__editor.clearAllHighlights()
160 164
161 if self.__vmConnected: 165 if self.__vmConnected:
162 self.__vm.editorChangedEd.disconnect(self.__editorChanged) 166 self.__vm.editorChangedEd.disconnect(self.__editorChanged)
163 self.__vm.editorSavedEd.disconnect(self.__editorSaved) 167 self.__vm.editorSavedEd.disconnect(self.__editorSaved)
164 self.__vm.editorDoubleClickedEd.disconnect( 168 self.__vm.editorDoubleClickedEd.disconnect(self.__editorDoubleClicked)
165 self.__editorDoubleClicked) 169 self.__vm.editorLanguageChanged.disconnect(self.__editorLanguageChanged)
166 self.__vm.editorLanguageChanged.disconnect(
167 self.__editorLanguageChanged)
168 self.__vmConnected = False 170 self.__vmConnected = False
169 171
170 def shutdown(self): 172 def shutdown(self):
171 """ 173 """
172 Public method to perform shutdown actions. 174 Public method to perform shutdown actions.
173 """ 175 """
174 self.__editor = None 176 self.__editor = None
175 177
176 def __astViewerStateChanged(self, on): 178 def __astViewerStateChanged(self, on):
177 """ 179 """
178 Private slot to toggle the display of the AST viewer. 180 Private slot to toggle the display of the AST viewer.
179 181
180 @param on flag indicating to show the AST 182 @param on flag indicating to show the AST
181 @type bool 183 @type bool
182 """ 184 """
183 editor = self.__vm.activeWindow() 185 editor = self.__vm.activeWindow()
184 if on: 186 if on:
187 self.show() 189 self.show()
188 self.__loadAST() 190 self.__loadAST()
189 else: 191 else:
190 self.hide() 192 self.hide()
191 self.__editor = None 193 self.__editor = None
192 194
193 def __createErrorItem(self, error): 195 def __createErrorItem(self, error):
194 """ 196 """
195 Private method to create a top level error item. 197 Private method to create a top level error item.
196 198
197 @param error error message 199 @param error error message
198 @type str 200 @type str
199 @return generated item 201 @return generated item
200 @rtype QTreeWidgetItem 202 @rtype QTreeWidgetItem
201 """ 203 """
202 itm = QTreeWidgetItem(self.__astWidget, [error]) 204 itm = QTreeWidgetItem(self.__astWidget, [error])
203 itm.setFirstColumnSpanned(True) 205 itm.setFirstColumnSpanned(True)
204 itm.setForeground(0, self.__errorColor) 206 itm.setForeground(0, self.__errorColor)
205 return itm 207 return itm
206 208
207 def __loadAST(self): 209 def __loadAST(self):
208 """ 210 """
209 Private method to generate the AST from the source of the current 211 Private method to generate the AST from the source of the current
210 editor and visualize it. 212 editor and visualize it.
211 """ 213 """
212 if not self.__editor: 214 if not self.__editor:
213 self.__createErrorItem(self.tr( 215 self.__createErrorItem(self.tr("No editor has been opened."))
214 "No editor has been opened."
215 ))
216 return 216 return
217 217
218 self.__astWidget.clear() 218 self.__astWidget.clear()
219 self.__editor.clearAllHighlights() 219 self.__editor.clearAllHighlights()
220 220
221 source = self.__editor.text() 221 source = self.__editor.text()
222 if not source.strip(): 222 if not source.strip():
223 # empty editor or white space only 223 # empty editor or white space only
224 self.__createErrorItem(self.tr( 224 self.__createErrorItem(
225 "The current editor does not contain any source code." 225 self.tr("The current editor does not contain any source code.")
226 )) 226 )
227 return 227 return
228 228
229 if not self.__editor.isPyFile(): 229 if not self.__editor.isPyFile():
230 self.__createErrorItem(self.tr( 230 self.__createErrorItem(
231 "The current editor does not contain Python source code." 231 self.tr("The current editor does not contain Python source code.")
232 )) 232 )
233 return 233 return
234 234
235 with EricOverrideCursor(): 235 with EricOverrideCursor():
236 try: 236 try:
237 # generate the AST 237 # generate the AST
238 root = ast.parse(source, self.__editor.getFileName(), "exec") 238 root = ast.parse(source, self.__editor.getFileName(), "exec")
239 self.__markTextRanges(root, source) 239 self.__markTextRanges(root, source)
240 astValid = True 240 astValid = True
241 except Exception as exc: 241 except Exception as exc:
242 self.__createErrorItem(str(exc)) 242 self.__createErrorItem(str(exc))
243 astValid = False 243 astValid = False
244 244
245 if astValid: 245 if astValid:
246 self.setUpdatesEnabled(False) 246 self.setUpdatesEnabled(False)
247 247
248 # populate the AST tree 248 # populate the AST tree
249 self.__populateNode(self.tr("Module"), root, self.__astWidget) 249 self.__populateNode(self.tr("Module"), root, self.__astWidget)
250 self.__selectItemForEditorSelection() 250 self.__selectItemForEditorSelection()
251 QTimer.singleShot(0, self.__resizeColumns) 251 QTimer.singleShot(0, self.__resizeColumns)
252 252
253 self.setUpdatesEnabled(True) 253 self.setUpdatesEnabled(True)
254 254
255 self.__grabFocus() 255 self.__grabFocus()
256 256
257 def __populateNode(self, name, nodeOrFields, parent): 257 def __populateNode(self, name, nodeOrFields, parent):
258 """ 258 """
259 Private method to populate the tree view with a node. 259 Private method to populate the tree view with a node.
260 260
261 @param name name of the node 261 @param name name of the node
262 @type str 262 @type str
263 @param nodeOrFields reference to the node or a list node fields 263 @param nodeOrFields reference to the node or a list node fields
264 @type ast.AST or list 264 @type ast.AST or list
265 @param parent reference to the parent item 265 @param parent reference to the parent item
275 else: 275 else:
276 value = "[...]" 276 value = "[...]"
277 else: 277 else:
278 fields = [] 278 fields = []
279 value = repr(nodeOrFields) 279 value = repr(nodeOrFields)
280 280
281 text = self.tr("{0}: {1}").format(name, value) 281 text = self.tr("{0}: {1}").format(name, value)
282 itm = QTreeWidgetItem(parent, [text]) 282 itm = QTreeWidgetItem(parent, [text])
283 itm.setExpanded(True) 283 itm.setExpanded(True)
284 284
285 if ( 285 if hasattr(nodeOrFields, "lineno") and hasattr(nodeOrFields, "col_offset"):
286 hasattr(nodeOrFields, "lineno") and
287 hasattr(nodeOrFields, "col_offset")
288 ):
289 itm.setData(0, self.StartLineRole, nodeOrFields.lineno) 286 itm.setData(0, self.StartLineRole, nodeOrFields.lineno)
290 itm.setData(0, self.StartIndexRole, nodeOrFields.col_offset) 287 itm.setData(0, self.StartIndexRole, nodeOrFields.col_offset)
291 startStr = self.tr("{0},{1}").format( 288 startStr = self.tr("{0},{1}").format(
292 nodeOrFields.lineno, nodeOrFields.col_offset) 289 nodeOrFields.lineno, nodeOrFields.col_offset
290 )
293 endStr = "" 291 endStr = ""
294 292
295 if ( 293 if hasattr(nodeOrFields, "end_lineno") and hasattr(
296 hasattr(nodeOrFields, "end_lineno") and 294 nodeOrFields, "end_col_offset"
297 hasattr(nodeOrFields, "end_col_offset")
298 ): 295 ):
299 itm.setData(0, self.EndLineRole, nodeOrFields.end_lineno) 296 itm.setData(0, self.EndLineRole, nodeOrFields.end_lineno)
300 itm.setData(0, self.EndIndexRole, 297 itm.setData(0, self.EndIndexRole, nodeOrFields.end_col_offset)
301 nodeOrFields.end_col_offset)
302 endStr = self.tr("{0},{1}").format( 298 endStr = self.tr("{0},{1}").format(
303 nodeOrFields.end_lineno, nodeOrFields.end_col_offset) 299 nodeOrFields.end_lineno, nodeOrFields.end_col_offset
300 )
304 else: 301 else:
305 itm.setData(0, self.EndLineRole, nodeOrFields.lineno) 302 itm.setData(0, self.EndLineRole, nodeOrFields.lineno)
306 itm.setData(0, self.EndIndexRole, 303 itm.setData(0, self.EndIndexRole, nodeOrFields.col_offset + 1)
307 nodeOrFields.col_offset + 1)
308 if endStr: 304 if endStr:
309 rangeStr = self.tr("{0} - {1}").format(startStr, endStr) 305 rangeStr = self.tr("{0} - {1}").format(startStr, endStr)
310 else: 306 else:
311 rangeStr = startStr 307 rangeStr = startStr
312 308
313 itm.setText(1, rangeStr) 309 itm.setText(1, rangeStr)
314 310
315 for fieldName, fieldValue in fields: 311 for fieldName, fieldValue in fields:
316 self.__populateNode(fieldName, fieldValue, itm) 312 self.__populateNode(fieldName, fieldValue, itm)
317 313
318 def __markTextRanges(self, tree, source): 314 def __markTextRanges(self, tree, source):
319 """ 315 """
320 Private method to modify the AST nodes with end_lineno and 316 Private method to modify the AST nodes with end_lineno and
321 end_col_offset information. 317 end_col_offset information.
322 318
323 Note: The modifications are only done for nodes containing lineno and 319 Note: The modifications are only done for nodes containing lineno and
324 col_offset attributes. 320 col_offset attributes.
325 321
326 @param tree reference to the AST to be modified 322 @param tree reference to the AST to be modified
327 @type ast.AST 323 @type ast.AST
328 @param source source code the AST was created from 324 @param source source code the AST was created from
329 @type str 325 @type str
330 """ 326 """
331 ASTTokens(source, tree=tree) 327 ASTTokens(source, tree=tree)
332 for child in ast.walk(tree): 328 for child in ast.walk(tree):
333 if hasattr(child, 'last_token'): 329 if hasattr(child, "last_token"):
334 child.end_lineno, child.end_col_offset = child.last_token.end 330 child.end_lineno, child.end_col_offset = child.last_token.end
335 if hasattr(child, 'lineno'): 331 if hasattr(child, "lineno"):
336 # Fixes problems with some nodes like binop 332 # Fixes problems with some nodes like binop
337 child.lineno, child.col_offset = child.first_token.start 333 child.lineno, child.col_offset = child.first_token.start
338 334
339 def __findClosestContainingNode(self, node, textRange): 335 def __findClosestContainingNode(self, node, textRange):
340 """ 336 """
341 Private method to search for the AST node that contains a range 337 Private method to search for the AST node that contains a range
342 closest. 338 closest.
343 339
344 @param node AST node to start searching at 340 @param node AST node to start searching at
345 @type ast.AST 341 @type ast.AST
346 @param textRange tuple giving the start and end positions 342 @param textRange tuple giving the start and end positions
347 @type tuple of (int, int, int, int) 343 @type tuple of (int, int, int, int)
348 @return best matching node 344 @return best matching node
349 @rtype ast.AST 345 @rtype ast.AST
350 """ 346 """
351 if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]: 347 if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]:
352 # no valid range, i.e. no selection 348 # no valid range, i.e. no selection
353 return None 349 return None
354 350
355 # first look among children 351 # first look among children
356 for child in ast.iter_child_nodes(node): 352 for child in ast.iter_child_nodes(node):
357 result = self.__findClosestContainingNode(child, textRange) 353 result = self.__findClosestContainingNode(child, textRange)
358 if result is not None: 354 if result is not None:
359 return result 355 return result
360 356
361 # no suitable child was found 357 # no suitable child was found
362 if hasattr(node, "lineno") and self.__rangeContainsSmaller( 358 if hasattr(node, "lineno") and self.__rangeContainsSmaller(
363 (node.lineno, node.col_offset, node.end_lineno, 359 (node.lineno, node.col_offset, node.end_lineno, node.end_col_offset),
364 node.end_col_offset), textRange): 360 textRange,
361 ):
365 return node 362 return node
366 else: 363 else:
367 # nope 364 # nope
368 return None 365 return None
369 366
370 def __findClosestContainingItem(self, itm, textRange): 367 def __findClosestContainingItem(self, itm, textRange):
371 """ 368 """
372 Private method to search for the tree item that contains a range 369 Private method to search for the tree item that contains a range
373 closest. 370 closest.
374 371
375 @param itm tree item to start searching at 372 @param itm tree item to start searching at
376 @type QTreeWidgetItem 373 @type QTreeWidgetItem
377 @param textRange tuple giving the start and end positions 374 @param textRange tuple giving the start and end positions
378 @type tuple of (int, int, int, int) 375 @type tuple of (int, int, int, int)
379 @return best matching tree item 376 @return best matching tree item
380 @rtype QTreeWidgetItem 377 @rtype QTreeWidgetItem
381 """ 378 """
382 if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]: 379 if textRange in [(-1, -1, -1, -1), (0, -1, 0, -1)]:
383 # no valid range, i.e. no selection 380 # no valid range, i.e. no selection
384 return None 381 return None
385 382
386 lineno = itm.data(0, self.StartLineRole) 383 lineno = itm.data(0, self.StartLineRole)
387 if lineno is not None and not self.__rangeContainsSmallerOrEqual( 384 if lineno is not None and not self.__rangeContainsSmallerOrEqual(
388 (itm.data(0, self.StartLineRole), itm.data(0, self.StartIndexRole), 385 (
389 itm.data(0, self.EndLineRole), itm.data(0, self.EndIndexRole)), 386 itm.data(0, self.StartLineRole),
390 textRange): 387 itm.data(0, self.StartIndexRole),
388 itm.data(0, self.EndLineRole),
389 itm.data(0, self.EndIndexRole),
390 ),
391 textRange,
392 ):
391 return None 393 return None
392 394
393 # first look among children 395 # first look among children
394 for index in range(itm.childCount()): 396 for index in range(itm.childCount()):
395 child = itm.child(index) 397 child = itm.child(index)
396 result = self.__findClosestContainingItem(child, textRange) 398 result = self.__findClosestContainingItem(child, textRange)
397 if result is not None: 399 if result is not None:
398 return result 400 return result
399 401
400 # no suitable child was found 402 # no suitable child was found
401 lineno = itm.data(0, self.StartLineRole) 403 lineno = itm.data(0, self.StartLineRole)
402 if lineno is not None and self.__rangeContainsSmallerOrEqual( 404 if lineno is not None and self.__rangeContainsSmallerOrEqual(
403 (itm.data(0, self.StartLineRole), itm.data(0, self.StartIndexRole), 405 (
404 itm.data(0, self.EndLineRole), itm.data(0, self.EndIndexRole)), 406 itm.data(0, self.StartLineRole),
405 textRange): 407 itm.data(0, self.StartIndexRole),
408 itm.data(0, self.EndLineRole),
409 itm.data(0, self.EndIndexRole),
410 ),
411 textRange,
412 ):
406 return itm 413 return itm
407 else: 414 else:
408 # nope 415 # nope
409 return None 416 return None
410 417
411 def __resizeColumns(self): 418 def __resizeColumns(self):
412 """ 419 """
413 Private method to resize the columns to suitable values. 420 Private method to resize the columns to suitable values.
414 """ 421 """
415 for col in range(self.__astWidget.columnCount()): 422 for col in range(self.__astWidget.columnCount()):
416 self.__astWidget.resizeColumnToContents(col) 423 self.__astWidget.resizeColumnToContents(col)
417 424
418 rangeSize = self.__astWidget.columnWidth(1) + 10 425 rangeSize = self.__astWidget.columnWidth(1) + 10
419 # 10 px extra for the range 426 # 10 px extra for the range
420 nodeSize = max(400, self.__astWidget.viewport().width() - rangeSize) 427 nodeSize = max(400, self.__astWidget.viewport().width() - rangeSize)
421 self.__astWidget.setColumnWidth(0, nodeSize) 428 self.__astWidget.setColumnWidth(0, nodeSize)
422 self.__astWidget.setColumnWidth(1, rangeSize) 429 self.__astWidget.setColumnWidth(1, rangeSize)
423 430
424 def resizeEvent(self, evt): 431 def resizeEvent(self, evt):
425 """ 432 """
426 Protected method to handle resize events. 433 Protected method to handle resize events.
427 434
428 @param evt resize event 435 @param evt resize event
429 @type QResizeEvent 436 @type QResizeEvent
430 """ 437 """
431 # just adjust the sizes of the columns 438 # just adjust the sizes of the columns
432 self.__resizeColumns() 439 self.__resizeColumns()
433 440
434 def __rangeContainsSmaller(self, first, second): 441 def __rangeContainsSmaller(self, first, second):
435 """ 442 """
436 Private method to check, if second is contained in first. 443 Private method to check, if second is contained in first.
437 444
438 @param first text range to check against 445 @param first text range to check against
439 @type tuple of (int, int, int, int) 446 @type tuple of (int, int, int, int)
440 @param second text range to check for 447 @param second text range to check for
441 @type tuple of (int, int, int, int) 448 @type tuple of (int, int, int, int)
442 @return flag indicating second is contained in first 449 @return flag indicating second is contained in first
446 firstEnd = first[2:] 453 firstEnd = first[2:]
447 secondStart = second[:2] 454 secondStart = second[:2]
448 secondEnd = second[2:] 455 secondEnd = second[2:]
449 456
450 return ( 457 return (
451 (firstStart < secondStart and firstEnd > secondEnd) or 458 (firstStart < secondStart and firstEnd > secondEnd)
452 (firstStart == secondStart and firstEnd > secondEnd) or 459 or (firstStart == secondStart and firstEnd > secondEnd)
453 (firstStart < secondStart and firstEnd == secondEnd) 460 or (firstStart < secondStart and firstEnd == secondEnd)
454 ) 461 )
455 462
456 def __rangeContainsSmallerOrEqual(self, first, second): 463 def __rangeContainsSmallerOrEqual(self, first, second):
457 """ 464 """
458 Private method to check, if second is contained in or equal to first. 465 Private method to check, if second is contained in or equal to first.
459 466
460 @param first text range to check against 467 @param first text range to check against
461 @type tuple of (int, int, int, int) 468 @type tuple of (int, int, int, int)
462 @param second text range to check for 469 @param second text range to check for
463 @type tuple of (int, int, int, int) 470 @type tuple of (int, int, int, int)
464 @return flag indicating second is contained in or equal to first 471 @return flag indicating second is contained in or equal to first
465 @rtype bool 472 @rtype bool
466 """ 473 """
467 return first == second or self.__rangeContainsSmaller(first, second) 474 return first == second or self.__rangeContainsSmaller(first, second)
468 475
469 def __clearSelection(self): 476 def __clearSelection(self):
470 """ 477 """
471 Private method to clear all selected items. 478 Private method to clear all selected items.
472 """ 479 """
473 for itm in self.__astWidget.selectedItems(): 480 for itm in self.__astWidget.selectedItems():
474 itm.setSelected(False) 481 itm.setSelected(False)
475 482
476 def __selectItemForEditorSelection(self): 483 def __selectItemForEditorSelection(self):
477 """ 484 """
478 Private slot to select the item corresponding to an editor selection. 485 Private slot to select the item corresponding to an editor selection.
479 """ 486 """
480 # step 1: clear all selected items 487 # step 1: clear all selected items
481 self.__clearSelection() 488 self.__clearSelection()
482 489
483 # step 2: retrieve the editor selection 490 # step 2: retrieve the editor selection
484 selection = self.__editor.getSelection() 491 selection = self.__editor.getSelection()
485 # make the line numbers 1-based 492 # make the line numbers 1-based
486 selection = (selection[0] + 1, selection[1], 493 selection = (selection[0] + 1, selection[1], selection[2] + 1, selection[3])
487 selection[2] + 1, selection[3]) 494
488
489 # step 3: search the corresponding item, scroll to it and select it 495 # step 3: search the corresponding item, scroll to it and select it
490 itm = self.__findClosestContainingItem( 496 itm = self.__findClosestContainingItem(
491 self.__astWidget.topLevelItem(0), selection) 497 self.__astWidget.topLevelItem(0), selection
498 )
492 if itm: 499 if itm:
493 self.__astWidget.scrollToItem( 500 self.__astWidget.scrollToItem(
494 itm, QAbstractItemView.ScrollHint.PositionAtCenter) 501 itm, QAbstractItemView.ScrollHint.PositionAtCenter
502 )
495 itm.setSelected(True) 503 itm.setSelected(True)
496 504
497 def __grabFocus(self): 505 def __grabFocus(self):
498 """ 506 """
499 Private method to grab the input focus. 507 Private method to grab the input focus.
500 """ 508 """
501 self.__astWidget.setFocus(Qt.FocusReason.OtherFocusReason) 509 self.__astWidget.setFocus(Qt.FocusReason.OtherFocusReason)
502 510
503 @pyqtSlot(QTreeWidgetItem, int) 511 @pyqtSlot(QTreeWidgetItem, int)
504 def __astItemClicked(self, itm, column): 512 def __astItemClicked(self, itm, column):
505 """ 513 """
506 Private slot handling a user click on an AST node item. 514 Private slot handling a user click on an AST node item.
507 515
508 @param itm reference to the clicked item 516 @param itm reference to the clicked item
509 @type QTreeWidgetItem 517 @type QTreeWidgetItem
510 @param column column number of the click 518 @param column column number of the click
511 @type int 519 @type int
512 """ 520 """
513 self.__editor.clearAllHighlights() 521 self.__editor.clearAllHighlights()
514 522
515 if itm is not None: 523 if itm is not None:
516 startLine = itm.data(0, self.StartLineRole) 524 startLine = itm.data(0, self.StartLineRole)
517 if startLine is not None: 525 if startLine is not None:
518 startIndex = itm.data(0, self.StartIndexRole) 526 startIndex = itm.data(0, self.StartIndexRole)
519 endLine = itm.data(0, self.EndLineRole) 527 endLine = itm.data(0, self.EndLineRole)
520 endIndex = itm.data(0, self.EndIndexRole) 528 endIndex = itm.data(0, self.EndIndexRole)
521 529
522 self.__editor.gotoLine(startLine, firstVisible=True, 530 self.__editor.gotoLine(startLine, firstVisible=True, expand=True)
523 expand=True) 531 self.__editor.setHighlight(
524 self.__editor.setHighlight(startLine - 1, startIndex, 532 startLine - 1, startIndex, endLine - 1, endIndex
525 endLine - 1, endIndex) 533 )
526 534
527 @pyqtSlot() 535 @pyqtSlot()
528 def preferencesChanged(self): 536 def preferencesChanged(self):
529 """ 537 """
530 Public slot handling changes of the AST viewer settings. 538 Public slot handling changes of the AST viewer settings.
531 """ 539 """
532 self.__errorColor = QBrush( 540 self.__errorColor = QBrush(Preferences.getPython("ASTViewerErrorColor"))
533 Preferences.getPython("ASTViewerErrorColor"))

eric ide

mercurial