├── examples ├── test_all.txt ├── test_errors.txt ├── test_all.cpp ├── test_enums.hpp ├── test_templates.hpp ├── test_errors.cpp ├── test_code.cpp ├── test_comments.cpp ├── test_fields.hpp └── test_classes.cpp ├── doc ├── img │ ├── Errors.png │ ├── Input.png │ ├── Exception.png │ ├── PyClASVi_Linux.png │ └── PyClASVi_Windows.png ├── PyClASVi_usage.md └── python_clang_usage.md ├── LICENSE ├── README.md └── pyclasvi.py /examples/test_all.txt: -------------------------------------------------------------------------------- 1 | examples/test_all.cpp 2 | -xc++ 3 | -std=c++11 4 | -------------------------------------------------------------------------------- /examples/test_errors.txt: -------------------------------------------------------------------------------- 1 | examples/test_errors.cpp 2 | -Weverything 3 | -------------------------------------------------------------------------------- /doc/img/Errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FraMuCoder/PyClASVi/HEAD/doc/img/Errors.png -------------------------------------------------------------------------------- /doc/img/Input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FraMuCoder/PyClASVi/HEAD/doc/img/Input.png -------------------------------------------------------------------------------- /doc/img/Exception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FraMuCoder/PyClASVi/HEAD/doc/img/Exception.png -------------------------------------------------------------------------------- /doc/img/PyClASVi_Linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FraMuCoder/PyClASVi/HEAD/doc/img/PyClASVi_Linux.png -------------------------------------------------------------------------------- /doc/img/PyClASVi_Windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FraMuCoder/PyClASVi/HEAD/doc/img/PyClASVi_Windows.png -------------------------------------------------------------------------------- /examples/test_all.cpp: -------------------------------------------------------------------------------- 1 | #include "test_classes.cpp" 2 | #include "test_code.cpp" 3 | #include "test_comments.cpp" 4 | #include "test_enums.hpp" 5 | #include "test_fields.hpp" 6 | #include "test_templates.hpp" -------------------------------------------------------------------------------- /examples/test_enums.hpp: -------------------------------------------------------------------------------- 1 | // use this to test 2 | // enum_type 3 | // enum_value 4 | namespace test_enums 5 | { 6 | enum e1 { e1_a, e1_b, e1_c, e1_d }; 7 | enum e2 { 8 | e2_a = 5, 9 | e2_b, 10 | e2_c = 10, 11 | e2_d = 1, 12 | e2_e 13 | }; 14 | enum class ec1 { 15 | ec1_a, ec1_b, ec1_c 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /examples/test_templates.hpp: -------------------------------------------------------------------------------- 1 | // use this to test 2 | // get_num_template_arguments 3 | // get_template_xxx 4 | namespace test_templates 5 | { 6 | template 7 | T f1(T a); 8 | 9 | template <> 10 | int f1(int a); 11 | 12 | template <> 13 | float f1(float a); 14 | 15 | template 16 | class C1; 17 | 18 | template <> 19 | class C1; 20 | 21 | template 22 | class C1; 23 | } 24 | -------------------------------------------------------------------------------- /examples/test_errors.cpp: -------------------------------------------------------------------------------- 1 | // warnings 2 | 3 | #warning "Warning message 1" 4 | #warning "Warning message 2" 5 | #warning "Warning message 3" 6 | 7 | // errors 8 | 9 | #error "Error message 1" 10 | #error "Error message 2" 11 | #error "Error message 3" 12 | 13 | #pragma GCC warning "More warning" 14 | #pragma GCC error "More errors" 15 | 16 | #if abc 17 | 18 | #endif abc 19 | 20 | noReturn() 21 | { 22 | a = b; 23 | } 24 | 25 | void func() 26 | { 27 | func2(); 28 | } 29 | 30 | // fatal 31 | 32 | #include "file_not_found.h" -------------------------------------------------------------------------------- /examples/test_code.cpp: -------------------------------------------------------------------------------- 1 | // use this to test 2 | // kind 3 | // storage_class 4 | 5 | namespace 6 | { 7 | int var_a = 4; 8 | } 9 | 10 | static int var_b = 5; 11 | 12 | namespace test_code 13 | { 14 | extern int rnd(); 15 | 16 | static int s1 = var_a; 17 | 18 | void f1(void) 19 | { 20 | int a; 21 | int b = 1; 22 | auto c = 2; 23 | 24 | a = b + c; 25 | 26 | static int d = 0; 27 | 28 | for (int i = 0; i < 10; ++i) 29 | d += rnd(); 30 | 31 | volatile int e = var_b; 32 | 33 | #pragma clang diagnostic push 34 | #pragma clang diagnostic ignored "-Wdeprecated-register" 35 | register int i; 36 | #pragma clang diagnostic pop 37 | for (i = 0; i < 10; ++i) 38 | ++e; 39 | 40 | s1 = d + e; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/test_comments.cpp: -------------------------------------------------------------------------------- 1 | // use this to test 2 | // brief_com,ent 3 | // raw_comment 4 | namespace test_comments 5 | { 6 | /// @brief Class c1 7 | /// And more comment 8 | class c1 9 | { 10 | public: 11 | c1(); ///< constructor 12 | /** Function f2 */ 13 | int f2(); 14 | /** 15 | * This is Member m2, 16 | * and it's type is float. 17 | * This is all you need. 18 | */ 19 | float m2; 20 | protected: 21 | // \brief Comment 1 22 | // \return An integer 23 | int f3(int a); ///< Comment 2 24 | bool m3; 25 | private: 26 | /** 27 | * \brief Function f3 28 | * \return An other integer 29 | */ 30 | int f4(); 31 | /** 32 | * Member m4 33 | */ 34 | double m4; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /examples/test_fields.hpp: -------------------------------------------------------------------------------- 1 | // use this to test 2 | // get_bitfield_width 3 | // get_field_offset 4 | // is_anonymous 5 | // is_bitfield 6 | // type 7 | namespace test_fields 8 | { 9 | typedef struct { 10 | bool b1; 11 | bool b2; 12 | bool b3; 13 | } s0_t; 14 | 15 | union u1 16 | { 17 | s0_t s0; 18 | struct s1_t 19 | { 20 | int a : 1; 21 | int b : 2; 22 | int c : 3; 23 | int d : 4; 24 | int x : 16; 25 | } s1; 26 | struct 27 | { 28 | int i; 29 | float f; 30 | struct 31 | { 32 | int *p1; 33 | void *p2; 34 | double *p3; 35 | }; // realy anonymous 36 | } s2; 37 | struct s4 38 | { 39 | int i1; 40 | int i2; 41 | }; // anonymous? 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Frank Müller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/test_classes.cpp: -------------------------------------------------------------------------------- 1 | // use this to test 2 | // access_specifier 3 | // get_arguments 4 | // is_const_method 5 | // is_mutable_field 6 | // is_pure_virtual_methode 7 | // is_static_methode 8 | // is_virtual_methode 9 | // result_type 10 | namespace test_classes 11 | { 12 | class c1 13 | { 14 | // this is private 15 | void f1(int a); 16 | int m1; 17 | public: 18 | c1(); 19 | int f2() const; 20 | virtual void v1() = 0; 21 | virtual void v2(); 22 | static void s1(); 23 | float m2; 24 | protected: 25 | const int * f3(const int a); 26 | bool m3; 27 | private: 28 | void f4(); 29 | mutable double m4; 30 | }; 31 | 32 | struct s1 33 | { 34 | // this is public 35 | void f1(int * a); 36 | int m1; 37 | public: 38 | s1(const int * a); 39 | int f2(int const * a); 40 | float m2; 41 | protected: 42 | int f3(int * const a); 43 | bool m3; 44 | private: 45 | void f4(int const * const a); 46 | double m4; 47 | }; 48 | 49 | void c1::f1(int a) {} 50 | c1::c1() {} 51 | int c1::f2() const { return 0; } 52 | void c1::v2() {} 53 | void c1::s1() {} 54 | const int * c1::f3(int a) { return 0; } 55 | void c1::f4() {} 56 | 57 | void s1::f1(int * a) {} 58 | s1::s1(const int * a) {} 59 | int s1::f2(int const * a) { return 0; } 60 | int s1::f3(int * const a) { return 0; } 61 | void s1::f4(int const * const a) {} 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyClASVi - Python Clang AST Viewer 2 | 3 | **Python Clang AST Viewer is a simple GUI program helping you to understand the Clang Abstract Syntax Tree** 4 | 5 | This tool is helpful if you want to use the Python binding for clang library, e.g. for a code generator. 6 | This binding use the c interface (not c++). So if you plan to use the c interface PyClASVi may also help you 7 | but be award the python binding may have some differences. 8 | 9 | The goal is to make this program runable on all systems supported by Clang and Python 10 | independent of Clang and Python version or support at least as many different versions as possible. 11 | Currently PyClASVi is tested under Ubuntu and Windows 10 with Python 2.7 and Python 3 12 | using different Clang versions starting at 3.3. 13 | Not all combinations are tested but I thing it works nearly everywhere with nearly every versions. 14 | 15 | ## Getting Started 16 | 17 | Depending on your operation system you need to install different things like: 18 | 19 | * Python 20 | * tk for Python 21 | * Clang or just the library 22 | * Compiler with standard header 23 | * Clang Python binding 24 | 25 | ### Linux 26 | 27 | ![PyClASVi under Linux](doc/img/PyClASVi_Linux.png) 28 | 29 | This was tested with Ubuntu 14.04 and 16.04. 30 | 31 | First you need to install Clang, Clang Indexing Library Bindings for Python and tk for Python. 32 | 33 | For Python 2 you can run this: 34 | 35 | sudo apt-get install libclang1-3.8 python-clang-3.8 python-tk 36 | 37 | Some older versions of Python clang binding do not support Python 3. 38 | For clang up to version 3.9 you can use libclang-py3 instead. 39 | For Python 3 you can run this: 40 | 41 | sudo apt-get install clang-3.8 python3-pip python3-tk 42 | sudo pip3 install libclang-py3==3.8 43 | 44 | Instead of `libclang1-3.8` you can install the complete compiler using `clang-3.8` 45 | and of course you can use everey supported version. 46 | 47 | Now you can run the file `pyclasvi.py` but you may get an error. 48 | Clang Python binding is looking for `libclang.so` or `libclang-.so` 49 | but Ubuntu install something like `libclang.so.1` or `libclang-3.8.so.1`. 50 | Use the `-l` option to set the used library. This is also fine if you have differed versions installed. 51 | 52 | To run PyClASVi just call `./pyclasvi.py -l /usr/lib/llvm-3.8/lib/libclang.so.1`. 53 | 54 | You will see a tabbed window. The first tab is the Input frame. 55 | Select a file to parse and add some arguments for the Clang parser. 56 | There must be only one argument per line. 57 | It looks like you need also the standard includes like `-I/usr/include` and `-I/usr/include/clang/3.8/include`. 58 | 59 | Press `[Parse]` to start the parser. 60 | 61 | If all works fine there is no warning or error on the Error tab and the Output tab shows the AST on the left. 62 | Select one entry (Clang calls it Cursor) to find more information on the right. 63 | 64 | ### Windows 65 | 66 | ![PyClASVi under Windows](doc/img/PyClASVi_Windows.png) 67 | 68 | First download and install Python (). Do not use the embeddable zip file, it does not include Tcl/tk. 69 | 70 | Also download and install Clang as part of the LLVM project (). 71 | If you check the option to set the PATH variable you don't need to do it later. 72 | LLVM do not install any standard libraries or headers so you need to install an extra compiler. 73 | As default VisualStudio is used. 74 | 75 | Install clang python binding using pip. 76 | If you want a special version, e.g. 5.0, you can enter the following command. 77 | 78 | py -m pip install clang==5.0 79 | 80 | To run PyClASVi enter the following commands: 81 | 82 | set PATH=%PATH%;C:\Program Files\LLVM\bin 83 | py pyclasvi.py 84 | 85 | You don't need the first line if you checked the corresponding option while installing LLVM. 86 | 87 | ## Documentation 88 | 89 | [How to use Python Clang AST Viewer](doc/PyClASVi_usage.md) 90 | 91 | [How to use Python Clang](doc/python_clang_usage.md) 92 | 93 | ## License 94 | 95 | PyClASVi is distributed under the [MIT License](LICENSE). 96 | -------------------------------------------------------------------------------- /doc/PyClASVi_usage.md: -------------------------------------------------------------------------------- 1 | # How to use Python Clang AST Viewer 2 | 3 | ## Command line 4 | 5 | You can call `pyclasvi.py -h` to get the command line help. 6 | 7 | usage: pyclasvi.py [-h] [-l LIBFILE] [file] 8 | 9 | Python Clang AST Viewer 10 | 11 | positional arguments: 12 | file Text file containing input data, 1st line = file to 13 | parse, next lines = Clang arguments, one argument per 14 | line 15 | 16 | optional arguments: 17 | -h, --help show this help message and exit 18 | -l LIBFILE, --libfile LIBFILE 19 | select Clang library file 20 | 21 | A typical call may be `./pyclasvi.py /usr/lib/llvm-3.8/lib/libclang.so.1 examples/test_all.txt`. 22 | 23 | ## Input 24 | 25 | After starting PyClASVi you will see the Input tab. 26 | 27 | ![Input tab](img/Input.png) 28 | 29 | In the first line you can select the file to parse. The lower textbox contains all options for the parser, 30 | one option per line. 31 | You can enter this options by yourself or use the toolbar. 32 | 33 | Finally press `[parse]` to start. 34 | 35 | * `Input file`: 36 | 37 | You can enter the file to parse by yourself or use the following button. 38 | 39 | * `[...]` 40 | 41 | Select file to parse. 42 | 43 | * `[+ Include]` 44 | 45 | Add an include option. A dialog allows you to select an include path. 46 | 47 | * `[+ Define]` 48 | 49 | Add a dummy define option. You must edit name and value by yourself. 50 | 51 | * `-x`-Combobox 52 | 53 | Add, remove or change the option to specify the language (C/C++). 54 | 55 | * `-std`-Combobox 56 | 57 | Add, remove or change the option for language standard/version. 58 | 59 | Normally you do not need a `-x` option if you have a `-std` option. 60 | 61 | * `[Load]` 62 | 63 | All inputs you have done here can be restored from a text file. 64 | 65 | * `[Save]` 66 | 67 | Save all inputs you have done in a text file. 68 | 69 | * `[Parse]` 70 | 71 | Start parsing. 72 | If there is nothing to parse you will stay in Input tab. 73 | If errors or warnings occurs while parsing you will go to Error tab. 74 | If parse work fine you will go to Output tab. 75 | 76 | ## Errors 77 | 78 | If there are some warnings or errors while parsing you will find all diagnostics here. 79 | 80 | ![Errors tab](img/Errors.png) 81 | 82 | Click on one diagnostic in the table to see corresponding location (yellow) in source code below. 83 | Only the first occurrence will be shown. 84 | 85 | * `Filter:` 86 | 87 | Select a severity level to filter shown diagnostics. 88 | 89 | ## Output 90 | 91 | After parsing (even with warnings or errors) you can walk through the Abstract Syntax Tree 92 | using the Output tab. 93 | 94 | ![Output tab](img/PyClASVi_Linux.png) 95 | 96 | On the left you see the AST. One node is called a cursor. Click at once to see its attributes 97 | on the upper right and its location (yellow) and range (grey) in source code on the lower right. 98 | 99 | By default all attributes are folded so you can see only the name, type and a `[+]` in front of it. 100 | Click `[+]` to unfold it an see the attribute value. Click `[-]` to fold it again. 101 | You will see an unfolded attribute stays unfolded even you select an other cursor. 102 | Some attributes have sub attributes so you can also fold and unfold it. 103 | 104 | 105 | If you are interesting in a special attribute click its name to hightlight it. 106 | If you select on other cursor the view is automatically scrolled to this attribute. 107 | 108 | * Main toolbar 109 | * `[<]` `[>]` 110 | 111 | The history buttons allows you to go to last viewed cursors and back. 112 | 113 | * `Doubles:` `[<]` `#/#` `[>]` 114 | 115 | Some cursors exists several times in AST. These buttons are only active for such cursor. 116 | You can go the others position in AST using the arrow buttons. The label between the buttons 117 | shows who many doubles there are and which one you currently have selected. 118 | 119 | * `[Search]` `[<]` `#/#` `[>]` 120 | 121 | Click the `[Search]` button to search for cursors. A new window allows you to enter a search pattern. 122 | You can search for spelling and the kind of cursor. 123 | 124 | If you want to search only for a kind leave `Spelling` blank and activate `Use RegEx`. 125 | 126 | * `[MS]` `[M1]` .. `[M5]` 127 | 128 | The marker buttons are helpful if you found an interesting cursor. First click `[MS]` (marker store) 129 | and than one of the other buttons e.g. `[M1]`. 130 | Now you can browse through the AST and if you want go back to marked cursor just click `[M1]` 131 | or where ever you have store it. 132 | 133 | * Source code toolbar 134 | * `[Cursor]` 135 | 136 | Click this to see position and whole range of the cursor in source code. 137 | 138 | This is the default after selecting a cursor. 139 | 140 | * `[Tokens]` `[<]` `#/#` `[>]` 141 | 142 | Click `[Tokens]` to show the position and range of a singe token belonging to current cursor. 143 | One cursor may belong to one or more tokens. The number is shown between the arrow buttons. 144 | Use this buttons to select on other token. -------------------------------------------------------------------------------- /doc/python_clang_usage.md: -------------------------------------------------------------------------------- 1 | # How to use Python Clang 2 | 3 | ## Basics 4 | 5 | A simple script to parse a file and output the root cursors kind and name (spelling). 6 | 7 | ```python 8 | import clang.cindex 9 | 10 | # use this if libclang could not be found 11 | #libfile = '/usr/lib/llvm-3.8/lib/libclang.so.1' 12 | #clang.cindex.Config.set_library_file(libfile) 13 | 14 | index = clang.cindex.Index.create() 15 | filename = 'test_all.cpp' 16 | args=('-std=c++11',) 17 | 18 | tu = index.parse(filename, args=args) # args may be omitted if not needed 19 | root = tu.cursor 20 | 21 | print(root.kind, root.spelling) 22 | ``` 23 | 24 | Nearly the same. 25 | 26 | ```python 27 | import clang.cindex 28 | 29 | # use this if libclang could not be found 30 | #libfile = '/usr/lib/llvm-3.8/lib/libclang.so.1' 31 | #clang.cindex.Config.set_library_file(libfile) 32 | 33 | filename = 'test_all.cpp' 34 | args=('-std=c++11',) 35 | 36 | tu = clang.cindex.TranslationUnit.from_source(filename, args=args) # args may be omitted if not needed 37 | 38 | root = tu.cursor 39 | 40 | print(root.kind, root.spelling) 41 | ``` 42 | 43 | You can also just parse a string. 44 | 45 | ```python 46 | import clang.cindex 47 | 48 | # use this if libclang could not be found 49 | #libfile = '/usr/lib/llvm-3.8/lib/libclang.so.1' 50 | #clang.cindex.Config.set_library_file(libfile) 51 | 52 | index = clang.cindex.Index.create() 53 | testcode = (('test.h', 'int a;'),) # iterable of tupels with file name and file context 54 | 55 | tu = index.parse('test.h', unsaved_files=testcode) 56 | 57 | root = tu.cursor 58 | 59 | print(root.kind, root.spelling) 60 | ``` 61 | 62 | The following code snippets can not run by itself. Start with one of the basics code snippets above. 63 | 64 | ## Browse AST 65 | 66 | We have a tree so we want to see the child nodes. 67 | 68 | ```python 69 | for child in root.get_children(): 70 | print(child.kind, child.spelling) 71 | ``` 72 | 73 | Remark clang will never return a list but something you can iterate. 74 | 75 | ```python 76 | children = root.get_children() 77 | #print(children[0].spelling) # does not work 78 | children = list(children) 79 | print(children[0].spelling) # works 80 | ``` 81 | 82 | Print the whole AST. 83 | 84 | ```python 85 | def print_ast(cursor, deep=0): 86 | print(' '.join((deep*' ', str(cursor.kind), str(cursor.spelling)))) 87 | for child in cursor.get_children(): 88 | print_ast(child, deep+1) 89 | 90 | print_ast(root) 91 | ``` 92 | 93 | If you just need to test all cursors you can use `walk_preorder()`. 94 | 95 | ```python 96 | for c in root.walk_preorder(): 97 | print(c.kind, c.spelling) 98 | ``` 99 | 100 | You might think every cursor must have a parent cursor without the root of course. 101 | But there are two properties with parent in name: `semantic_parent` and `lexical_parent`. 102 | 103 | Try the following code and you will see that you can't always use it to go back to parent in AST. 104 | 105 | ```python 106 | def check_parents(cursor, root=None): 107 | sem_root = None 108 | lex_root = None 109 | if cursor.semantic_parent is None: 110 | sem = 'no' 111 | else: 112 | sem = 'a' 113 | if root is not None: 114 | if cursor.semantic_parent == root: 115 | sem_root = 'also' 116 | else: 117 | sem_root = 'not' 118 | if cursor.lexical_parent is None: 119 | lex = 'no' 120 | else: 121 | lex = 'a' 122 | if root is not None: 123 | if cursor.lexical_parent == root: 124 | lex_root = 'also' 125 | else: 126 | lex_root = 'not' 127 | print('\nThe Cursor {} of type {} has {} semantic parent and {} lexical parent.'.format( 128 | cursor.spelling, cursor.kind, sem, lex)) 129 | if (cursor.semantic_parent is not None) and (cursor.lexical_parent is not None): 130 | if cursor.semantic_parent == cursor.lexical_parent: 131 | print('Both are the same.') 132 | else: 133 | print('They differ.') 134 | if sem_root: 135 | print('The semantic parent is {} the parent in AST.'.format(sem_root)) 136 | if lex_root: 137 | print('The lexical parent is {} the parent in AST.'.format(lex_root)) 138 | 139 | for child in cursor.get_children(): 140 | check_parents(child, cursor) 141 | 142 | check_parents(root) 143 | ``` 144 | 145 | There is a special feature of the AST with make it impossible to have a general parent property. 146 | Some cursors are several times present in the AST. 147 | 148 | ```python 149 | testcode = (('test.h', 150 | """ 151 | struct { 152 | int a; 153 | } s1; 154 | """),) 155 | 156 | tu = index.parse(testcode[0][0], unsaved_files=testcode) 157 | 158 | root = tu.cursor 159 | 160 | allcursors = list(root.walk_preorder()) 161 | 162 | for c1 in allcursors: 163 | count = 0 164 | for c2 in allcursors: 165 | if c1 == c2: 166 | count += 1 167 | if count > 1: 168 | print('Found cursor {} {} {} times in AST.'.format( 169 | c1.spelling, c1.kind, count)) 170 | ``` 171 | 172 | ## Hints 173 | 174 | Sometimes you want to compare two cursors. You can use the compare operator but you must be sure 175 | to compare only equal kind of object or an error will be thrown. 176 | As we see in last section some methods typical returning a cursor 177 | may also return nothing (None). 178 | 179 | 180 | ```python 181 | cmp = root 182 | 183 | for c in root.walk_preorder(): 184 | #if c.lexical_parent == cmp: # will throw an error 185 | if (c.lexical_parent is not None) and (c.lexical_parent == cmp): # works 186 | print('Cursor {} {} has {} {} as lexical_parent.'.format( 187 | c.spelling, c.kind, cmp.spelling, cmp.kind)) 188 | ``` 189 | 190 | You can not access every attribute for every cursor or its sub attributes. 191 | For example you can not access `enum_type` and `enum_value` from not enum cursors. 192 | In PyClASVi you can see the exceptions you will get. 193 | 194 | ![Exceptions in PyClASVi](img/Exception.png) 195 | 196 | Also do not try to access every attribute of a `Type` object witch has the kind `TypeKind.INVALID`. 197 | Especially `get_address_space()` may cause a crash not just an exception (tested with libclang 5.0). -------------------------------------------------------------------------------- /pyclasvi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Python Clang AST Viewer 5 | 6 | Clang AST Viewer shows the abstract syntax tree of a c/c++ file in a window. 7 | 8 | Enter 'pyclasvi.py -h' to show the usage 9 | 10 | PyClASVi is distributed under the MIT License, see LICENSE file. 11 | """ 12 | 13 | import sys 14 | 15 | if sys.version_info.major == 2: 16 | import ttk 17 | import Tkinter as tk 18 | import tkFont 19 | import tkFileDialog 20 | import tkMessageBox 21 | else: # python3 22 | import tkinter.ttk as ttk 23 | import tkinter as tk 24 | import tkinter.font as tkFont 25 | import tkinter.filedialog as tkFileDialog 26 | import tkinter.messagebox as tkMessageBox 27 | 28 | import clang.cindex 29 | import ctypes 30 | import argparse 31 | import inspect 32 | import re 33 | 34 | 35 | # Convert objects to a string. 36 | # Some object have no suitable standard string conversation, so use this. 37 | def toStr(data): 38 | if isinstance(data, bytes): # Python3 clang binding sometimes return bytes instead of strings 39 | return data.decode('ascii') # ASCII should be default in C/C++ but what about comments 40 | elif ((data.__class__ == int) # int but not bool, show decimal and hex 41 | or (sys.version_info.major) == 2 and isinstance(data, long)): 42 | if data < 0: # no negative hex values 43 | return str(data) 44 | else: 45 | return '{0} ({0:#010x})'.format(data) 46 | elif isinstance(data, clang.cindex.Cursor): # default output for cursors 47 | return '{0} ({1:#010x}) {2}'.format(data.kind.name, 48 | data.hash, 49 | data.displayname) 50 | elif isinstance(data, clang.cindex.SourceLocation): 51 | return 'file: {0}\nline: {1}\ncolumn: {2}\noffset: {3}'.format( 52 | data.file, data.line, data.column, data.offset) 53 | else: 54 | return str(data) 55 | 56 | 57 | # Just join strings. 58 | def join(*args): 59 | return ''.join(args) 60 | 61 | 62 | # Join everything to a string 63 | def xjoin(*args): 64 | return ''.join((str(a) for a in args)) 65 | 66 | 67 | # check if m is an instance methode 68 | def is_instance_methode(m): 69 | return inspect.ismethod(m) 70 | 71 | 72 | # has this instance methode only the self parameter? 73 | def is_simple_instance_methode(m): 74 | argSpec = inspect.getargspec(m) 75 | return len(argSpec.args) == 1 # only self 76 | 77 | 78 | # get methode definition like "(arg1, arg2)" as string 79 | def get_methode_prototype(m): 80 | argSpec = inspect.getargspec(m) 81 | return inspect.formatargspec(*argSpec) 82 | 83 | 84 | # check if obj is in list 85 | def is_obj_in_stack(obj, objStack): 86 | for o in objStack: 87 | if o.__class__ == obj.__class__: # some compare function trow exception if types are not equal 88 | if o == obj: 89 | return True 90 | return False 91 | 92 | 93 | # Cursor objects have a hash property but no __hash__ method 94 | # You can use this class to make Cursor object hashable 95 | class HashableObj: 96 | def __init__(self, obj): 97 | self.obj = obj 98 | 99 | def __eq__(self, other): 100 | return self.obj == other.obj 101 | 102 | def __hash__(self): 103 | return self.obj.hash 104 | 105 | 106 | # Make widget scrollable by adding scrollbars to the right and below it. 107 | # Of course parent is the parent widget of widget. 108 | # If there are more than one widget inside the parent use widgetRow and widgetColumn 109 | # to specify witch widget should be scrollable. 110 | def make_scrollable(parent, widget, widgetRow=0, widgetColumn=0): 111 | vsb = ttk.Scrollbar(parent, orient='vertical',command=widget.yview) 112 | widget.configure(yscrollcommand=vsb.set) 113 | vsb.grid(row=widgetRow, column=widgetColumn+1, sticky='ns') 114 | 115 | hsb = ttk.Scrollbar(parent, orient='horizontal',command=widget.xview) 116 | widget.configure(xscrollcommand=hsb.set) 117 | hsb.grid(row=widgetRow+1, column=widgetColumn, sticky='we') 118 | 119 | 120 | # Widget to handle all inputs (file name and parameters). 121 | # Contain [Parse] Button to start parsing and fill result in output frames 122 | class InputFrame(ttk.Frame): 123 | def __init__(self, master=None, parseCmd=None): 124 | ttk.Frame.__init__(self, master) 125 | self.grid(sticky='nswe') 126 | self.parseCmd = parseCmd 127 | self.filename = tk.StringVar(value='') 128 | self.xValue = tk.StringVar(value=InputFrame._X_OPTIONS[0]) # Option starting with "-x" 129 | self.stdValue = tk.StringVar(value=InputFrame._STD_OPTIONS[0]) # Option starting with "-std" 130 | self._create_widgets() 131 | 132 | _SOURCEFILETYPES = ( 133 | ('All source files', '.h', 'TEXT'), 134 | ('All source files', '.c', 'TEXT'), 135 | ('All source files', '.hh', 'TEXT'), 136 | ('All source files', '.hpp', 'TEXT'), 137 | ('All source files', '.hxx', 'TEXT'), 138 | ('All source files', '.h++', 'TEXT'), 139 | ('All source files', '.C', 'TEXT'), 140 | ('All source files', '.cc', 'TEXT'), 141 | ('All source files', '.cpp', 'TEXT'), 142 | ('All source files', '.cxx', 'TEXT'), 143 | ('All source files', '.c++', 'TEXT'), 144 | ('All files', '*'), 145 | ) 146 | 147 | _FILETYPES = ( 148 | ('Text files', '.txt', 'TEXT'), 149 | ('All files', '*'), 150 | ) 151 | _X_OPTIONS = ( 152 | 'no -x', 153 | '-xc', 154 | '-xc++' 155 | ) 156 | _STD_OPTIONS = ( 157 | 'no -std', 158 | '-std=c89', 159 | '-std=c90', 160 | '-std=iso9899:1990', 161 | '-std=iso9899:199409', 162 | '-std=gnu89', 163 | '-std=gnu90', 164 | '-std=c99', 165 | '-std=iso9899:1999', 166 | '-std=gnu99', 167 | '-std=c11', 168 | '-std=iso9899:2011', 169 | '-std=gnu11', 170 | '-std=c17', 171 | '-std=iso9899:2017', 172 | '-std=gnu17', 173 | '-std=c++98', 174 | '-std=c++03', 175 | '-std=gnu++98', 176 | '-std=gnu++03', 177 | '-std=c++11', 178 | '-std=gnu++11', 179 | '-std=c++14', 180 | '-std=gnu++14', 181 | '-std=c++17', 182 | '-std=gnu++17', 183 | '-std=c++2a', 184 | '-std=gnu++2a' 185 | ) 186 | 187 | def _create_widgets(self): 188 | self.rowconfigure(4, weight=1) 189 | self.columnconfigure(0, weight=1) 190 | 191 | ttk.Label(self, text='Input file:').grid(row=0, sticky='w') 192 | fileFrame = ttk.Frame(self) 193 | fileFrame.columnconfigure(0, weight=1) 194 | fileFrame.grid(row=1, column=0, columnspan=2, sticky='we') 195 | filenameEntry = ttk.Entry(fileFrame, textvariable=self.filename) 196 | filenameEntry.grid(row=0, column=0, sticky='we') 197 | button = ttk.Button(fileFrame, text='...', command=self._on_select_file) 198 | button.grid(row=0, column=1) 199 | 200 | ttk.Label(self, text='Arguments:').grid(row=2, sticky='w') 201 | buttonFrame = ttk.Frame(self) 202 | buttonFrame.grid(row=3, column=0, columnspan=2, sticky='we') 203 | button = ttk.Button(buttonFrame, text='+ Include', command=self._on_include) 204 | button.grid() 205 | button = ttk.Button(buttonFrame, text='+ Define', command=self._on_define) 206 | button.grid(row=0, column=1) 207 | 208 | xCBox = ttk.Combobox(buttonFrame, textvariable=self.xValue, 209 | values=InputFrame._X_OPTIONS) 210 | xCBox.bind('<>', self._on_select_x) 211 | xCBox.grid(row=0, column=2) 212 | 213 | stdCBox = ttk.Combobox(buttonFrame, textvariable=self.stdValue, 214 | values=InputFrame._STD_OPTIONS) 215 | stdCBox.bind('<>', self._on_select_std) 216 | stdCBox.grid(row=0, column=3) 217 | 218 | self.argsText = tk.Text(self, wrap='none') 219 | self.argsText.grid(row=4, sticky='nswe') 220 | make_scrollable(self, self.argsText, widgetRow=4, widgetColumn=0) 221 | 222 | buttonFrame = ttk.Frame(self) 223 | buttonFrame.grid(row=6, column=0, columnspan=2, sticky='we') 224 | buttonFrame.columnconfigure(2, weight=1) 225 | 226 | button = ttk.Button(buttonFrame, text='Load', command=self._on_file_load) 227 | button.grid(row=0, column=0) 228 | 229 | button = ttk.Button(buttonFrame, text='Save', command=self._on_file_save) 230 | button.grid(row=0, column=1) 231 | 232 | button = ttk.Button(buttonFrame, text='Parse', command=self.parseCmd) 233 | button.grid(row=0, column=2, sticky='we') 234 | 235 | def load_filename(self, filename): 236 | data = [] 237 | with open(filename, 'r') as f: 238 | data = f.read() 239 | if data: 240 | lines = data.split('\n') 241 | if len(lines) > 0: 242 | self.set_filename(lines[0]) 243 | self.set_args(lines[1:]) 244 | 245 | def _on_file_load(self): 246 | fn = tkFileDialog.askopenfilename(filetypes=InputFrame._FILETYPES) 247 | if fn: 248 | self.load_filename(fn) 249 | 250 | def _on_file_save(self): 251 | with tkFileDialog.asksaveasfile(defaultextension='.txt', filetypes=InputFrame._FILETYPES) as f: 252 | f.write(join(self.get_filename(), '\n')) 253 | for arg in self.get_args(): 254 | f.write(join(arg, '\n')) 255 | 256 | def _on_select_file(self): 257 | fn = tkFileDialog.askopenfilename(filetypes=self._SOURCEFILETYPES) 258 | if fn: 259 | self.set_filename(fn) 260 | 261 | def _on_include(self): 262 | dir = tkFileDialog.askdirectory() 263 | if dir: 264 | self.add_arg(join('-I', dir)) 265 | 266 | def _on_define(self): 267 | self.add_arg('-D=') 268 | 269 | def _on_select_x(self, e): 270 | arg = self.xValue.get() 271 | if arg == InputFrame._X_OPTIONS[0]: 272 | arg = None 273 | self.set_arg('-x', arg) 274 | 275 | def _on_select_std(self, e): 276 | arg = self.stdValue.get() 277 | if arg == InputFrame._STD_OPTIONS[0]: 278 | arg = None 279 | self.set_arg('-std', arg) 280 | 281 | def set_parse_cmd(self, parseCmd): 282 | self.parseCmd = parseCmd 283 | 284 | def set_filename(self, fn): 285 | self.filename.set(fn) 286 | 287 | def get_filename(self): 288 | return self.filename.get() 289 | 290 | # Set a single arg starting with name. 291 | # Replace or erase the first arg if there is still one starting with name. 292 | # total is full argument string starting with name for replacement or None or empty string for erase. 293 | def set_arg(self, name, total): 294 | args = self.get_args() 295 | i = 0 296 | for arg in args: 297 | if arg[:len(name)] == name: 298 | break; 299 | i += 1 300 | 301 | newArgs = args[:i] 302 | if total: 303 | newArgs.append(total) 304 | if i < len(args): 305 | newArgs.extend(args[i+1:]) 306 | 307 | self.set_args(newArgs) 308 | 309 | # Set/replace all args 310 | def set_args(self, args): 311 | self.argsText.delete('1.0', 'end') 312 | for arg in args: 313 | self.add_arg(arg) 314 | 315 | def add_arg(self, arg): 316 | txt = self.argsText.get('1.0', 'end') 317 | if len(txt) > 1: # looks like there is always a trailing newline 318 | prefix = '\n' 319 | else: 320 | prefix = '' 321 | self.argsText.insert('end', join(prefix, arg)) 322 | 323 | def get_args(self): 324 | args = [] 325 | 326 | argStr = self.argsText.get('1.0', 'end') 327 | argStrList = argStr.split('\n') 328 | for arg in argStrList: 329 | if len(arg) > 0: 330 | args.append(arg) 331 | 332 | return args 333 | 334 | 335 | # Widget to show all parse warnings and errors. 336 | # The upper part shows the list, the lower part the Source position of selected diagnostics. 337 | class ErrorFrame(ttk.Frame): 338 | def __init__(self, master=None): 339 | ttk.Frame.__init__(self, master) 340 | self.grid(sticky='nswe') 341 | self.filterValue=tk.StringVar(value=ErrorFrame._DIAG_STR_TAB[0]) # filter by severity 342 | self._create_widgets() 343 | self.errors = [] # list of diagnostics (also warnings not only errors) 344 | 345 | # _DIAG_LEVEL_TAB and _DIAG_STR_TAB must have the same size and order 346 | _DIAG_LEVEL_TAB = ( 347 | clang.cindex.Diagnostic.Ignored, 348 | clang.cindex.Diagnostic.Note, 349 | clang.cindex.Diagnostic.Warning, 350 | clang.cindex.Diagnostic.Error, 351 | clang.cindex.Diagnostic.Fatal 352 | ) 353 | _DIAG_STR_TAB = ( 354 | xjoin(clang.cindex.Diagnostic.Ignored, ' Ignored'), 355 | xjoin(clang.cindex.Diagnostic.Note, ' Note'), 356 | xjoin(clang.cindex.Diagnostic.Warning, ' Warning'), 357 | xjoin(clang.cindex.Diagnostic.Error, ' Error'), 358 | xjoin(clang.cindex.Diagnostic.Fatal, ' Fatal') 359 | ) 360 | _DIAG_TAG_TAB = {clang.cindex.Diagnostic.Warning:('warning',), 361 | clang.cindex.Diagnostic.Error:('error',), 362 | clang.cindex.Diagnostic.Fatal:('fatal',)} 363 | 364 | def _create_widgets(self): 365 | self.rowconfigure(0, weight=1) 366 | self.columnconfigure(0, weight=1) 367 | 368 | charSize = tkFont.nametofont('TkHeadingFont').measure('#') 369 | 370 | pw = tk.PanedWindow(self, orient='vertical') 371 | pw.grid(row=0, column=0, sticky='nswe') 372 | 373 | frame = ttk.Frame(pw) 374 | frame.rowconfigure(1, weight=1) 375 | frame.columnconfigure(0, weight=1) 376 | 377 | buttonFrame = ttk.Frame(frame) 378 | buttonFrame.grid(row=0, column=0, columnspan=2, sticky='we') 379 | 380 | label = tk.Label(buttonFrame, text='Filter:') 381 | label.grid(row=0, column=0) 382 | filterCBox = ttk.Combobox(buttonFrame, textvariable=self.filterValue, 383 | values=ErrorFrame._DIAG_STR_TAB) 384 | filterCBox.bind('<>', self._filter) 385 | filterCBox.grid(row=0, column=1) 386 | 387 | self.errorTable = ttk.Treeview(frame, columns=('category', 'severity', 'spelling', 'location', 388 | 'option')) 389 | 390 | self.errorTable.tag_configure('warning', background='light yellow') 391 | self.errorTable.tag_configure('error', background='indian red') 392 | self.errorTable.tag_configure('fatal', background='dark red', foreground='white') 393 | 394 | self.errorTable.bind('<>', self._on_selection) 395 | self.errorTable.grid(row=1, column=0, sticky='nswe') 396 | make_scrollable(frame, self.errorTable, 1) 397 | pw.add(frame, stretch='always') 398 | 399 | self.errorTable.heading('#0', text='#') 400 | self.errorTable.column('#0', width=4*charSize, anchor='e', stretch=False) 401 | self.errorTable.heading('category', text='Category') 402 | self.errorTable.column('category', width=20*charSize, stretch=False) 403 | self.errorTable.heading('severity', text='Severity') 404 | self.errorTable.column('severity', width=8*charSize, stretch=False) 405 | self.errorTable.heading('spelling', text='Text') 406 | self.errorTable.column('spelling', width=50*charSize, stretch=False) 407 | self.errorTable.heading('location', text='Location') 408 | self.errorTable.column('location', width=50*charSize, stretch=False) 409 | self.errorTable.heading('option', text='Option') 410 | self.errorTable.column('option', width=20*charSize, stretch=False) 411 | 412 | self.fileOutputFrame = FileOutputFrame(pw) 413 | pw.add(self.fileOutputFrame, stretch='always') 414 | 415 | # Show selected diagnostic in source file 416 | def _on_selection(self, event): 417 | curItem = self.errorTable.focus() 418 | err = self.errors[int(curItem)] 419 | range1 = None 420 | for r in err.ranges: 421 | range1 = r 422 | break 423 | self.fileOutputFrame.set_location(range1, err.location) 424 | 425 | # Filter by selected severity 426 | def _filter(self, e=None): 427 | for i in self.errorTable.get_children(): 428 | self.errorTable.delete(i) 429 | i = ErrorFrame._DIAG_STR_TAB.index(self.filterValue.get()) 430 | diagLevel = ErrorFrame._DIAG_LEVEL_TAB[i] 431 | cnt = 0 432 | for err in self.errors: 433 | cnt = cnt + 1 434 | if err.severity < diagLevel: 435 | continue 436 | if err.severity in ErrorFrame._DIAG_LEVEL_TAB: 437 | i = ErrorFrame._DIAG_LEVEL_TAB.index(err.severity) 438 | serverity = ErrorFrame._DIAG_STR_TAB[i] 439 | else: 440 | serverity = str(err.severity) 441 | if err.severity in ErrorFrame._DIAG_TAG_TAB: 442 | tagsVal=ErrorFrame._DIAG_TAG_TAB[err.severity] 443 | else: 444 | tagsVal=() 445 | if err.location.file: 446 | location = '{} {}:{}'.format(err.location.file.name, 447 | err.location.line, 448 | err.location.offset) 449 | else: 450 | location = None 451 | self.errorTable.insert('', 'end', text=str(cnt), values=[ 452 | join(str(err.category_number), ' ', toStr(err.category_name)), 453 | serverity, 454 | err.spelling, 455 | location, 456 | err.option 457 | ], 458 | tags=tagsVal, 459 | iid=str(cnt-1)) 460 | 461 | def clear(self): 462 | self.errors = [] 463 | self.fileOutputFrame.clear() 464 | for i in self.errorTable.get_children(): 465 | self.errorTable.delete(i) 466 | 467 | def set_errors(self, errors): 468 | self.clear() 469 | for err in errors: 470 | self.errors.append(err) 471 | self._filter() 472 | 473 | return len(self.errors) 474 | 475 | 476 | # Widget to show the AST in a Treeview like folders in a file browser 477 | # This widget is the master for current selected Cursor object. 478 | # If you want to show an other Cursor call set_current_cursor(...) 479 | class ASTOutputFrame(ttk.Frame): 480 | def __init__(self, master=None, selectCmd=None): 481 | ttk.Frame.__init__(self, master) 482 | self.grid(sticky='nswe') 483 | self._create_widgets() 484 | self.translationunit = None 485 | self.mapIIDtoCursor = {} # Treeview use IIDs (stings) to identify a note, 486 | self.mapCursorToIID = {} # so we need to map between IID and Cursor in both direction. 487 | # One Cursor may have a list of IIDs if several times found in AST. 488 | self.selectCmd = selectCmd # Callback after selecting a Cursor 489 | 490 | def _create_widgets(self): 491 | self.rowconfigure(0, weight=1) 492 | self.columnconfigure(0, weight=1) 493 | 494 | charSize = tkFont.nametofont('TkFixedFont').measure('#') 495 | 496 | self.astView = ttk.Treeview(self, selectmode='browse') 497 | self.astView.tag_configure('default', font='TkFixedFont') 498 | self.astView.bind('<>', self._on_selection) 499 | 500 | make_scrollable(self, self.astView) 501 | 502 | self.astView.heading('#0', text='Cursor') 503 | self.astView.grid(row=0, column=0, sticky='nswe') 504 | 505 | def _on_selection(self, event): 506 | if self.selectCmd is not None: 507 | self.selectCmd() 508 | 509 | def set_select_cmd(self, cmd): 510 | self.selectCmd = cmd 511 | 512 | def get_current_iid(self): 513 | return self.astView.focus() 514 | 515 | # Return a single IID or a list of IIDs. 516 | def get_current_iids(self): 517 | cursor = self.get_current_cursor() 518 | if cursor is not None: 519 | return self.mapCursorToIID[HashableObj(cursor)] 520 | else: 521 | return None 522 | 523 | def get_current_cursor(self): 524 | curCursor = None 525 | curItem = self.astView.focus() 526 | if curItem: 527 | curCursor = self.mapIIDtoCursor[curItem] 528 | return curCursor 529 | 530 | def set_current_iid(self, iid): 531 | self.astView.focus(iid) 532 | self.astView.selection_set(iid) 533 | self.astView.see(iid) 534 | 535 | def set_current_cursor(self, cursor): 536 | iid = self.mapCursorToIID[HashableObj(cursor)] 537 | if isinstance(iid, list): # partly multimap 538 | iid = iid[0] 539 | self.set_current_iid(iid) 540 | 541 | def clear(self): 542 | for i in self.astView.get_children(): 543 | self.astView.delete(i) 544 | self.translationunit = None 545 | self.mapIIDtoCursor = {} 546 | self.mapCursorToIID = {} 547 | 548 | def _insert_children(self, cursor, iid, deep=1): 549 | cntChildren = 0 550 | for childCursor in cursor.get_children(): 551 | cntChildren = cntChildren + 1 552 | newIID = self.astView.insert(iid, 553 | 'end', 554 | text=toStr(childCursor), 555 | tags=['default']) 556 | self.mapIIDtoCursor[newIID] = childCursor 557 | hCursor = HashableObj(childCursor) 558 | if hCursor in self.mapCursorToIID: # already in map, make a partly multimap 559 | self.cntDouble = self.cntDouble + 1 560 | data = self.mapCursorToIID[hCursor] 561 | if isinstance(data, str): 562 | data = [data] 563 | self.mapCursorToIID[hCursor] = data 564 | data.append(newIID) 565 | if len(data) > self.cntMaxDoubles: 566 | self.cntMaxDoubles = len(data) 567 | else: 568 | self.mapCursorToIID[hCursor] = newIID 569 | self._insert_children(childCursor, newIID, deep+1) 570 | self.cntCursors = self.cntCursors + 1 571 | 572 | if cntChildren > 0: 573 | if cntChildren > self.cntMaxChildren: 574 | self.cntMaxChildren = cntChildren 575 | if deep > self.cntMaxDeep: 576 | self.cntMaxDeep = deep 577 | 578 | def set_translationunit(self, tu): 579 | self.cntCursors = 1 580 | self.cntDouble = 0 581 | self.cntMaxDoubles = 0 582 | self.cntMaxChildren = 0 583 | self.cntMaxDeep = 0 584 | self.clear() 585 | self.translationunit = tu 586 | root = tu.cursor 587 | iid = self.astView.insert('', 588 | 'end', 589 | text=toStr(root), 590 | tags=['default']) 591 | self.mapIIDtoCursor[iid] = root 592 | self.mapCursorToIID[HashableObj(root)] = iid 593 | self._insert_children(root, iid) 594 | 595 | # some statistics 596 | print('AST has {0} cursors including {1} doubles.'.format(self.cntCursors, self.cntDouble)) 597 | print('max doubles: {0}, max children {1}, max deep {2}'.format( 598 | self.cntMaxDoubles, self.cntMaxChildren, self.cntMaxDeep)) 599 | 600 | # Search for IIDs matching to Cursors matching to kwargs. 601 | def search(self, **kwargs): 602 | result = [] 603 | useCursorKind = kwargs['use_CursorKind'] 604 | cursorKind = kwargs['CursorKind'] 605 | spelling = kwargs['spelling'] 606 | caseInsensitive = kwargs['caseInsensitive'] 607 | useRegEx = kwargs['use_RexEx'] 608 | if useRegEx: 609 | reFlags = 0 610 | if caseInsensitive: 611 | reFlags = re.IGNORECASE 612 | try: 613 | reObj = re.compile(spelling, reFlags) 614 | except Exception as e: 615 | tkMessageBox.showerror('Search RegEx', str(e)) 616 | return result 617 | elif caseInsensitive: 618 | spelling = spelling.lower() 619 | 620 | for iid in self.mapIIDtoCursor: 621 | cursor = self.mapIIDtoCursor[iid] 622 | found = True 623 | if useCursorKind: 624 | found = cursorKind == cursor.kind.name 625 | if found: 626 | if useRegEx: 627 | if not reObj.match(toStr(cursor.spelling)): 628 | found = False 629 | 630 | elif caseInsensitive: 631 | found = spelling == toStr(cursor.spelling).lower() 632 | else: 633 | found = spelling == toStr(cursor.spelling) 634 | if found: 635 | result.append(iid) 636 | 637 | result.sort() 638 | 639 | return result 640 | 641 | 642 | # Helper class to represent un-/folded sections in Text widget of CursorOutputFrame. 643 | # One node is of type FoldSection. 644 | # No Node will be removed even if a new selected cursor object have less section. 645 | # Therefore if you open the next cursor with the same section hierarchy it this tree 646 | # can remember witch section should be shown and witch not. 647 | # 648 | # Remark, this only works if always the same kind of object with the same attribute order is shown. 649 | # An example: 650 | # Obj A:Cursor B:Cursor C:WahtElse 651 | # +- brief_comment = None +- brief_comment = "a function" +- brief_comment = None 652 | # +- get_arguments:iterator => empty +- get_arguments:iterator | 653 | # | | +- [0]:Cursor | 654 | # | | +- [1]:Cursor | 655 | # +- spelling:sting = "a" +- spelling:sting = "func" +- spelling:sting = "x" 656 | # 657 | # Object A and B are fine, B create more active nodes in FoldSectionTree 658 | # but the attribute order is identical. Object C have some same called attributes but in wrong order. 659 | # If first B is shown with get_arguments (2nd attribute) open but spelling (3rd attribute) closed 660 | # and than C was shown spelling (here 2nd attribute) will be open. 661 | # If A is show some of the nodes representing the two sub Cursor in object B will be inactive. 662 | class FoldSectionTree: 663 | def __init__(self): 664 | self.root = FoldSection(True) # just a root node witch is always shown 665 | self.marker = None # a singe section header (attribute name) can be highlighted 666 | 667 | def get_root(self): 668 | return self.root 669 | 670 | def set_marker(self, marker): 671 | self.marker = marker 672 | 673 | def get_marker(self): 674 | return self.marker 675 | 676 | def set_all_show(self, show): 677 | FoldSection.show_default = show 678 | self.root.set_all_show(show) 679 | 680 | # Deactivate all section but do not erase it, they still know if they should be shown or not. 681 | def clear_lines(self): 682 | self.root.clear_lines() 683 | 684 | # Find section starting at startLine in Text widget. 685 | def find_section(self, startLine): 686 | return self._find_section(startLine, self.root.members) 687 | 688 | def _find_section(self, startLine, sectionList): 689 | if sectionList: 690 | lastSec = None 691 | for sec in sectionList: 692 | if sec.startLine == 0: 693 | break 694 | elif sec.startLine == startLine: 695 | return sec 696 | elif sec.startLine < startLine: 697 | lastSec = sec 698 | else: 699 | break 700 | if lastSec is not None: 701 | return self._find_section(startLine, lastSec.members) 702 | else: 703 | return None 704 | else: 705 | return None 706 | 707 | 708 | # Node in FoldSectionTree 709 | class FoldSection: 710 | def __init__(self, show, deep=-1): 711 | self.startLine = 0 # if 0 this section is not active 712 | self.show = show # fold or not 713 | self.members = None # children 714 | self.parent = None 715 | self.childNr = -1 # child index from parent view 716 | self.deep = deep # current deep in tree, root is -1, first real sections 0 717 | 718 | show_default = False # default section are closed 719 | 720 | # Map this section to starting text line in Text widget. 721 | # This also activate this section (startLine > 0). 722 | def set_line(self, startLine): 723 | self.startLine = startLine 724 | 725 | def set_show(self, show): 726 | self.show = show 727 | 728 | # Open this an all children sections. 729 | def set_all_show(self, show): 730 | self.show = show 731 | if self.members: 732 | for m in self.members: 733 | m.set_all_show(show) 734 | 735 | # Get the n-th children. 736 | # Remark, children (nodes) represent Cursor attributes in same order. 737 | # num may convert to attribute name to support different kind of objects in CursorOutputFrame. 738 | def get_child(self, num): 739 | if self.members is None: 740 | self.members = [] 741 | 742 | while (num+1) > len(self.members): 743 | newFS = FoldSection(FoldSection.show_default, self.deep+1) 744 | newFS.parent = self 745 | newFS.childNr = num 746 | self.members.append(newFS) 747 | 748 | return self.members[num] 749 | 750 | # Deactivate this section and all children. 751 | def clear_lines(self): 752 | self.startLine = 0 753 | if self.members: 754 | for m in self.members: 755 | m.clear_lines() 756 | 757 | 758 | # Widget to show nearly all attributes of a Cursor object 759 | # Private member are ignored and other Cursors are just shown as link. 760 | # If the attribute its self have attributes they are also up to a defined deep, so you have still a tree. 761 | # So you have still a tree. This is not implemented by a Treewiew widget but just Text widget. 762 | # The text widget gives you more layout possibilities but you have to implement the folding logic 763 | # by your self. Therefore the classes FoldSectionTree and FoldSection are used which also 764 | # implements some features missed in the Treeview widget. 765 | class CursorOutputFrame(ttk.Frame): 766 | def __init__(self, master=None, selectCmd=None): 767 | ttk.Frame.__init__(self, master) 768 | self.grid(sticky='nswe') 769 | self._create_widgets() 770 | self.cursor = None 771 | self.selectCmd = selectCmd # will be called on clicking a cursor link 772 | self.cursorList = [] # list of cursor in same order as links are shown in text 773 | self.foldTree = FoldSectionTree() # contains infos about foldable section (a single member) 774 | 775 | _MAX_DEEP = 8 # max deep of foldable sections / attributes 776 | _MAX_ITER_OUT = 25 # is a member is an iterator show just the first x elements 777 | _DATA_INDENT = ' ' # indentation for sub sections / attributes 778 | 779 | # ignore member with this types 780 | _IGNORE_TYPES = ('function',) 781 | 782 | def _create_widgets(self): 783 | self.rowconfigure(0, weight=1) 784 | self.columnconfigure(0, weight=1) 785 | 786 | defFont = tkFont.Font(font='TkFixedFont') 787 | defFontProp = defFont.actual() 788 | self.cursorText = tk.Text(self, wrap='none') 789 | self.cursorText.grid(row=0, sticky='nswe') 790 | self.cursorText.bind('', self._on_right_click) 791 | 792 | make_scrollable(self, self.cursorText) 793 | 794 | self.cursorText.tag_configure('attr_name', font=(defFontProp['family'], defFontProp['size'], 'bold')) 795 | self.cursorText.tag_bind('attr_name', '', self._on_attr_click) 796 | self.cursorText.tag_configure('attr_name_marked', background='lightblue') 797 | self.cursorText.tag_configure('attr_type', foreground='green') 798 | self.cursorText.tag_configure('attr_err', foreground='red') 799 | self.cursorText.tag_configure('link', foreground='blue') 800 | self.cursorText.tag_bind('link', '', self._on_cursor_click) 801 | self.cursorText.tag_bind('link', '', self._on_link_enter) 802 | self.cursorText.tag_bind('link', '', self._on_link_leave) 803 | self.cursorText.tag_configure('special', font=(defFontProp['family'], defFontProp['size'], 'italic')) 804 | 805 | for n in range(CursorOutputFrame._MAX_DEEP): 806 | self.cursorText.tag_configure(xjoin('section_header_', n), foreground='gray') 807 | self.cursorText.tag_bind(xjoin('section_header_', n), '', self._on_section_click) 808 | self.cursorText.tag_bind(xjoin('section_header_', n), '', self._on_section_enter) 809 | self.cursorText.tag_bind(xjoin('section_header_', n), '', self._on_section_leave) 810 | self.cursorText.tag_configure(xjoin('section_hidden_', n), elide=True) 811 | self.cursorText.tag_configure(xjoin('section_', n)) 812 | 813 | self.cursorText.config(state='disabled') 814 | 815 | # Change mouse cursor over links. 816 | def _on_link_enter(self, event): 817 | self.cursorText.configure(cursor='hand1') 818 | 819 | # Reset mouse cursor after leaving links. 820 | def _on_link_leave(self, event): 821 | self.cursorText.configure(cursor='xterm') 822 | 823 | # Cursor link was clicked, run callback. 824 | def _on_cursor_click(self, event): 825 | if self.selectCmd is None: 826 | return 827 | 828 | curIdx = self.cursorText.index('@{0},{1}'.format(event.x, event.y)) 829 | linkIdxs = list(self.cursorText.tag_ranges('link')) 830 | listIdx = 0 831 | 832 | for start, end in zip(linkIdxs[0::2], linkIdxs[1::2]): 833 | if (self.cursorText.compare(curIdx, '>=', start) and 834 | self.cursorText.compare(curIdx, '<', end)): 835 | cursor = self.cursorList[listIdx] 836 | self.selectCmd(cursor) 837 | break 838 | listIdx += 1 839 | 840 | # Mark clicked attribute name and store it in foldTree. 841 | def _on_attr_click(self, event): 842 | self.cursorText.tag_remove('attr_name_marked', '1.0', 'end') 843 | curIdx = self.cursorText.index('@{0},{1}'.format(event.x, event.y)) 844 | curLine = curIdx.split('.')[0] 845 | 846 | curSec = self.foldTree.find_section(int(curLine)) 847 | if curSec is not None: 848 | self.foldTree.set_marker(curSec) 849 | 850 | attr = self.cursorText.tag_nextrange('attr_name', join(curLine, '.0')) 851 | self.cursorText.tag_add('attr_name_marked', attr[0], attr[1]) 852 | 853 | # Scroll text widget so marked attribute is shown. 854 | def goto_marker(self): 855 | curMarker = self.cursorText.tag_nextrange('attr_name_marked', '1.0') 856 | if curMarker: 857 | self.cursorText.see('end') # first jump to the end, so also the lines ... 858 | self.cursorText.see(curMarker[0]) # ...after attribute name are shown 859 | 860 | # Show context menu. 861 | def _on_right_click(self, event): 862 | menu = tk.Menu(None, tearoff=0) 863 | menu.add_command(label='Expand all', command=self.expand_all) 864 | menu.add_command(label='Collapse all', command=self.collapse_all) 865 | menu.tk_popup(event.x_root, event.y_root) 866 | 867 | # Change mouse cursor over clickable [+]/[-] of a node of a foldable section. 868 | def _on_section_enter(self, event): 869 | self.cursorText.configure(cursor='arrow') 870 | 871 | # Reset mouse cursor. 872 | def _on_section_leave(self, event): 873 | self.cursorText.configure(cursor='xterm') 874 | 875 | # There was a click on [+]/[-], so we need to fold or unfold a section. 876 | def _on_section_click(self, event): 877 | curIdx = self.cursorText.index('@{0},{1}'.format(event.x, event.y)) 878 | curLine = int(curIdx.split('.')[0]) 879 | curSec = self.foldTree.find_section(curLine) # find clicked section in foldTree 880 | if curSec is None: 881 | return # should never happen 882 | 883 | # find the matching section tag 884 | curLev = curSec.deep 885 | next_section = self.cursorText.tag_nextrange(xjoin('section_', curLev), curIdx) 886 | 887 | if next_section: # should always be true 888 | self.cursorText.config(state='normal') 889 | cur_header = self.cursorText.tag_prevrange(xjoin('section_header_', curLev), next_section[0]) 890 | next_hidden = self.cursorText.tag_nextrange(xjoin('section_hidden_', curLev), curIdx) 891 | self.cursorText.delete(join(cur_header[0], ' +1c'), join(cur_header[0], ' +2c')) 892 | newShow = next_hidden and (next_hidden == next_section) 893 | if newShow: 894 | self.cursorText.tag_remove(xjoin('section_hidden_', curLev), next_section[0], next_section[1]) 895 | self.cursorText.insert(join(cur_header[0], ' +1c'), '-') 896 | else: 897 | self.cursorText.tag_add(xjoin('section_hidden_', curLev), next_section[0], next_section[1]) 898 | self.cursorText.insert(join(cur_header[0], ' +1c'), '+') 899 | curSec.set_show(newShow) 900 | self.cursorText.config(state='disabled') 901 | 902 | # Expand all section (via context menu). 903 | def expand_all(self): 904 | self.foldTree.set_all_show(True) 905 | self.cursorText.config(state='normal') 906 | for n in range(CursorOutputFrame._MAX_DEEP): 907 | secs = self.cursorText.tag_ranges(xjoin('section_', n)) 908 | for start, end in zip(secs[0::2], secs[1::2]): 909 | cur_header = self.cursorText.tag_prevrange(xjoin('section_header_', n), start) 910 | self.cursorText.delete(join(cur_header[0], ' +1c'), join(cur_header[0], ' +2c')) 911 | self.cursorText.tag_remove(xjoin('section_hidden_', n), start, end) 912 | self.cursorText.insert(join(cur_header[0], ' +1c'), '-') 913 | self.cursorText.config(state='disabled') 914 | 915 | # Collapse all sections (via context menu). 916 | def collapse_all(self): 917 | self.foldTree.set_all_show(False) 918 | self.cursorText.config(state='normal') 919 | for n in range(CursorOutputFrame._MAX_DEEP): 920 | secs = self.cursorText.tag_ranges(xjoin('section_', n)) 921 | for start, end in zip(secs[0::2], secs[1::2]): 922 | cur_header = self.cursorText.tag_prevrange(xjoin('section_header_', n), start) 923 | self.cursorText.delete(join(cur_header[0], ' +1c'), join(cur_header[0], ' +2c')) 924 | self.cursorText.tag_add(xjoin('section_hidden_', n), start, end) 925 | self.cursorText.insert(join(cur_header[0], ' +1c'), '+') 926 | self.cursorText.config(state='disabled') 927 | 928 | def clear(self): 929 | self.cursorText.config(state='normal') 930 | self.cursorText.delete('1.0', 'end') 931 | self.cursorText.config(state='disabled') 932 | self.cursor = None 933 | self.cursorList = [] 934 | 935 | # Output cursor with link in text widget. 936 | def _add_cursor(self, cursor): 937 | # we got an exception if we compare a Cursor object with an other none Cursor object like None 938 | # Therfore Cursor == None will not work so we use a try 939 | if isinstance(cursor, clang.cindex.Cursor): 940 | self.cursorText.insert('end', 941 | toStr(cursor), 942 | 'link') 943 | self.cursorList.append(cursor) 944 | else: 945 | self.cursorText.insert('end', str(cursor)) 946 | 947 | # Add a single attribute or a value of an iterable to the output. 948 | # This output contains a header and the value that can be fold/unfold. 949 | # if index is >= 0 a value of an iterable is outputted else an attribute. 950 | # Some attributes are skipped, so this function may output nothing. 951 | # The attributes name is attrName and it belongs to the last object in objStack. 952 | # foldNode contains FoldSection matching to current object the attribute belongs to. 953 | # For values of an iterable objStack also contains this value and foldeNode 954 | # belongs to the value. attrName may same useful name e.g. "[0]". 955 | def _add_attr(self, objStack, attrName, foldNode, index=-1): 956 | obj = objStack[-1] 957 | deep = len(objStack) - 1 958 | prefix = '\t' * deep 959 | isIterData = index >= 0 960 | 961 | # set default values 962 | attrData = None # attribute value for output 963 | attrDataTag = None # special tag for special output format (None, attr_err, special) 964 | attrTypeTag = 'attr_type' # tag for attribute name (attr_type, attr_err) 965 | 966 | if not isIterData: 967 | try: 968 | attrData = getattr(obj, attrName) 969 | attrType = attrData.__class__.__name__ 970 | if attrType in CursorOutputFrame._IGNORE_TYPES: 971 | return False # no new section created 972 | except BaseException as e: 973 | attrType = join(e.__class__.__name__, ' => do not use this') 974 | attrTypeTag = 'attr_err' 975 | else: 976 | attrData = obj 977 | attrType = attrData.__class__.__name__ 978 | 979 | if (isinstance(obj, clang.cindex.Type) 980 | and (obj.kind == clang.cindex.TypeKind.INVALID) 981 | and (attrName in ('get_address_space', 'get_typedef_name'))): 982 | attrData = 'Do not uses this if kind is TypeKind.INVALID!' 983 | attrDataTag = 'attr_err' 984 | elif is_instance_methode(attrData): 985 | attrType = join(attrType, ' ', get_methode_prototype(attrData)) 986 | if is_simple_instance_methode(attrData): 987 | try: 988 | attrData = attrData() 989 | attrType = join(attrType, ' => ', attrData.__class__.__name__) 990 | except BaseException as e: 991 | attrData = join(e.__class__.__name__, ' => do not use this') 992 | attrDataTag = 'attr_err' 993 | if attrName == 'get_children': 994 | cnt = 0 995 | for c in attrData: 996 | cnt = cnt+1 997 | attrData = xjoin(cnt, ' children, see tree on the left') 998 | attrDataTag = 'special' 999 | 1000 | # start output, first line is always shown if parent section is also shown 1001 | curIdx = self.cursorText.index('end -1c') 1002 | curLine = int(curIdx.split('.')[0]) 1003 | foldNode.set_line(curLine) 1004 | self.cursorText.insert('end', prefix) 1005 | self.cursorText.insert('end', '[-] ', xjoin('section_header_', deep)) 1006 | 1007 | if not isIterData: 1008 | if self.foldTree.get_marker() == foldNode: 1009 | attTags = ('attr_name', 'attr_name_marked') 1010 | else: 1011 | attTags = 'attr_name' 1012 | else: 1013 | attTags = () 1014 | self.cursorText.insert('end', attrName, attTags) 1015 | self.cursorText.insert('end', ' (') 1016 | self.cursorText.insert('end', attrType, attrTypeTag) 1017 | self.cursorText.insert('end', '):\n') 1018 | # first line done 1019 | 1020 | startIdx = self.cursorText.index('end -1c') 1021 | 1022 | # special behauviour for special attributes like functions or iterables 1023 | if attrName in ('get_template_argument_kind', 1024 | 'get_template_argument_type', 1025 | 'get_template_argument_value', 1026 | 'get_template_argument_unsigned_value'): 1027 | nums = obj.get_num_template_arguments() 1028 | if nums > 0: 1029 | for n in range(nums): 1030 | result = attrData(n) 1031 | subFoldNode = foldNode.get_child(n) 1032 | objStack.append(result) 1033 | self._add_attr(objStack, xjoin('num=', n), subFoldNode, n) 1034 | objStack.pop() 1035 | else: 1036 | self.cursorText.insert('end', '\n') 1037 | elif hasattr(attrData, '__iter__') and not isinstance(attrData, (str, bytes)): 1038 | self.cursorText.insert('end', join(prefix, CursorOutputFrame._DATA_INDENT, '[\n')) 1039 | cnt = 0 1040 | for d in attrData: 1041 | if cnt < CursorOutputFrame._MAX_ITER_OUT: 1042 | subFoldNode = foldNode.get_child(cnt) 1043 | objStack.append(d) 1044 | self._add_attr(objStack, str(cnt), subFoldNode, cnt) 1045 | objStack.pop() 1046 | else: 1047 | self.cursorText.insert('end', 1048 | join(prefix, 1049 | ' ', 1050 | CursorOutputFrame._DATA_INDENT, 1051 | 'and some more...\n'), 1052 | 'special') 1053 | break 1054 | cnt = cnt+1 1055 | self.cursorText.insert('end', join(prefix, CursorOutputFrame._DATA_INDENT, ']\n')) 1056 | else: 1057 | self._add_attr_data(objStack, foldNode, attrData, attrDataTag, isIterData) 1058 | 1059 | #self.cursorText.insert('end', '\n') # use this if you want an extra line witch can be hidden 1060 | endIdx = self.cursorText.index('end -1c') 1061 | #self.cursorText.insert('end', '\n') # use this if you want an extra line witch can't be hidden 1062 | 1063 | # add tags for the section needed to hide a section or find the position for later hidding 1064 | self.cursorText.tag_add(xjoin('section_', deep), startIdx, endIdx) 1065 | if not foldNode.show: 1066 | cur_header = self.cursorText.tag_prevrange(xjoin('section_header_', deep), 'end') 1067 | self.cursorText.delete(join(cur_header[0], ' +1c'), join(cur_header[0], ' +2c')) 1068 | self.cursorText.insert(join(cur_header[0], ' +1c'), '+') 1069 | self.cursorText.tag_add(xjoin('section_hidden_', deep), startIdx, endIdx) 1070 | 1071 | return True # new section created 1072 | 1073 | # Add a single attribute or a value of an iterable to the output. 1074 | # This output contains only the value. 1075 | # If isIterData is true a value of an iterable is outputted else an attribute. 1076 | # objStack and foldNode have the same meaning like at function _add_attr. 1077 | # attrData is the value and attrDataTag a tag for output format. 1078 | def _add_attr_data(self, objStack, foldNode, attrData, attrDataTag, isIterData): 1079 | deep = len(objStack) - 1 1080 | prefix = '\t' * deep 1081 | 1082 | if isinstance(attrData, clang.cindex.Cursor): 1083 | self.cursorText.insert('end', join(prefix, CursorOutputFrame._DATA_INDENT)) 1084 | self._add_cursor(attrData) 1085 | self.cursorText.insert('end', '\n') 1086 | elif (isinstance(attrData, clang.cindex.Type) 1087 | or isinstance(attrData, clang.cindex.SourceRange) 1088 | or isinstance(attrData, clang.cindex.Token)): 1089 | if isIterData: 1090 | cmpStack = objStack[:-1] 1091 | else: 1092 | cmpStack = objStack 1093 | if not is_obj_in_stack(attrData, cmpStack): #attrData not in objStack: 1094 | if (deep+1) < CursorOutputFrame._MAX_DEEP: 1095 | objStack.append(attrData) 1096 | self._add_obj(objStack, foldNode) 1097 | objStack.pop() 1098 | else: 1099 | self.cursorText.insert('end', join(prefix, CursorOutputFrame._DATA_INDENT)) 1100 | self.cursorText.insert('end', 1101 | join('To deep to show ', toStr(attrData)), 1102 | 'special') 1103 | self.cursorText.insert('end', '\n') 1104 | else: 1105 | self.cursorText.insert('end', join(prefix, CursorOutputFrame._DATA_INDENT)) 1106 | self.cursorText.insert('end', 1107 | join(toStr(attrData), ' already shown!'), 1108 | 'special') 1109 | self.cursorText.insert('end', '\n') 1110 | else: 1111 | lines = toStr(attrData).split('\n') 1112 | for line in lines: 1113 | self.cursorText.insert('end', join(prefix, CursorOutputFrame._DATA_INDENT)) 1114 | self.cursorText.insert('end', line, attrDataTag) 1115 | self.cursorText.insert('end', '\n') 1116 | 1117 | return 1118 | 1119 | # Add nearly all attributes of last object in objStack to the output. 1120 | # objStack contains the current clang object an all its parents starting from root. 1121 | # foldNode contains FoldSection matching to current clang object. 1122 | def _add_obj(self, objStack, foldNode): 1123 | if objStack and (len(objStack) > 0): 1124 | obj = objStack[-1] 1125 | attrs = dir(obj) 1126 | attIdx = 0 1127 | for attrName in attrs: 1128 | # ignore all starts with '_' 1129 | if attrName[0] == '_': 1130 | continue 1131 | subFoldNode = foldNode.get_child(attIdx) 1132 | res = self._add_attr(objStack, attrName, subFoldNode) 1133 | if res: 1134 | attIdx += 1 1135 | 1136 | # Set cursor for output. 1137 | def set_cursor(self, c): 1138 | self.foldTree.clear_lines() 1139 | 1140 | if not isinstance(c, clang.cindex.Cursor): 1141 | self.clear() 1142 | return 1143 | if isinstance(self.cursor, clang.cindex.Cursor): 1144 | if self.cursor == c: 1145 | return 1146 | self.cursorList = [] 1147 | self.cursor = c 1148 | self.cursorText.config(state='normal') 1149 | self.cursorText.delete('1.0', 'end') 1150 | self._add_obj([c], self.foldTree.get_root()) 1151 | 1152 | self.cursorText.config(state='disabled') 1153 | self.goto_marker() 1154 | 1155 | 1156 | # Widget to show a position (Range and Location) in a source file. 1157 | class FileOutputFrame(ttk.Frame): 1158 | def __init__(self, master=None): 1159 | ttk.Frame.__init__(self, master) 1160 | self.grid(sticky='nswe') 1161 | self._create_widgets() 1162 | self.fileName = None 1163 | 1164 | def _create_widgets(self): 1165 | self.rowconfigure(0, weight=1) 1166 | self.columnconfigure(0, weight=1) 1167 | 1168 | self.fileText = tk.Text(self, wrap='none') 1169 | self.fileText.grid(row=0, sticky='nswe') 1170 | self.fileText.tag_configure('range', background='gray') 1171 | self.fileText.tag_configure('location', background='yellow') 1172 | 1173 | make_scrollable(self, self.fileText) 1174 | 1175 | self.fileText.config(state='disabled') 1176 | 1177 | def clear(self): 1178 | self.fileText.config(state='normal') 1179 | self.fileText.delete('1.0', 'end') 1180 | self.fileText.config(state='disabled') 1181 | self.fileName = None 1182 | 1183 | def set_location(self, srcRange, srcLocation): 1184 | self.fileText.config(state='normal') 1185 | self.fileText.tag_remove('range', '1.0', 'end') 1186 | self.fileText.tag_remove('location', '1.0', 'end') 1187 | 1188 | newFileName = None 1189 | if isinstance(srcRange, clang.cindex.SourceRange) and srcRange.start.file: 1190 | newFileName = srcRange.start.file.name 1191 | elif (isinstance(srcLocation, clang.cindex.SourceLocation) and 1192 | srcLocation.file): 1193 | newFileName = srcLocation.file.name 1194 | else: 1195 | self.fileText.delete('1.0', 'end') 1196 | 1197 | if newFileName and (self.fileName != newFileName): 1198 | self.fileText.delete('1.0', 'end') 1199 | data = [] 1200 | with open(newFileName, 'r') as f: 1201 | data = f.read() 1202 | self.fileText.insert('end', data) 1203 | 1204 | self.fileName = newFileName 1205 | 1206 | if isinstance(srcRange, clang.cindex.SourceRange): 1207 | srcFrom = '{0}.{1}'.format(srcRange.start.line, srcRange.start.column-1) 1208 | srcTo = '{0}.{1}'.format(srcRange.end.line, srcRange.end.column-1) 1209 | self.fileText.tag_add('range', srcFrom, srcTo) 1210 | self.fileText.see(srcTo) # first scroll to the end 1211 | self.fileText.see(srcFrom) # then to the start, so usually all is shown 1212 | 1213 | if isinstance(srcLocation, clang.cindex.SourceLocation): 1214 | if srcLocation.file: 1215 | locFrom = '{0}.{1}'.format(srcLocation.line, srcLocation.column-1) 1216 | locTo = '{0}.{1}'.format(srcLocation.line, srcLocation.column) 1217 | self.fileText.tag_add('location', locFrom, locTo) 1218 | self.fileText.see(locFrom) 1219 | 1220 | self.fileText.config(state='disabled') 1221 | 1222 | 1223 | # Widget to show the position of cursor or its token in source file 1224 | # This consists on a small toolbar to select kind of output (cursor or token) 1225 | # and the FileOutputFrame 1226 | class CursorFileOutputFrame(ttk.Frame): 1227 | def __init__(self, master=None): 1228 | ttk.Frame.__init__(self, master) 1229 | self.grid(sticky='nswe') 1230 | self.outState = tk.IntVar(value=0) 1231 | self._create_widgets() 1232 | self.cursor = None 1233 | self.tokens = [] 1234 | self.tokenIdx = 0 1235 | 1236 | def _create_widgets(self): 1237 | self.rowconfigure(1, weight=1) 1238 | self.columnconfigure(0, weight=1) 1239 | 1240 | toolbar = ttk.Frame(self) 1241 | toolbar.grid(row=0, column=0, sticky='wne') 1242 | toolbar.columnconfigure(5, weight=1) 1243 | 1244 | self.cursorBtn = ttk.Radiobutton(toolbar, text='Cursor', style='Toolbutton', 1245 | variable=self.outState, value=0, command=self.change_out) 1246 | self.cursorBtn.grid(row=0, column=0) 1247 | 1248 | self.tokensBtn = ttk.Radiobutton(toolbar, text='Tokens', style='Toolbutton', 1249 | variable=self.outState, value=1, command=self.change_out) 1250 | self.tokensBtn.grid(row=0, column=1) 1251 | 1252 | self.tokensPrevBtn = ttk.Button(toolbar, text='<', width=-3, style='Toolbutton', 1253 | command=self.show_prev_token) 1254 | self.tokensPrevBtn.grid(row=0, column=2) 1255 | self.tokensLabel = ttk.Label(toolbar, text='-/-', width=-7, anchor='center') 1256 | self.tokensLabel.grid(row=0, column=3) 1257 | self.tokensNextBtn = ttk.Button(toolbar, text='>', width=-3, style='Toolbutton', 1258 | command=self.show_next_token) 1259 | self.tokensNextBtn.grid(row=0, column=4) 1260 | 1261 | self.tokenKind = ttk.Label(toolbar, text='') 1262 | self.tokenKind.grid(row=0, column=5, sticky='we') 1263 | 1264 | self.fileOutputFrame = FileOutputFrame(self) 1265 | 1266 | def clear(self): 1267 | self.fileOutputFrame.clear() 1268 | self.outState.set(0) 1269 | self.cursor = None 1270 | self.tokens = [] 1271 | self.tokenIdx = 0 1272 | self.tokensLabel.config(text='-/-') 1273 | self.tokenKind.config(text='') 1274 | self.cursorBtn.config(state='disabled') 1275 | self.tokensBtn.config(state='disabled') 1276 | self.tokensPrevBtn.config(state='disabled') 1277 | self.tokensLabel.config(state='disabled') 1278 | self.tokensNextBtn.config(state='disabled') 1279 | 1280 | def set_cursor(self, cursor): 1281 | self.clear() 1282 | if isinstance(cursor, clang.cindex.Cursor): 1283 | self.cursor = cursor 1284 | for token in cursor.get_tokens(): 1285 | self.tokens.append(token) 1286 | self.tokenIdx = 0 1287 | self.show_cursor() 1288 | self.cursorBtn.config(state='normal') 1289 | if len(self.tokens) > 0: 1290 | self._show_label() 1291 | self.tokensBtn.config(state='normal') 1292 | self.tokensPrevBtn.config(state='normal') 1293 | self.tokensLabel.config(state='normal') 1294 | self.tokensNextBtn.config(state='normal') 1295 | 1296 | # New kind of output (Cursor pos/Token pos) selected. 1297 | def change_out(self): 1298 | if self.outState.get() == 0: 1299 | self.show_cursor() 1300 | else: 1301 | self.show_token() 1302 | 1303 | def show_prev_token(self): 1304 | self.tokenIdx-=1 1305 | if self.tokenIdx < 0: 1306 | self.tokenIdx = len(self.tokens)-1 1307 | self.show_token() 1308 | 1309 | def show_next_token(self): 1310 | self.tokenIdx+=1 1311 | if self.tokenIdx >= len(self.tokens): 1312 | self.tokenIdx = 0 1313 | self.show_token() 1314 | 1315 | def show_cursor(self): 1316 | self.outState.set(0) 1317 | self.fileOutputFrame.set_location(self.cursor.extent, self.cursor.location) 1318 | 1319 | def _show_label(self): 1320 | self.tokensLabel.config(text='{0}/{1}'.format(self.tokenIdx+1, len(self.tokens))) 1321 | self.tokenKind.config(text=str(self.tokens[self.tokenIdx].kind)) 1322 | 1323 | def show_token(self): 1324 | self.outState.set(1) 1325 | self._show_label() 1326 | token = self.tokens[self.tokenIdx] 1327 | self.fileOutputFrame.set_location(token.extent, token.location) 1328 | 1329 | 1330 | # Separate modal dialog window for search. 1331 | class SearchDialog(tk.Toplevel): 1332 | 1333 | _old_data = None # remember last entered data 1334 | 1335 | def __init__(self, master=None): 1336 | tk.Toplevel.__init__(self, master) 1337 | self.transient(master) 1338 | 1339 | self.result = False # True if [OK] pressed 1340 | self.kindOptions = [] 1341 | for kind in clang.cindex.CursorKind.get_all_kinds(): 1342 | self.kindOptions.append(kind.name) 1343 | self.kindOptions.sort() 1344 | self.kindState = tk.IntVar(value=0) 1345 | self.kindValue = tk.StringVar(value=self.kindOptions[0]) 1346 | self.searchtext = tk.StringVar(value='') 1347 | self.caseInsensitive = tk.IntVar(value=0) 1348 | self.useRegEx = tk.IntVar(value=0) 1349 | 1350 | if SearchDialog._old_data is not None: 1351 | self.set_data(**SearchDialog._old_data) 1352 | 1353 | self.title('Search') 1354 | self._create_widgets() 1355 | self._on_check_kind() 1356 | 1357 | self.grab_set() 1358 | 1359 | self.bind('', self._on_ok) 1360 | self.bind('', self._on_cancel) 1361 | 1362 | self.protocol('WM_DELETE_WINDOW', self._on_cancel) 1363 | 1364 | self.wait_window(self) 1365 | 1366 | def _create_widgets(self): 1367 | self.columnconfigure(0, weight=1) 1368 | 1369 | frame = ttk.Frame(self) 1370 | frame.grid(row=0, column=0, sticky='nesw') 1371 | frame.columnconfigure(1, weight=1) 1372 | 1373 | cb=ttk.Checkbutton(frame, text='Kind:', variable=self.kindState, command=self._on_check_kind) 1374 | cb.grid(row=0, column=0) 1375 | self.kindCBox = ttk.Combobox(frame, textvariable=self.kindValue, values=self.kindOptions) 1376 | self.kindCBox.grid(row=0, column=1, sticky='we') 1377 | 1378 | label = tk.Label(frame, text='Spelling:') 1379 | label.grid(row=1, column=0) 1380 | searchEntry = ttk.Entry(frame, textvariable=self.searchtext, width=25) 1381 | searchEntry.grid(row=1, column=1, sticky='we') 1382 | 1383 | cb=ttk.Checkbutton(frame, text='Ignore case', variable=self.caseInsensitive) 1384 | cb.grid(row=2, column=1, sticky='w') 1385 | cb=ttk.Checkbutton(frame, text='Use RegEx', variable=self.useRegEx) 1386 | cb.grid(row=3, column=1, sticky='w') 1387 | 1388 | frame = ttk.Frame(self) 1389 | frame.grid(row=1, column=0, sticky='e') 1390 | 1391 | btn = tk.Button(frame, text='OK', width=8, command=self._on_ok) 1392 | btn.grid(row=0, column=0, sticky='e') 1393 | 1394 | btn = tk.Button(frame, text='Cancel', width=8, command=self._on_cancel) 1395 | btn.grid(row=0, column=1, sticky='e') 1396 | 1397 | def get_data(self): 1398 | data = {} 1399 | data['use_CursorKind'] = self.kindState.get() 1400 | data['CursorKind'] = self.kindValue.get() 1401 | data['spelling'] = self.searchtext.get() 1402 | data['caseInsensitive'] = self.caseInsensitive.get() 1403 | data['use_RexEx'] = self.useRegEx.get() 1404 | return data 1405 | 1406 | def set_data(self, **kwargs): 1407 | self.kindState.set(kwargs['use_CursorKind']) 1408 | self.kindValue.set(kwargs['CursorKind']) 1409 | self.searchtext.set(kwargs['spelling']) 1410 | self.caseInsensitive.set(kwargs['caseInsensitive']) 1411 | self.useRegEx.set(kwargs['use_RexEx']) 1412 | 1413 | def _on_check_kind(self): 1414 | if self.kindState.get(): 1415 | self.kindCBox.config(state='normal') 1416 | else: 1417 | self.kindCBox.config(state='disable') 1418 | 1419 | def _on_ok(self, event=None): 1420 | self.result = True 1421 | SearchDialog._old_data = self.get_data() 1422 | self.destroy() 1423 | 1424 | def _on_cancel(self, event=None): 1425 | self.destroy() 1426 | 1427 | 1428 | # Output frame shows the AST on the left (TreeView, ASTOutputFrame) and the selected Cursor on the right 1429 | # The right shows all member and the location of the cursor in source file. 1430 | # ASTOutputFrame on the left is the master for current selected cursor. 1431 | class OutputFrame(ttk.Frame): 1432 | def __init__(self, master=None): 1433 | ttk.Frame.__init__(self, master) 1434 | self.grid(sticky='nswe') 1435 | self.markerSetState = tk.IntVar(value=0) # after click [M#] Button 0: jump to marked cursor 1436 | # 1: mark current cursor 1437 | self._create_widgets() 1438 | 1439 | self.curIID = '' # IID of current marked cursor in TreeView on the left 1440 | self.curCursor = None 1441 | self.history = [] # history of last visit cursors 1442 | self.historyPos = -1 # current pos in this history, to walk in both directions 1443 | self.searchResult = [] 1444 | self.searchPos = -1 # you can also walk through searchResult 1445 | self.marker = [] # marked cursor using the [M#] Buttons 1446 | for n in range(0, OutputFrame._MARKER_BTN_CNT): # list must have fixed size to check if there is 1447 | self.marker.append(None) # a cursor at position x stored no not (None) 1448 | self.clear() 1449 | 1450 | _MAX_HISTORY = 25 1451 | _MARKER_BTN_CNT = 5 1452 | 1453 | def _create_widgets(self): 1454 | self.rowconfigure(1, weight=1) 1455 | self.columnconfigure(0, weight=1) 1456 | 1457 | # Toolbar start 1458 | toolbar = ttk.Frame(self) 1459 | toolbar.grid(row=0, column=0, sticky='we') 1460 | 1461 | self.historyBackwardBtn = ttk.Button(toolbar, text='<', width=-3, style='Toolbutton', 1462 | command=self.go_history_backward) 1463 | self.historyBackwardBtn.grid(row=0, column=0) 1464 | self.historyForwardBtn = ttk.Button(toolbar, text='>', width=-3, style='Toolbutton', 1465 | command=self.go_history_forward) 1466 | self.historyForwardBtn.grid(row=0, column=1) 1467 | 1468 | sep = ttk.Separator(toolbar, orient='vertical') 1469 | sep.grid(row=0, column=2, sticky='ns', padx=5, pady=5) 1470 | 1471 | label = tk.Label(toolbar, text='Doubles:') 1472 | label.grid(row=0, column=3) 1473 | 1474 | self.doublesBackwardBtn = ttk.Button(toolbar, text='<', width=-3, style='Toolbutton', 1475 | command=self.go_doubles_backward) 1476 | self.doublesBackwardBtn.grid(row=0, column=4) 1477 | self.doublesLabel = ttk.Label(toolbar, text='-/-', width=-3, anchor='center') 1478 | self.doublesLabel.grid(row=0, column=5) 1479 | self.doublesForwardBtn = ttk.Button(toolbar, text='>', width=-3, style='Toolbutton', 1480 | command=self.go_doubles_forward) 1481 | self.doublesForwardBtn.grid(row=0, column=6) 1482 | 1483 | sep = ttk.Separator(toolbar, orient='vertical') 1484 | sep.grid(row=0, column=7, sticky='ns', padx=5, pady=5) 1485 | 1486 | self.searchBtn = ttk.Button(toolbar, text='Search', style='Toolbutton', 1487 | command=self._on_search) 1488 | self.searchBtn.grid(row=0, column=8) 1489 | self.searchBackwardBtn = ttk.Button(toolbar, text='<', width=-3, style='Toolbutton', 1490 | command=self.go_search_backward) 1491 | self.searchBackwardBtn.grid(row=0, column=9) 1492 | self.serachLabel = ttk.Label(toolbar, text='-/-', width=-7, anchor='center') 1493 | self.serachLabel.grid(row=0, column=10) 1494 | self.searchForwardBtn = ttk.Button(toolbar, text='>', width=-3, style='Toolbutton', 1495 | command=self.go_search_forward) 1496 | self.searchForwardBtn.grid(row=0, column=11) 1497 | 1498 | sep = ttk.Separator(toolbar, orient='vertical') 1499 | sep.grid(row=0, column=12, sticky='ns', padx=5, pady=5) 1500 | 1501 | self.markerSetBtn = ttk.Checkbutton(toolbar, text='MS', width=-4, style='Toolbutton', 1502 | variable=self.markerSetState, onvalue=1, offvalue=0, 1503 | command=self._on_marker_set) 1504 | self.markerSetBtn.grid(row=0, column=13) 1505 | 1506 | self.markerBtns = [] 1507 | for n in range(0, OutputFrame._MARKER_BTN_CNT): 1508 | btn = ttk.Button(toolbar, text='M{}'.format(n+1), width=-4, style='Toolbutton', 1509 | command=lambda n=n : self._on_marker_x(n)) 1510 | btn.grid(row=0, column=14+n) 1511 | self.markerBtns.append(btn) 1512 | # Toolbar end 1513 | 1514 | # ttk version of PanedWindow do not support all options 1515 | pw1 = tk.PanedWindow(self, orient='horizontal') 1516 | pw1.grid(row=1, column=0, sticky='nswe') 1517 | 1518 | self.astOutputFrame = ASTOutputFrame(pw1, selectCmd=self._on_cursor_selection) 1519 | pw1.add(self.astOutputFrame, stretch='always') 1520 | 1521 | pw2 = tk.PanedWindow(pw1, orient='vertical') 1522 | 1523 | # remark ASTOutputFrame is the master for current selected cursor but you can click on a link 1524 | # to other cursors in CursorOutputFrame, this must be forwarded to ASTOutputFrame.set_current_cursor 1525 | self.cursorOutputFrame = CursorOutputFrame(pw2, 1526 | selectCmd=self.astOutputFrame.set_current_cursor) 1527 | pw2.add(self.cursorOutputFrame, stretch='always') 1528 | 1529 | self.fileOutputFrame = CursorFileOutputFrame(pw2) 1530 | pw2.add(self.fileOutputFrame, stretch='always') 1531 | 1532 | pw1.add(pw2, stretch='always') 1533 | 1534 | # There was a cursor selected at left ASTOutputFrame (TreeView on left). 1535 | def _on_cursor_selection(self): 1536 | curIID = self.astOutputFrame.get_current_iid() 1537 | curCursor = self.astOutputFrame.get_current_cursor() 1538 | if curIID != self.curIID: # do not update history if you currently walk through it 1539 | self._set_active_cursor(curCursor) 1540 | self._add_history(curIID) 1541 | self.curIID = curIID 1542 | self._update_doubles() 1543 | self._update_search() 1544 | 1545 | # Set internal active cursor without updating history. 1546 | # This is only called on cursor selection (via ASTOutputFrame) or history walk. 1547 | def _set_active_cursor(self, cursor): 1548 | self.curCursor = cursor 1549 | self.cursorOutputFrame.set_cursor(cursor) 1550 | self.fileOutputFrame.set_cursor(cursor) 1551 | self.markerSetBtn.config(state='normal') 1552 | 1553 | def clear_history(self): 1554 | self.history = [] 1555 | self.historyPos = -1 1556 | self._update_history_buttons() 1557 | 1558 | def _add_history(self, iid): 1559 | if self.historyPos < len(self.history): 1560 | # we travel backward in time and change the history 1561 | # so we change the time line and the future 1562 | # therefore erase the old future 1563 | self.history = self.history[:(self.historyPos+1)] 1564 | # now the future is an empty sheet of paper 1565 | 1566 | if len(self.history) >= OutputFrame._MAX_HISTORY: # history to long? 1567 | self.history = self.history[1:] 1568 | else: 1569 | self.historyPos = self.historyPos + 1 1570 | 1571 | self.history.append(iid) 1572 | self._update_history_buttons() 1573 | 1574 | def go_history_backward(self): 1575 | if self.historyPos > 0: 1576 | self.historyPos = self.historyPos - 1 1577 | self._update_history() 1578 | self._update_history_buttons() 1579 | 1580 | def go_history_forward(self): 1581 | if (self.historyPos+1) < len(self.history): 1582 | self.historyPos = self.historyPos + 1 1583 | self._update_history() 1584 | self._update_history_buttons() 1585 | 1586 | # Switch to right cursor after walk through history. 1587 | def _update_history(self): 1588 | newIID = self.history[self.historyPos] 1589 | self.curIID = newIID # set this before _on_cursor_selection() is called 1590 | self.astOutputFrame.set_current_iid(newIID) # this will cause call of _on_cursor_selection() 1591 | self._set_active_cursor(self.astOutputFrame.get_current_cursor()) 1592 | 1593 | def _update_history_buttons(self): 1594 | hLen = len(self.history) 1595 | hPos = self.historyPos 1596 | 1597 | if hPos > 0: # we can go backward 1598 | self.historyBackwardBtn.config(state='normal') 1599 | else: 1600 | self.historyBackwardBtn.config(state='disabled') 1601 | 1602 | if (hLen > 1) and ((hPos+1) < hLen): # we can go forward 1603 | self.historyForwardBtn.config(state='normal') 1604 | else: 1605 | self.historyForwardBtn.config(state='disabled') 1606 | 1607 | def _clear_doubles(self): 1608 | self.doublesForwardBtn.config(state='disabled') 1609 | self.doublesLabel.config(state='disabled') 1610 | self.doublesLabel.config(text='-/-') 1611 | self.doublesBackwardBtn.config(state='disabled') 1612 | 1613 | def go_doubles_backward(self): 1614 | iids = self.astOutputFrame.get_current_iids() 1615 | if isinstance(iids, list): 1616 | newIdx = (iids.index(self.curIID) - 1) % len(iids) 1617 | newIID = iids[newIdx] 1618 | self.astOutputFrame.set_current_iid(newIID) 1619 | 1620 | def go_doubles_forward(self): 1621 | iids = self.astOutputFrame.get_current_iids() 1622 | if isinstance(iids, list): 1623 | newIdx = (iids.index(self.curIID) + 1) % len(iids) 1624 | newIID = iids[newIdx] 1625 | self.astOutputFrame.set_current_iid(newIID) 1626 | 1627 | # Update buttons states and label value for doubles (some cursors can be found several times in AST). 1628 | def _update_doubles(self): 1629 | iids = self.astOutputFrame.get_current_iids() 1630 | if isinstance(iids, list): 1631 | self.doublesForwardBtn.config(state='normal') 1632 | self.doublesLabel.config(state='normal') 1633 | self.doublesLabel.config(text='{0}/{1}'.format(iids.index(self.curIID)+1, len(iids))) 1634 | self.doublesBackwardBtn.config(state='normal') 1635 | else: 1636 | self._clear_doubles() 1637 | 1638 | def clear_search(self): 1639 | self.searchResult = [] 1640 | self._update_search() 1641 | 1642 | def _on_search(self): 1643 | search = SearchDialog(self.winfo_toplevel()) 1644 | if search.result: 1645 | data = search.get_data() 1646 | self.searchResult = self.astOutputFrame.search(**data) 1647 | self.searchPos = 0 1648 | self._update_search() 1649 | if len(self.searchResult) > 0: 1650 | self.astOutputFrame.set_current_iid(self.searchResult[self.searchPos]) 1651 | 1652 | def go_search_backward(self): 1653 | self.searchPos = (self.searchPos - 1) % len(self.searchResult) 1654 | self.astOutputFrame.set_current_iid(self.searchResult[self.searchPos]) 1655 | 1656 | def go_search_forward(self): 1657 | self.searchPos = (self.searchPos + 1) % len(self.searchResult) 1658 | self.astOutputFrame.set_current_iid(self.searchResult[self.searchPos]) 1659 | 1660 | # Update buttons states and label value for search. 1661 | def _update_search(self): 1662 | cnt = len(self.searchResult) 1663 | if cnt > 0: 1664 | serchIID = self.searchResult[self.searchPos] 1665 | if self.curIID != serchIID: 1666 | if self.curIID in self.searchResult: 1667 | self.searchPos = self.searchResult.index(self.curIID) 1668 | serchIID = self.curIID 1669 | if self.curIID != serchIID: 1670 | self.serachLabel.config(state='disabled') 1671 | else: 1672 | self.serachLabel.config(state='normal') 1673 | self.searchForwardBtn.config(state='normal') 1674 | self.serachLabel.config(text='{0}/{1}'.format(self.searchPos+1, cnt)) 1675 | self.searchBackwardBtn.config(state='normal') 1676 | else: 1677 | self.searchForwardBtn.config(state='disabled') 1678 | self.serachLabel.config(state='disabled') 1679 | self.serachLabel.config(text='-/-') 1680 | self.searchBackwardBtn.config(state='disabled') 1681 | 1682 | # Update button states for marker. 1683 | def _update_marker(self): 1684 | for n in range(0, OutputFrame._MARKER_BTN_CNT): 1685 | if self.marker[n] is not None: 1686 | self.markerBtns[n].config(state='normal') 1687 | else: 1688 | self.markerBtns[n].config(state='disabled') 1689 | 1690 | # [MS] clicked 1691 | def _on_marker_set(self): 1692 | if self.markerSetState.get(): 1693 | for btn in self.markerBtns: 1694 | btn.config(state='normal') 1695 | else: 1696 | self._update_marker() 1697 | 1698 | # [M#] clicked 1699 | def _on_marker_x(self, num): 1700 | if self.markerSetState.get(): 1701 | self.markerSetState.set(0) 1702 | self.marker[num]=self.curCursor 1703 | self._update_marker() 1704 | else: 1705 | self.astOutputFrame.set_current_cursor(self.marker[num]) 1706 | 1707 | # Reset all outputs. 1708 | def clear(self): 1709 | self.curIID = '' 1710 | self.clear_history() 1711 | self._clear_doubles() 1712 | self.clear_search() 1713 | for n in range(0, OutputFrame._MARKER_BTN_CNT): 1714 | self.marker[n] = None 1715 | self.searchBtn.config(state='disabled') 1716 | self.markerSetBtn.config(state='disabled') 1717 | self._update_marker() 1718 | self.astOutputFrame.clear() 1719 | self.cursorOutputFrame.clear() 1720 | self.fileOutputFrame.clear() 1721 | 1722 | def set_translationunit(self, tu): 1723 | self.clear() 1724 | self.astOutputFrame.set_translationunit(tu) 1725 | self.searchBtn.config(state='normal') 1726 | 1727 | 1728 | # Main window combine all frames in tabs an contains glue logic between these frames 1729 | class Application(ttk.Frame): 1730 | def __init__(self, master=None, file=None): 1731 | ttk.Frame.__init__(self, master) 1732 | self._set_style() 1733 | self.grid(sticky='nswe') 1734 | self._create_widgets() 1735 | 1736 | self.index = clang.cindex.Index.create() 1737 | 1738 | if file: 1739 | self.inputFrame.load_filename(file) 1740 | else: 1741 | self.inputFrame.set_filename('select file to parse =>') 1742 | self.inputFrame.set_args(['-xc++', 1743 | '-std=c++14', 1744 | '-I/your/include/path', 1745 | '-I/more/include/path']) 1746 | 1747 | 1748 | def _create_widgets(self): 1749 | top=self.winfo_toplevel() 1750 | top.rowconfigure(0, weight=1) 1751 | top.columnconfigure(0, weight=1) 1752 | 1753 | self.rowconfigure(0, weight=1) 1754 | self.columnconfigure(0, weight=1) 1755 | 1756 | self.notebook = ttk.Notebook(self) 1757 | 1758 | self.inputFrame = InputFrame(self.notebook, parseCmd=self._on_parse) 1759 | 1760 | self.errorFrame = ErrorFrame(self.notebook) 1761 | self.outputFrame = OutputFrame(self.notebook) 1762 | 1763 | self.notebook.add(self.inputFrame, text='Input') 1764 | self.notebook.add(self.errorFrame, text='Errors') 1765 | self.notebook.add(self.outputFrame, text='Output') 1766 | self.notebook.grid(row=0, column=0, sticky='nswe') 1767 | 1768 | quitButton = ttk.Button(self, text='Quit', 1769 | command=self.quit) 1770 | quitButton.grid(row=1, column=0, sticky='we') 1771 | 1772 | def _set_style(self): 1773 | s = ttk.Style() 1774 | # center text in toolbuttons 1775 | s.configure('Toolbutton', anchor='center', padding=s.lookup('TButton', 'padding')) 1776 | 1777 | # [parse] button is clicked 1778 | def _on_parse(self): 1779 | self.errorFrame.clear() 1780 | self.outputFrame.clear() 1781 | fileName = self.inputFrame.get_filename() 1782 | args = self.inputFrame.get_args() 1783 | tu = self.index.parse(fileName, args=args) 1784 | 1785 | cntErr = self.errorFrame.set_errors(tu.diagnostics) 1786 | self.outputFrame.set_translationunit(tu) 1787 | 1788 | if cntErr > 0: 1789 | self.notebook.select(self.errorFrame) 1790 | else: 1791 | self.notebook.select(self.outputFrame) 1792 | 1793 | 1794 | def main(): 1795 | parser = argparse.ArgumentParser(description='Python Clang AST Viewer') 1796 | parser.add_argument('-l', '--libfile', help='select Clang library file', nargs=1, dest='libFile') 1797 | parser.add_argument('file', help='''Text file containing input data, 1798 | 1st line = file to parse, 1799 | next lines = Clang arguments, one argument per line''', 1800 | nargs='?') 1801 | args = parser.parse_args() 1802 | 1803 | if args.libFile: 1804 | clang.cindex.Config.set_library_file(args.libFile[0]) 1805 | 1806 | app = Application(file=args.file) 1807 | app.master.title('PyClASVi') 1808 | app.mainloop() 1809 | 1810 | if __name__ == '__main__': 1811 | main() 1812 | --------------------------------------------------------------------------------