├── .gitignore ├── README.markdown ├── sourcecodebrowser.plugin └── sourcecodebrowser ├── __init__.py ├── ctags.py ├── data ├── configure_dialog.ui ├── icons │ └── 16x16 │ │ ├── missing-image.png │ │ ├── source-class.png │ │ ├── source-code-browser.png │ │ ├── source-define.png │ │ ├── source-enumerator.png │ │ ├── source-field.png │ │ ├── source-function.png │ │ ├── source-macro.png │ │ ├── source-member.png │ │ ├── source-method.png │ │ ├── source-namespace.png │ │ ├── source-property.png │ │ ├── source-struct.png │ │ ├── source-table.png │ │ ├── source-typedef.png │ │ └── source-variable.png └── org.gnome.gedit.plugins.sourcecodebrowser.gschema.xml └── plugin.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Gedit Source Code Browser 2 | ========================= 3 | 4 | A source code class and function browser plugin for Gedit 3. 5 | 6 | * Author: Micah Carrick 7 | 8 | This plugin will add a new tab to the side pane in the Gedit text editor which 9 | shows symbols (functions, classes, variables, etc.) for the active document. 10 | Clicking a symbol in the list wil jump to the line on which that symbol is 11 | defined. 12 | 13 | See the [ctags supported languages](http://ctags.sourceforge.net/languages.html) 14 | for a list of the 41 programming languages supported by this plugin. 15 | 16 | 17 | Requirements 18 | ------------ 19 | 20 | This plugins is for Gedit 3 and is **not compatible with Gedit 2.x**. 21 | 22 | The Gedit Source Code Browser plugin uses 23 | [Exuberant Ctags](http://ctags.sourceforge.net/) to parse symbols 24 | out of source code. Exuberant Ctags is avaialable in the software repository for 25 | many distributions. To make sure you have ctags correctly installed, issue 26 | the following command: 27 | 28 | ctags --version 29 | 30 | Make sure that you see *Exuberant* Ctags in the version output. 31 | 32 | 33 | Installation 34 | ------------ 35 | 36 | 1. Download this repository by clicking the Downloads button at the top of the 37 | github page or issue the following command in a terminal: 38 | 39 | ```git clone git://github.com/toobaz/gedit-source-code-browser.git --depth=1``` 40 | 41 | 2. Copy the file `sourcecodebrowser.plugin` and the folder `sourcecodebrowser` to 42 | `~/.local/share/gedit/plugins/`. 43 | 44 | 3. Restart Gedit. 45 | 46 | 4. Activate the plugin in Gedit by choosing 'Edit > Preferences', the selecting 47 | the 'Plugins' tab, and checking the box next to 'Soucre Code Browser'. 48 | 49 | 5. (Optional) If you want to enable the configuration dialog you need to compile 50 | the settings schema. You must do this as root. 51 | 52 | ``` 53 | cd /home//.local/share/gedit/plugins/sourcecodebrowser/data/ 54 | 55 | cp org.gnome.gedit.plugins.sourcecodebrowser.gschema.xml /usr/share/glib-2.0/schemas/ 56 | 57 | glib-compile-schemas /usr/share/glib-2.0/schemas/ 58 | ``` 59 | 60 | Screenshots 61 | ----------- 62 | 63 | ![Python code in Source Code Browser](http://www.micahcarrick.com/images/gedit-source-code-browser/python.png) 64 | 65 | 66 | Known Issues 67 | ------------ 68 | 69 | * CSS is not supported. This issue is about ctags and not this plugin. You can 70 | [extend ctags](http://ctags.sourceforge.net/EXTENDING.html) to add support for 71 | any language you like. Many people have provided their fixes to on internet 72 | such as this [patch for CSS support](http://scie.nti.st/2006/12/22/how-to-add-css-support-to-ctags). 73 | 74 | * PHP is supported, however, PHP5 classes are not well supported. This is again 75 | an issue with ctags. There are numerous fixes to be found onn the internet 76 | such as these 77 | [patches for better PHP5 support](http://www.jejik.com/articles/2008/11/patching_exuberant-ctags_for_better_php5_support_in_vim/). 78 | 79 | 80 | License 81 | ------- 82 | 83 | Copyright (c) 2011, Micah Carrick 84 | All rights reserved. 85 | 86 | Redistribution and use in source and binary forms, with or without modification, 87 | are permitted provided that the following conditions are met: 88 | 89 | * Redistributions of source code must retain the above copyright notice, this 90 | list of conditions and the following disclaimer. 91 | 92 | * Redistributions in binary form must reproduce the above copyright notice, 93 | this list of conditions and the following disclaimer in the documentation 94 | and/or other materials provided with the distribution. 95 | 96 | * Neither the name of Micah Carrick nor the names of its 97 | contributors may be used to endorse or promote products derived from this 98 | software without specific prior written permission. 99 | 100 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 101 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 102 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 103 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 104 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 105 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 106 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 107 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 108 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 109 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 110 | -------------------------------------------------------------------------------- /sourcecodebrowser.plugin: -------------------------------------------------------------------------------- 1 | [Plugin] 2 | Loader=python3 3 | Module=sourcecodebrowser 4 | IAge=3 5 | Name=Source Code Browser 6 | Description=A source code class and function browser for Gedit 3. 7 | Authors=Micah Carrick 8 | Copyright=Copyright © 2011 Micah Carrick 9 | Website=https://github.com/Quixotix/gedit-source-code-browser 10 | Version=3.0.3 11 | -------------------------------------------------------------------------------- /sourcecodebrowser/__init__.py: -------------------------------------------------------------------------------- 1 | from . import plugin 2 | from .plugin import SourceCodeBrowserPlugin 3 | -------------------------------------------------------------------------------- /sourcecodebrowser/ctags.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import shlex 4 | 5 | def get_ctags_version(executable=None): 6 | """ 7 | Return the text output from the --version option to ctags or None if ctags 8 | executable cannot be found. Use executable for custom ctags builds and/or 9 | path. 10 | """ 11 | args = shlex.split("ctags --version") 12 | try: 13 | p = subprocess.Popen(args, 0, shell=False, stdout=subprocess.PIPE, executable=executable) 14 | version = p.communicate()[0] 15 | except: 16 | version = None 17 | return version 18 | 19 | class Tag(object): 20 | """ 21 | Represents a ctags "tag" found in some source code. 22 | """ 23 | def __init__(self, name): 24 | self.name = name 25 | self.file = None 26 | self.ex_command = None 27 | self.kind = None 28 | self.fields = {} 29 | 30 | class Kind(object): 31 | """ 32 | Represents a ctags "kind" found in some source code such as "member" or 33 | "class". 34 | """ 35 | def __init__(self, name): 36 | self.name = name 37 | self.language = None 38 | 39 | def group_name(self): 40 | """ 41 | Return the kind name as a group name. For example, 'variable' would 42 | be 'Variables'. Pluralization is complex but this method is not. It 43 | works more often than not. 44 | """ 45 | group = self.name 46 | 47 | if self.name[-1] == 's': 48 | group += 'es' 49 | elif self.name[-1] == 'y': 50 | group = self.name[0:-1] + 'ies' 51 | else: 52 | group += 's' 53 | 54 | return group.capitalize() 55 | 56 | def icon_name(self): 57 | """ 58 | Return the icon name in the form of 'source-'. 59 | """ 60 | return 'source-' + self.name 61 | 62 | class Parser(object): 63 | """ 64 | Ctags Parser 65 | 66 | Parses the output of a ctags command into a list of tags and a dictionary 67 | of kinds. 68 | """ 69 | def has_kind(self, kind_name): 70 | """ 71 | Return true if kind_name is found in the list of kinds. 72 | """ 73 | if kind_name in self.kinds: 74 | return True 75 | else: 76 | return False 77 | 78 | def __init__(self): 79 | self.tags = [] 80 | self.kinds = {} 81 | self.tree = {} 82 | 83 | def parse(self, command, executable=None): 84 | """ 85 | Parse ctags tags from the output of a ctags command. For example: 86 | ctags -n --fields=fiKmnsSzt -f - some_file.php 87 | """ 88 | #args = [arg.replace('%20', ' ') for arg in shlex.split(command)] 89 | args = shlex.split(command) 90 | p = subprocess.Popen(args, 0, shell=False, stdout=subprocess.PIPE, executable=executable) 91 | symbols = self._parse_text(p.communicate()[0].decode('utf8')) 92 | 93 | def _parse_text(self, text): 94 | """ 95 | Parses ctags text which may have come from a TAG file or from raw output 96 | from a ctags command. 97 | """ 98 | for line in text.splitlines(): 99 | name = None 100 | file = None 101 | ex_command = None 102 | kind = None 103 | for i, field in enumerate(line.split("\t")): 104 | if i == 0: tag = Tag(field) 105 | elif i == 1: tag.file = field 106 | elif i == 2: tag.ex_command = field 107 | elif i > 2: 108 | if ":" in field: 109 | key, value = field.split(":")[0:2] 110 | tag.fields[key] = value 111 | if key == 'kind': 112 | kind = Kind(value) 113 | if not kind in self.kinds: 114 | self.kinds[value] = kind 115 | 116 | if kind is not None: 117 | if 'language' in tag.fields: 118 | kind.language = tag.fields['language'] 119 | tag.kind = kind 120 | 121 | self.tags.append(tag) 122 | """ 123 | def get_tree(self): 124 | tree = {} 125 | for tag in self.tags: 126 | if 'class' in tag.fields: 127 | parent = tag.fields['class'] 128 | if "." in parent: 129 | parents = parent.split(".") 130 | node = tree 131 | for p in parents: 132 | if not p in node: 133 | node[p] = {'tag':None, 'children':{}} 134 | node = node[p] 135 | print node 136 | node['tag'] = tag 137 | else: 138 | if not parent in self.tree: 139 | tree[parent] = {'tag':None, 'children':{}} 140 | tree[parent]['children'][tag.name] = {'tag':tag, 'children':{}} 141 | else: 142 | if tag.name in self.tree: 143 | tree[tag.name]['tag'] = tag 144 | else: 145 | tree[tag.name] = {'tag': tag, 'children':{}} 146 | return tree 147 | """ 148 | 149 | 150 | -------------------------------------------------------------------------------- /sourcecodebrowser/data/configure_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | False 6 | 5 7 | Source Code Browser 8 | dialog 9 | True 10 | True 11 | 12 | 13 | False 14 | vertical 15 | 2 16 | 17 | 18 | False 19 | end 20 | 21 | 22 | gtk-close 23 | True 24 | True 25 | True 26 | False 27 | True 28 | 29 | 30 | False 31 | True 32 | 0 33 | 34 | 35 | 36 | 37 | False 38 | True 39 | end 40 | 0 41 | 42 | 43 | 44 | 45 | True 46 | False 47 | vertical 48 | 49 | 50 | Show _line numbers in tree 51 | True 52 | True 53 | False 54 | False 55 | True 56 | 0 57 | True 58 | 59 | 60 | 61 | False 62 | True 63 | 6 64 | 0 65 | 66 | 67 | 68 | 69 | Load symbols from _remote files 70 | True 71 | True 72 | False 73 | False 74 | True 75 | 0 76 | True 77 | 78 | 79 | 80 | False 81 | True 82 | 6 83 | 1 84 | 85 | 86 | 87 | 88 | Start with rows _expanded 89 | True 90 | True 91 | False 92 | False 93 | True 94 | 0 95 | True 96 | 97 | 98 | 99 | False 100 | True 101 | 6 102 | 2 103 | 104 | 105 | 106 | 107 | _Sort list alphabetically 108 | True 109 | True 110 | False 111 | False 112 | True 113 | 0 114 | True 115 | 116 | 117 | 118 | False 119 | True 120 | 6 121 | 3 122 | 123 | 124 | 125 | 126 | True 127 | False 128 | 129 | 130 | True 131 | False 132 | 0 133 | ctags executable 134 | 135 | 136 | False 137 | True 138 | 0 139 | 140 | 141 | 142 | 143 | True 144 | True 145 | 146 | ctags 147 | True 148 | 149 | 150 | 151 | False 152 | True 153 | 6 154 | 1 155 | 156 | 157 | 158 | 159 | False 160 | True 161 | 6 162 | 4 163 | 164 | 165 | 166 | 167 | False 168 | True 169 | 6 170 | 1 171 | 172 | 173 | 174 | 175 | 176 | button1 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/missing-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/missing-image.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-class.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-code-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-code-browser.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-define.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-define.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-enumerator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-enumerator.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-field.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-function.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-macro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-macro.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-member.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-member.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-method.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-method.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-namespace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-namespace.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-property.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-property.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-struct.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-table.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-typedef.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-typedef.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/icons/16x16/source-variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toobaz/gedit-source-code-browser/2eae5cde75e98821f3f508f0ac6ed2b543508c27/sourcecodebrowser/data/icons/16x16/source-variable.png -------------------------------------------------------------------------------- /sourcecodebrowser/data/org.gnome.gedit.plugins.sourcecodebrowser.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | Show Line Numbers 7 | Show the line number of each item in the source tree. 8 | 9 | 10 | true 11 | Load Remote Files 12 | Download remote files into a temporary file for parsing the symbols. 13 | 14 | 15 | true 16 | Expand Rows 17 | Expand the entire source tree when initially loaded. 18 | 19 | 20 | true 21 | Sort List 22 | Sorts the list of symbols alphabetically rather than by where they occur in the source file. 23 | 24 | 25 | 'ctags' 26 | Ctags Executable 27 | Specifies the executable path for Exuberant Ctags. 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sourcecodebrowser/plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import tempfile 5 | from . import ctags 6 | from gi.repository import GObject, GdkPixbuf, Gedit, Gtk, PeasGtk, Gio 7 | 8 | logging.basicConfig() 9 | LOG_LEVEL = logging.WARN 10 | SETTINGS_SCHEMA = "org.gnome.gedit.plugins.sourcecodebrowser" 11 | DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') 12 | ICON_DIR = os.path.join(DATA_DIR, 'icons', '16x16') 13 | 14 | class SourceTree(Gtk.Box): 15 | """ 16 | Source Tree Widget 17 | 18 | A treeview storing the heirarchy of source code symbols within a particular 19 | document. Requries exhuberant-ctags. 20 | """ 21 | __gsignals__ = { 22 | "tag-activated": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,)), 23 | } 24 | 25 | def __init__(self): 26 | Gtk.Box.__init__(self) 27 | self.set_orientation(Gtk.Orientation.VERTICAL) 28 | self._log = logging.getLogger(self.__class__.__name__) 29 | self._log.setLevel(LOG_LEVEL) 30 | self._pixbufs = {} 31 | self._current_uri = None 32 | self.expanded_rows = {} 33 | 34 | # preferences (should be set by plugin) 35 | self.show_line_numbers = True 36 | self.ctags_executable = 'ctags' 37 | self.expand_rows = True 38 | self.sort_list = True 39 | self.create_ui() 40 | self.show_all() 41 | 42 | def get_missing_pixbuf(self): 43 | """ Used for symbols that do not have a known image. """ 44 | if not 'missing' in self._pixbufs: 45 | filename = os.path.join(ICON_DIR, "missing-image.png") 46 | self._pixbufs['missing'] = GdkPixbuf.Pixbuf.new_from_file(filename) 47 | 48 | return self._pixbufs['missing'] 49 | 50 | def get_pixbuf(self, icon_name): 51 | """ 52 | Get the pixbuf for a specific icon name fron an internal dictionary of 53 | pixbufs. If the icon is not already in the dictionary, it will be loaded 54 | from an external file. 55 | """ 56 | if icon_name not in self._pixbufs: 57 | filename = os.path.join(ICON_DIR, icon_name + ".png") 58 | if os.path.exists(filename): 59 | try: 60 | self._pixbufs[icon_name] = GdkPixbuf.Pixbuf.new_from_file(filename) 61 | except Exception as e: 62 | self._log.warn("Could not load pixbuf for icon '%s': %s", 63 | icon_name, 64 | str(e)) 65 | self._pixbufs[icon_name] = self.get_missing_pixbuf() 66 | else: 67 | self._pixbufs[icon_name] = self.get_missing_pixbuf() 68 | 69 | return self._pixbufs[icon_name] 70 | 71 | def clear(self): 72 | """ Clear the tree view so that new data can be loaded. """ 73 | if self.expand_rows: 74 | self._save_expanded_rows() 75 | self._store.clear() 76 | 77 | def create_ui(self): 78 | """ Craete the main user interface and pack into box. """ 79 | self._store = Gtk.TreeStore(GdkPixbuf.Pixbuf, # icon 80 | GObject.TYPE_STRING, # name 81 | GObject.TYPE_STRING, # kind 82 | GObject.TYPE_STRING, # uri 83 | GObject.TYPE_STRING, # line 84 | GObject.TYPE_STRING) # markup 85 | self._treeview = Gtk.TreeView.new_with_model(self._store) 86 | self._treeview.set_headers_visible(False) 87 | column = Gtk.TreeViewColumn("Symbol") 88 | cell = Gtk.CellRendererPixbuf() 89 | column.pack_start(cell, False) 90 | column.add_attribute(cell, 'pixbuf', 0) 91 | cell = Gtk.CellRendererText() 92 | column.pack_start(cell, True) 93 | column.add_attribute(cell, 'markup', 5) 94 | self._treeview.append_column(column) 95 | 96 | self._treeview.connect("row-activated", self.on_row_activated) 97 | 98 | sw = Gtk.ScrolledWindow() 99 | sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 100 | sw.add(self._treeview) 101 | self.pack_start(sw, True, True, 0) 102 | 103 | def _get_tag_iter(self, tag, parent_iter=None): 104 | """ 105 | Get the tree iter for the specified tag, or None if the tag cannot 106 | be found. 107 | """ 108 | tag_iter = self._store.iter_children(parent_iter) 109 | while tag_iter: 110 | if self._store.get_value(tag_iter, 1) == tag.name: 111 | return tag_iter 112 | tag_iter = self._store.iter_next(tag_iter) 113 | 114 | return None 115 | 116 | def _get_kind_iter(self, kind, uri, parent_iter=None): 117 | """ 118 | Get the iter for the specified kind. Creates a new node if the iter 119 | is not found under the specirfied parent_iter. 120 | """ 121 | kind_iter = self._store.iter_children(parent_iter) 122 | while kind_iter: 123 | if self._store.get_value(kind_iter, 2) == kind.name: 124 | return kind_iter 125 | kind_iter = self._store.iter_next(kind_iter) 126 | 127 | # Kind node not found, so we'll create it. 128 | pixbuf = self.get_pixbuf(kind.icon_name()) 129 | markup = "%s" % kind.group_name() 130 | kind_iter = self._store.append(parent_iter, (pixbuf, 131 | kind.group_name(), 132 | kind.name, 133 | uri, 134 | None, 135 | markup)) 136 | return kind_iter 137 | 138 | def load(self, kinds, tags, uri): 139 | """ 140 | Load the tags into the treeview and restore the expanded rows if 141 | applicable. 142 | """ 143 | self._current_uri = uri 144 | # load root-level tags first 145 | for i, tag in enumerate(tags): 146 | if "class" not in tag.fields: 147 | parent_iter = None 148 | pixbuf = self.get_pixbuf(tag.kind.icon_name()) 149 | if 'line' in tag.fields and self.show_line_numbers: 150 | markup = "%s [%s]" % (tag.name, tag.fields['line']) 151 | else: 152 | markup = tag.name 153 | kind_iter = self._get_kind_iter(tag.kind, uri, parent_iter) 154 | new_iter = self._store.append(kind_iter, (pixbuf, 155 | tag.name, 156 | tag.kind.name, 157 | uri, 158 | tag.fields['line'], 159 | markup)) 160 | # second level tags 161 | for tag in tags: 162 | if "class" in tag.fields and "." not in tag.fields['class']: 163 | pixbuf = self.get_pixbuf(tag.kind.icon_name()) 164 | if 'line' in tag.fields and self.show_line_numbers: 165 | markup = "%s [%s]" % (tag.name, tag.fields['line']) 166 | else: 167 | markup = tag.name 168 | for parent_tag in tags: 169 | if parent_tag.name == tag.fields['class']: 170 | break 171 | kind_iter = self._get_kind_iter(parent_tag.kind, uri, None) 172 | parent_iter = self._get_tag_iter(parent_tag, kind_iter) 173 | kind_iter = self._get_kind_iter(tag.kind, uri, parent_iter) # for sub-kind nodes 174 | new_iter = self._store.append(kind_iter, (pixbuf, 175 | tag.name, 176 | tag.kind.name, 177 | uri, 178 | tag.fields['line'], 179 | markup)) 180 | # TODO: We need to go at least one more level to deal with the inline 181 | # classes used in many python projects (eg. Models in Django) 182 | # Recursion would be even better. 183 | 184 | # sort 185 | if self.sort_list: 186 | self._store.set_sort_column_id(1, Gtk.SortType.ASCENDING) 187 | 188 | # expand 189 | if uri in self.expanded_rows: 190 | for strpath in self.expanded_rows[uri]: 191 | path = Gtk.TreePath.new_from_string(strpath) 192 | if path: 193 | self._treeview.expand_row(path, False) 194 | elif uri not in self.expanded_rows and self.expand_rows: 195 | self._treeview.expand_all() 196 | """ 197 | curiter = self._store.get_iter_first() 198 | while curiter: 199 | path = self._store.get_path(curiter) 200 | self._treeview.expand_row(path, False) 201 | curiter = self._store.iter_next(iter) 202 | """ 203 | 204 | def on_row_activated(self, treeview, path, column, data=None): 205 | """ 206 | If the row has uri and line number information, emits the tag-activated 207 | signal so that the editor can jump to the tag's location. 208 | """ 209 | model = treeview.get_model() 210 | iter = model.get_iter(path) 211 | uri = model.get_value(iter, 3) 212 | line = model.get_value(iter, 4) 213 | if uri and line: 214 | self.emit("tag-activated", (uri, line)) 215 | 216 | def parse_file(self, path, uri): 217 | """ 218 | Parse symbols out of a file using exhuberant ctags. The path is the local 219 | filename to pass to ctags, and the uri is the actual URI as known by 220 | Gedit. They would be different for remote files. 221 | """ 222 | command = "ctags -nu --fields=fiKlmnsSzt -f - '%s'" % path 223 | #self._log.debug(command) 224 | try: 225 | parser = ctags.Parser() 226 | parser.parse(command, self.ctags_executable) 227 | except Exception as e: 228 | self._log.warn("Could not execute ctags: %s (executable=%s)", 229 | str(e), 230 | self.ctags_executable) 231 | self.load(parser.kinds, parser.tags, uri) 232 | 233 | 234 | def _save_expanded_rows(self): 235 | self.expanded_rows[self._current_uri] = [] 236 | self._treeview.map_expanded_rows(self._save_expanded_rows_mapping_func, 237 | self._current_uri) 238 | 239 | def _save_expanded_rows_mapping_func(self, treeview, path, uri): 240 | self.expanded_rows[uri].append(str(path)) 241 | 242 | 243 | class Config(object): 244 | def __init__(self): 245 | self._log = logging.getLogger(self.__class__.__name__) 246 | self._log.setLevel(LOG_LEVEL) 247 | 248 | def get_widget(self, has_schema): 249 | filename = os.path.join(DATA_DIR, 'configure_dialog.ui') 250 | builder = Gtk.Builder() 251 | try: 252 | count = builder.add_objects_from_file(filename, ["configure_widget"]) 253 | assert(count > 0) 254 | except Exception as e: 255 | self._log.error("Failed to load %s: %s." % (filename, str(e))) 256 | return None 257 | widget = builder.get_object("configure_widget") 258 | widget.set_border_width(12) 259 | 260 | if not has_schema: 261 | widget.set_sensitive(False) 262 | else: 263 | self._settings = Gio.Settings.new(SETTINGS_SCHEMA) 264 | builder.get_object("show_line_numbers").set_active( 265 | self._settings.get_boolean('show-line-numbers') 266 | ) 267 | builder.get_object("expand_rows").set_active( 268 | self._settings.get_boolean('expand-rows') 269 | ) 270 | builder.get_object("load_remote_files").set_active( 271 | self._settings.get_boolean('load-remote-files') 272 | ) 273 | builder.get_object("sort_list").set_active( 274 | self._settings.get_boolean('sort-list') 275 | ) 276 | builder.get_object("ctags_executable").set_text( 277 | self._settings.get_string('ctags-executable') 278 | ) 279 | builder.connect_signals(self) 280 | return widget 281 | 282 | def on_show_line_numbers_toggled(self, button, data=None): 283 | self._settings.set_boolean('show-line-numbers', button.get_active()) 284 | 285 | def on_expand_rows_toggled(self, button, data=None): 286 | self._settings.set_boolean('expand-rows', button.get_active()) 287 | 288 | def on_load_remote_files_toggled(self, button, data=None): 289 | self._settings.set_boolean('load-remote-files', button.get_active()) 290 | 291 | def on_sort_list_toggled(self, button, data=None): 292 | self._settings.set_boolean('sort-list', button.get_active()) 293 | 294 | def on_ctags_executable_changed(self, editable, data=None): 295 | self._settings.set_string('ctags-executable', editable.get_text()) 296 | 297 | 298 | class SourceCodeBrowserPlugin(GObject.Object, Gedit.WindowActivatable, PeasGtk.Configurable): 299 | """ 300 | Source Code Browser Plugin for Gedit 3.x 301 | 302 | Adds a tree view to the side panel of a Gedit window which provides a list 303 | of programming symbols (functions, classes, variables, etc.). 304 | 305 | https://live.gnome.org/Gedit/PythonPluginHowTo 306 | """ 307 | __gtype_name__ = "SourceCodeBrowserPlugin" 308 | window = GObject.property(type=Gedit.Window) 309 | 310 | def __init__(self): 311 | GObject.Object.__init__(self) 312 | self._log = logging.getLogger(self.__class__.__name__) 313 | self._log.setLevel(LOG_LEVEL) 314 | self._is_loaded = False 315 | self._ctags_version = None 316 | 317 | filename = os.path.join(ICON_DIR, "source-code-browser.png") 318 | self.icon = Gtk.Image.new_from_file(filename) 319 | 320 | def do_create_configure_widget(self): 321 | return Config().get_widget(self._has_settings_schema()) 322 | 323 | def do_activate(self): 324 | """ Activate plugin """ 325 | self._log.debug("Activating plugin") 326 | self._init_settings() 327 | self._version_check() 328 | self._sourcetree = SourceTree() 329 | self._sourcetree.ctags_executable = self.ctags_executable 330 | self._sourcetree.show_line_numbers = self.show_line_numbers 331 | self._sourcetree.expand_rows = self.expand_rows 332 | self._sourcetree.sort_list = self.sort_list 333 | panel = self.window.get_side_panel() 334 | panel.add_titled(self._sourcetree, "SymbolBrowserPlugin", "Source Code") 335 | self._handlers = [] 336 | hid = self._sourcetree.connect("draw", self.on_sourcetree_draw) 337 | self._handlers.append((self._sourcetree, hid)) 338 | if self.ctags_version is not None: 339 | hid = self._sourcetree.connect('tag-activated', self.on_tag_activated) 340 | self._handlers.append((self._sourcetree, hid)) 341 | hid = self.window.connect("active-tab-state-changed", self.on_tab_state_changed) 342 | self._handlers.append((self.window, hid)) 343 | hid = self.window.connect("active-tab-changed", self.on_active_tab_changed) 344 | self._handlers.append((self.window, hid)) 345 | hid = self.window.connect("tab-removed", self.on_tab_removed) 346 | self._handlers.append((self.window, hid)) 347 | else: 348 | self._sourcetree.set_sensitive(False) 349 | 350 | def do_deactivate(self): 351 | """ Deactivate the plugin """ 352 | self._log.debug("Deactivating plugin") 353 | for obj, hid in self._handlers: 354 | obj.disconnect(hid) 355 | self._handlers = None 356 | pane = self.window.get_side_panel() 357 | pane.remove(self._sourcetree) 358 | self._sourcetree = None 359 | 360 | def _has_settings_schema(self): 361 | schemas = Gio.Settings.list_schemas() 362 | if not SETTINGS_SCHEMA in schemas: 363 | return False 364 | else: 365 | return True 366 | 367 | def _init_settings(self): 368 | """ Initialize GSettings if available. """ 369 | if self._has_settings_schema(): 370 | settings = Gio.Settings.new(SETTINGS_SCHEMA) 371 | self.load_remote_files = settings.get_boolean("load-remote-files") 372 | self.show_line_numbers = settings.get_boolean("show-line-numbers") 373 | self.expand_rows = settings.get_boolean("expand-rows") 374 | self.sort_list = settings.get_boolean("sort-list") 375 | self.ctags_executable = settings.get_string("ctags-executable") 376 | settings.connect("changed::load-remote-files", self.on_setting_changed) 377 | settings.connect("changed::show-line-numbers", self.on_setting_changed) 378 | settings.connect("changed::expand-rows", self.on_setting_changed) 379 | settings.connect("changed::sort-list", self.on_setting_changed) 380 | settings.connect("changed::ctags-executable", self.on_setting_changed) 381 | self._settings = settings 382 | else: 383 | self._log.warn("Settings schema not installed. Plugin will not be configurable.") 384 | self._settings = None 385 | self.load_remote_files = True 386 | self.show_line_numbers = False 387 | self.expand_rows = True 388 | self.sort_list = True 389 | self.ctags_executable = 'ctags' 390 | 391 | def _load_active_document_symbols(self): 392 | """ Load the symbols for the given URI. """ 393 | self._sourcetree.clear() 394 | self._is_loaded = False 395 | # do not load if not the active tab in the panel 396 | panel = self.window.get_side_panel() 397 | if panel.get_visible_child() != self._sourcetree: 398 | return 399 | 400 | document = self.window.get_active_document() 401 | if document: 402 | location = document.get_location() 403 | if location: 404 | uri = location.get_uri() 405 | self._log.debug("Loading %s...", uri) 406 | if uri is not None: 407 | if uri[:7] == "file://": 408 | # use get_parse_name() to get path in UTF-8 409 | filename = location.get_parse_name() 410 | self._sourcetree.parse_file(filename, uri) 411 | elif self.load_remote_files: 412 | basename = location.get_basename() 413 | fd, filename = tempfile.mkstemp('.'+basename) 414 | contents = document.get_text(document.get_start_iter(), 415 | document.get_end_iter(), 416 | True) 417 | os.write(fd, bytes(contents, 'UTF-8')) 418 | os.close(fd) 419 | while Gtk.events_pending(): 420 | Gtk.main_iteration() 421 | self._sourcetree.parse_file(filename, uri) 422 | os.unlink(filename) 423 | self._loaded_document = document 424 | self._is_loaded = True 425 | 426 | def on_active_tab_changed(self, window, tab, data=None): 427 | self._load_active_document_symbols() 428 | 429 | def on_setting_changed(self, settings, key, data=None): 430 | """ 431 | self.load_remote_files = True 432 | self.show_line_numbers = False 433 | self.expand_rows = True 434 | self.ctags_executable = 'ctags' 435 | """ 436 | if key == 'load-remote-files': 437 | self.load_remote_files = self._settings.get_boolean(key) 438 | elif key == 'show-line-numbers': 439 | self.show_line_numbers = self._settings.get_boolean(key) 440 | elif key == 'expand-rows': 441 | self.expand_rows = self._settings.get_boolean(key) 442 | elif key == 'sort-list': 443 | self.sort_list = self._settings.get_boolean(key) 444 | elif key == 'ctags-executable': 445 | self.ctags_executable = self._settings.get_string(key) 446 | 447 | if self._sourcetree is not None: 448 | self._sourcetree.ctags_executable = self.ctags_executable 449 | self._sourcetree.show_line_numbers = self.show_line_numbers 450 | self._sourcetree.expand_rows = self.expand_rows 451 | self._sourcetree.sort_list = self.sort_list 452 | self._sourcetree.expanded_rows = {} 453 | self._load_active_document_symbols() 454 | 455 | def on_sourcetree_draw(self, sourcetree, data=None): 456 | if not self._is_loaded: 457 | self._load_active_document_symbols() 458 | return False 459 | 460 | def on_tab_state_changed(self, window, data=None): 461 | self._load_active_document_symbols() 462 | 463 | def on_tab_removed(self, window, tab, data=None): 464 | if not self.window.get_active_document(): 465 | self._sourcetree.clear() 466 | 467 | def on_tag_activated(self, sourcetree, location, data=None): 468 | """ Go to the line where the double-clicked symbol is defined. """ 469 | uri, line = location 470 | self._log.debug("%s, line %s." % (uri, line)) 471 | document = self.window.get_active_document() 472 | view = self.window.get_active_view() 473 | line = int(line) - 1 # lines start from 0 474 | document.goto_line(line) 475 | view.scroll_to_cursor() 476 | 477 | def _version_check(self): 478 | """ Make sure the exhuberant ctags is installed. """ 479 | self.ctags_version = ctags.get_ctags_version(self.ctags_executable) 480 | if not self.ctags_version: 481 | self._log.warn("Could not find ctags executable: %s" % 482 | (self.ctags_executable)) 483 | 484 | 485 | 486 | --------------------------------------------------------------------------------