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")) |
|