Source

pida-main / pida / services / grepper / grepper.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# -*- coding: utf-8 -*-
"""
    :copyright: 2005-2008 by The PIDA Project
    :license: GPL 2 or later (see README/COPYING/LICENSE)
"""

import os, re, cgi
import gtk, gobject

from glob import fnmatch

from pygtkhelpers.ui.objectlist import Column
from pida.ui.views import PidaView
from pida.core.commands import CommandsConfig
from pida.core.service import Service
from pida.core.events import EventsConfig
from pida.core.options import OptionsConfig
from pida.core.features import FeaturesConfig
from pida.core.actions import ActionsConfig
from pygtkhelpers.gthreads import GeneratorTask

# locale
from pida.core.locale import Locale
locale = Locale('grepper')
_ = locale.gettext

class GrepperItem(object):
    """
    A match item in grepper.
    
    Contains the data for the matches path, and linenumber that if falls on, as
    well as actual line of code that matched, and the matches data.
    """

    def __init__(self, path, manager, linenumber=None, line=None, matches=None):
        self._manager = manager
        self._path = path
        self._line = line
        self._matches = matches
        self.linenumber = linenumber
        self.path = self._escape_text(self._path)
        self.line = self._format_line()

    def _markup_match(self, text):
        if len(self._manager._views):
            color = self._manager._views[0].matches_list.\
                        style.lookup_color('pida-match')
        else:
            color = None
        if color:
            #color = color.to_string() # gtk 2.12 or higher
            color = "#%04x%04x%04x" % (color.red, color.green, color.blue)
        if not color:
            color = "red"
        return ('<span color="%s"><b>%s</b></span>' %
                (color, self._escape_text(text)))

    def _escape_text(self, text):
        return cgi.escape(text)

    # This has to be quite ugly since we need to markup and split and escape as
    # we go along. Since once we have escaped things, we won't know where they
    # are
    #
    def _format_line(self):
        line = self._line
        line_pieces = []
        for match in self._matches:
            # ignore empty string matches
            if not match:
                continue
            # this should never happen
            if match not in line:
                continue
            # split the line into before the match, and after
            # leaving the after for searching
            prematch, line = line.split(match, 1)
            line_pieces.append(self._escape_text(prematch))
            line_pieces.append(self._markup_match(match))
        # Append the remainder
        line_pieces.append(self._escape_text(line))
        # strip() the line to give the view more vertical space
        return ''.join(line_pieces).strip()


class GrepperActionsConfig(ActionsConfig):
    actions = [
        gtk.Action('show_grepper',
            _('Find _in files'),
            _('Show the grepper view'),
            gtk.STOCK_FIND),

        gtk.Action('grep_current_word',
            _('Find word in _project'),
            _('Find the current word in the current project'),
            gtk.STOCK_FIND),

        gtk.Action('grep_current_word_file',
            _('Find word in document _directory'),
            _('Find the current word in current document directory'),
            gtk.STOCK_FIND),

        gtk.Action('show_grepper_search',
            _('Find in directory'),
            _('Find in directory'),
            gtk.STOCK_FIND),
        ]

    accels = {
        'show_grepper': '<Shift><Control>g',
        'grep_current_word': '<Shift><Control>question',
        'grep_current_word_file': '<Shift><Control><Alt>question',
    }

    def on_show_grepper_search(self, action):
        self.svc.show_grepper(action.contexts_kw['dir_name'])

    def on_show_grepper(self, action):
        self.svc.show_grepper_in_project_source_directory()

    def on_grep_current_word(self, action):
        self.svc.grep_current_word()

    def on_grep_current_word_file(self, action):
        document = self.svc.boss.cmd('buffer', 'get_current')
        if document is not None:
            self.svc.grep_current_word(document.directory)
        else:
            self.svc.error_dlg(_('There is no current document.'))


class GrepperView(PidaView):
    builder_file = 'grepper_window'
    locale = locale
    label_text = _('Find in Files')
    icon_name = gtk.STOCK_FIND

    def create_ui(self):
        self.grepper_dir = ''
        self.matches_list.set_columns([
            Column('linenumber', editable=False, title="#",),
            Column('path', editable=False, use_markup=True, sorted=True),
            Column('line', expand=True, editable=False, use_markup=True),
            ])

        # we should set this to the current project I think
        self.path_chooser.set_filename(os.path.expanduser('~/'))

        self._history = gtk.ListStore(gobject.TYPE_STRING)
        self.pattern_combo.set_model(self._history)
        self.pattern_combo.set_text_column(0)
        self.recursive.set_active(True)
        self.re_check.set_active(True)
        self.pattern_entry = self.pattern_combo.child
        self.pattern_entry.connect('activate', self._on_pattern_entry_activate)

        self.task = GeneratorTask(self.svc.grep, self.append_to_matches_list,
                                  self.grep_complete, pass_generator=True)
        self.running = False

    def on_matches_list__item_activated(self, ol, item):
        self.svc.boss.cmd('buffer', 'open_file', file_name=item.path,
                                                 line=item.linenumber)
        self.svc.boss.editor.cmd('grab_focus')

    def append_to_matches_list(self, grepper_item):
        # select the first item (slight hack)
        select = not len(self.matches_list)
        self.matches_list.append(grepper_item, select=select)

    def on_find_button__clicked(self, button):
        if self.running:
            self.stop()
            self.grep_complete()
        else:
            self.start_grep()

    def on_current_folder__clicked(self, button):
        cpath = self.svc.boss.cmd('filemanager', 'get_browsed_path')
        self.path_chooser.set_current_folder(cpath)

    def _on_pattern_entry_activate(self, entry):
        self.start_grep()

    def _translate_glob(self, glob):
        # ensure we dont just match the end of the string
        return fnmatch.translate(glob).rstrip('$').replace(r'\Z', '')

    def set_location(self, location):
        self.path_chooser.set_filename(location)
        # setting the location takes a *long* time
        self._hacky_extra_location = location

    def start_grep_for_word(self, word):
        if not word:
            self.svc.error_dlg(_('Empty search string'))
            self.close()
        else:
            self.pattern_entry.set_text(word)
            # very unlikely that a selected text is a regex
            self.re_check.set_active(False)
            self.start_grep()

    def start_grep(self):
        self.matches_list.clear()
        pattern = self.pattern_entry.get_text()
        self._history.insert(0, (pattern,))
        self.pattern_combo.set_active(0)
        location = self.path_chooser.get_filename()
        if location is None:
            location = self._hacky_extra_location
        recursive = self.recursive.get_active()

        self.matches_list.grab_focus()

        # data checking is done here as opposed to in the grep functions
        # because of threading
        if not pattern:
            self.svc.error_dlg(_('Empty search string'))
            return False

        if not os.path.exists(location):
            self.svc.boss.error_dlg(_('Path does not exist'))
            return False

        if not self.re_check.get_active():
            pattern = self._translate_glob(pattern)

        try:
            regex = re.compile(pattern)
        except Exception, e:
            # More verbose error dialog
            self.svc.error_dlg(
                _('Improper regular expression "%s"') % pattern,
                str(e))
            return False

        self.grep_started()
        self.task.start(location, regex, recursive)

    def can_be_closed(self):
        self.stop()
        return True

    def close(self):
        self.svc.boss.cmd('window', 'remove_view', view=self)

    def grep_started(self):
        self.running = True
        self.progress_bar.show()
        gobject.timeout_add(100, self.pulse)
        self.find_button.set_label(gtk.STOCK_STOP)

    def grep_complete(self):
        self.running = False
        self.find_button.set_label(gtk.STOCK_FIND)
        self.progress_bar.hide()

    def pulse(self):
        self.progress_bar.pulse()
        return self.running

    def stop(self):
        self.task.stop()
        self.grep_complete()


class GrepperCommandsConfig(CommandsConfig):
    
    # Are either of these commands necessary?
    def get_view(self):
        return self.svc.get_view()

    def present_view(self):
        return self.svc.boss.cmd('window', 'present_view',
                                 view=self.svc.get_view())

class GrepperOptions(OptionsConfig):

    def create_options(self):
        self.create_option(
            'maximum_results',
            _('Maximum Results'),
            int,
            500,
            _('The maximum number of results to find (approx).'),
        )

class GrepperEvents(EventsConfig):

    def subscribe_all_foreign(self):
        self.subscribe_foreign('project', 'project_switched',
            self.svc.set_current_project)

class GrepperFeatures(FeaturesConfig):

    def subscribe_all_foreign(self):
        self.subscribe_foreign('contexts', 'dir-menu',
            (self.svc, 'grepper-dir-menu.xml'))



class Grepper(Service):
    # format this docstring
    """
    Search text in files.

    Grepper is a graphical grep tool used for search through the contents of
    files for a given match or regular expression. 
    """
    actions_config = GrepperActionsConfig
    events_config = GrepperEvents
    options_config = GrepperOptions
    features_config = GrepperFeatures

    def pre_start(self):
        self.current_project_source_directory = None
        self._views = []

    def show_grepper_in_project_source_directory(self):
        if self.current_project_source_directory is None:
            path = os.getcwd()
        else:
            path = self.current_project_source_directory
        return self.show_grepper(path)

    def show_grepper(self, path):
        view = GrepperView(self)
        view.set_location(path)
        self.boss.cmd('window', 'add_view', paned='Terminal', view=view)
        self._views.append(view)
        return view

    def grep_current_word(self, path=None):
        if path is None:
            view = self.show_grepper_in_project_source_directory()
        else:
            view = self.show_grepper(path)
        self.boss.editor.cmd('call_with_selection_or_word',
                             callback=view.start_grep_for_word)

    def grep(self, top, regex, recursive=False, show_hidden=False,
             generator_task=None):
        """
        grep is a wrapper around _grep_file_list and _grep_file.
        """
        self._result_count = 0
        if os.path.isfile(top):
            file_results = self._grep_file(top, regex)
            for result in file_results:
                yield result
        elif recursive:
            for root, dirs, files in os.walk(top):
                if generator_task.is_stopped:
                    return
                # Remove hidden directories
                if os.path.basename(root).startswith('.') and not show_hidden:
                    del dirs[:]
                    continue
                for matches in self._grep_file_list(files, root, regex,
                                                generator_task=generator_task,
                                                show_hidden=show_hidden):
                    yield matches
        else:
            for matches in self._grep_file_list(os.listdir(top), top, regex,
                                                generator_task=generator_task,
                                                show_hidden=show_hidden):
                yield matches

    def _grep_file_list(self, file_list, root, regex, show_hidden=False,
                                               generator_task=None):
        """
        Grep for a list of files.

        takes as it's arguments a list of files to grep, the directory
        containing that list, and a regular expression to search for in them
        (optionaly whether or not to search hidden files).

        _grep_file_list itterates over that file list, and calls _grep_file on
        each of them with the supplied arguments.
        """
        for file in file_list:
            if generator_task and generator_task.is_stopped:
                return
            if self._result_count > self.opt('maximum_results'):
                break
            if file.startswith(".") and not show_hidden:
                continue
            # never do this, always use os.path.join
            # filename = "%s/%s" % (root, file,)
            filename = os.path.join(root, file)
            file_results = self._grep_file(filename, regex)
            for result in file_results:
                yield result

    def _grep_file(self, filename, regex):
        """
        Grep a file.

        Takes as it's arguments a full path to a file, and a regular expression
        to search for. It returns a generator that yields a GrepperItem for
        each cycle, that contains the path, line number and matches data.
        """
        try:
            with open(filename) as fp:
                # simple guess for binaries
                if '\0' in fp.read(4096):
                    return
                fp.seek(0)
                for linenumber, line in enumerate(fp):

                    line_matches = regex.findall(line)

                    if line_matches:
                        self._result_count += 1
                        # enumerate is 0 based, line numbers are 1 based
                        yield GrepperItem(filename, self, linenumber+1, line, line_matches)
        except IOError:
            pass

    def set_current_project(self, project):
        self.current_project_source_directory = project.source_directory
        #self.set_view_location(project.source_directory)

    def set_view_location(self, directory):
        self._view.set_location(directory)

    def stop(self):
        for view in self._views:
            view.stop()