1#    Gedit snippets plugin
2#    Copyright (C) 2005-2006  Jesse van den Kieboom <jesse@icecrew.nl>
3#
4#    This program is free software; you can redistribute it and/or modify
5#    it under the terms of the GNU General Public License as published by
6#    the Free Software Foundation; either version 2 of the License, or
7#    (at your option) any later version.
8#
9#    This program is distributed in the hope that it will be useful,
10#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12#    GNU General Public License for more details.
13#
14#    You should have received a copy of the GNU General Public License
15#    along with this program; if not, write to the Free Software
16#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18import os
19import weakref
20import sys
21import re
22
23from gi.repository import Gdk, Gtk
24
25import xml.etree.ElementTree as et
26from . import helper
27
28class NamespacedId:
29    def __init__(self, namespace, id):
30        if not id:
31            self.id = None
32        else:
33            if namespace:
34                self.id = namespace + '-'
35            else:
36                self.id = 'global-'
37
38            self.id += id
39
40class SnippetData:
41    PROPS = {'tag': '', 'text': '', 'description': 'New snippet',
42            'accelerator': '', 'drop-targets': ''}
43
44    def __init__(self, node, library):
45        self.priv_id = node.attrib.get('id')
46
47        self.set_library(library)
48        self.valid = False
49        self.set_node(node)
50
51    def can_modify(self):
52        return (self.library and (isinstance(self.library(), SnippetsUserFile)))
53
54    def set_library(self, library):
55        if library:
56            self.library = weakref.ref(library)
57        else:
58            self.library = None
59
60        self.id = NamespacedId(self.language(), self.priv_id).id
61
62    def set_node(self, node):
63        if self.can_modify():
64            self.node = node
65        else:
66            self.node = None
67
68        self.init_snippet_data(node)
69
70    def init_snippet_data(self, node):
71        if node is None:
72            return
73
74        self.override = node.attrib.get('override')
75
76        self.properties = {}
77        props = SnippetData.PROPS.copy()
78
79        # Store all properties present
80        for child in node:
81            if child.tag in props:
82                del props[child.tag]
83
84                # Normalize accelerator
85                if child.tag == 'accelerator' and child.text != None:
86                    keyval, mod = Gtk.accelerator_parse(child.text)
87
88                    if Gtk.accelerator_valid(keyval, mod):
89                        child.text = Gtk.accelerator_name(keyval, mod)
90                    else:
91                        child.text = ''
92
93                if self.can_modify():
94                    self.properties[child.tag] = child
95                else:
96                    self.properties[child.tag] = child.text or ''
97
98        # Create all the props that were not found so we stay consistent
99        for prop in props:
100            if self.can_modify():
101                child = et.SubElement(node, prop)
102
103                child.text = props[prop]
104                self.properties[prop] = child
105            else:
106                self.properties[prop] = props[prop]
107
108        self.check_validation()
109
110    def check_validation(self):
111        if not self['tag'] and not self['accelerator'] and not self['drop-targets']:
112            return False
113
114        library = Library()
115        keyval, mod = Gtk.accelerator_parse(self['accelerator'])
116
117        self.valid = library.valid_tab_trigger(self['tag']) and \
118                (not self['accelerator'] or library.valid_accelerator(keyval, mod))
119
120    def _format_prop(self, prop, value):
121        if prop == 'drop-targets' and value != '':
122            return re.split('\\s*[,;]\\s*', value)
123        else:
124            return value
125
126    def __getitem__(self, prop):
127        if prop in self.properties:
128            if self.can_modify():
129                return self._format_prop(prop, self.properties[prop].text or '')
130            else:
131                return self._format_prop(prop, self.properties[prop] or '')
132
133        return self._format_prop(prop, '')
134
135    def __setitem__(self, prop, value):
136        if not prop in self.properties:
137            return
138
139        if isinstance(value, list):
140            value = ','.join(value)
141
142        if not self.can_modify() and self.properties[prop] != value:
143            # ohoh, this is not can_modify, but it needs to be changed...
144            # make sure it is transfered to the changes file and set all the
145            # fields.
146            # This snippet data container will effectively become the container
147            # for the newly created node, but transparently to whoever uses
148            # it
149            self._override()
150
151        if self.can_modify() and self.properties[prop].text != value:
152            if self.library():
153                self.library().tainted = True
154
155            oldvalue = self.properties[prop].text
156            self.properties[prop].text = value
157
158            if prop == 'tag' or prop == 'accelerator' or prop == 'drop-targets':
159                container = Library().container(self.language())
160                container.prop_changed(self, prop, oldvalue)
161
162        self.check_validation()
163
164    def language(self):
165        if self.library and self.library():
166            return self.library().language
167        else:
168            return None
169
170    def is_override(self):
171        return self.override and Library().overridden[self.override]
172
173    def to_xml(self):
174        return self._create_xml()
175
176    def _create_xml(self, parent=None, update=False, attrib={}):
177        # Create a new node
178        if parent != None:
179            element = et.SubElement(parent, 'snippet', attrib)
180        else:
181            element = et.Element('snippet')
182
183        # Create all the properties
184        for p in self.properties:
185            prop = et.SubElement(element, p)
186            prop.text = self[p]
187
188            if update:
189                self.properties[p] = prop
190
191        return element
192
193    def _override(self):
194        # Find the user file
195        target = Library().get_user_library(self.language())
196
197        # Create a new node there with override
198        element = self._create_xml(target.root, True, {'override': self.id})
199
200        # Create an override snippet data, feed it element so that it stores
201        # all the values and then set the node to None so that it only contains
202        # the values in .properties
203        override = SnippetData(element, self.library())
204        override.set_node(None)
205        override.id = self.id
206
207        # Set our node to the new element
208        self.node = element
209
210        # Set the override to our id
211        self.override = self.id
212        self.id = None
213
214        # Set the new library
215        self.set_library(target)
216
217        # The library is tainted because we added this snippet
218        target.tainted = True
219
220        # Add the override
221        Library().overridden[self.override] = override
222
223    def revert(self, snippet):
224        userlib = self.library()
225        self.set_library(snippet.library())
226
227        userlib.remove(self.node)
228
229        self.set_node(None)
230
231        # Copy the properties
232        self.properties = snippet.properties
233
234        # Set the id
235        self.id = snippet.id
236
237        # Reset the override flag
238        self.override = None
239
240class SnippetsTreeBuilder(et.TreeBuilder):
241    def __init__(self, start=None, end=None):
242        et.TreeBuilder.__init__(self)
243        self.set_start(start)
244        self.set_end(end)
245
246    def set_start(self, start):
247        self._start_cb = start
248
249    def set_end(self, end):
250        self._end_cb = end
251
252    def start(self, tag, attrs):
253        result = et.TreeBuilder.start(self, tag, attrs)
254
255        if self._start_cb:
256            self._start_cb(result)
257
258        return result
259
260    def end(self, tag):
261        result = et.TreeBuilder.end(self, tag)
262
263        if self._end_cb:
264            self._end_cb(result)
265
266        return result
267
268class LanguageContainer:
269    def __init__(self, language):
270        self.language = language
271        self.snippets = []
272        self.snippets_by_prop = {'tag': {}, 'accelerator': {}, 'drop-targets': {}}
273        self.accel_group = Gtk.AccelGroup()
274        self._refs = 0
275
276    def _add_prop(self, snippet, prop, value=0):
277        if value == 0:
278            value = snippet[prop]
279
280        if not value or value == '':
281            return
282
283        helper.snippets_debug('Added ', prop ,' ', value, ' to ', str(self.language))
284
285        if prop == 'accelerator':
286            keyval, mod = Gtk.accelerator_parse(value)
287            self.accel_group.connect(keyval, mod, 0, \
288                    Library().accelerator_activated)
289
290        snippets = self.snippets_by_prop[prop]
291
292        if not isinstance(value, list):
293            value = [value]
294
295        for val in value:
296            if val in snippets:
297                snippets[val].append(snippet)
298            else:
299                snippets[val] = [snippet]
300
301    def _remove_prop(self, snippet, prop, value=0):
302        if value == 0:
303            value = snippet[prop]
304
305        if not value or value == '':
306            return
307
308        helper.snippets_debug('Removed ', prop, ' ', value, ' from ', str(self.language))
309
310        if prop == 'accelerator':
311            keyval, mod = Gtk.accelerator_parse(value)
312            self.accel_group.disconnect_key(keyval, mod)
313
314        snippets = self.snippets_by_prop[prop]
315
316        if not isinstance(value, list):
317            value = [value]
318
319        for val in value:
320            try:
321                snippets[val].remove(snippet)
322            except:
323                True
324
325    def append(self, snippet):
326        self.snippets.append(snippet)
327
328        self._add_prop(snippet, 'tag')
329        self._add_prop(snippet, 'accelerator')
330        self._add_prop(snippet, 'drop-targets')
331
332        return snippet
333
334    def remove(self, snippet):
335        try:
336            self.snippets.remove(snippet)
337        except:
338            True
339
340        self._remove_prop(snippet, 'tag')
341        self._remove_prop(snippet, 'accelerator')
342        self._remove_prop(snippet, 'drop-targets')
343
344    def prop_changed(self, snippet, prop, oldvalue):
345        helper.snippets_debug('PROP CHANGED (', prop, ')', oldvalue)
346
347        self._remove_prop(snippet, prop, oldvalue)
348        self._add_prop(snippet, prop)
349
350    def from_prop(self, prop, value):
351        snippets = self.snippets_by_prop[prop]
352
353        if prop == 'drop-targets':
354            s = []
355
356            # FIXME: change this to use
357            # gnomevfs.mime_type_get_equivalence when it comes
358            # available
359            for key, val in snippets.items():
360                if not value.startswith(key):
361                    continue
362
363                for snippet in snippets[key]:
364                    if not snippet in s:
365                        s.append(snippet)
366
367            return s
368        else:
369            if value in snippets:
370                return snippets[value]
371            else:
372                return []
373
374    def ref(self):
375        self._refs += 1
376
377        return True
378
379    def unref(self):
380        if self._refs > 0:
381            self._refs -= 1
382
383        return self._refs != 0
384
385class SnippetsSystemFile:
386    def __init__(self, path=None):
387        self.path = path
388        self.loaded = False
389        self.language = None
390        self.ok = True
391        self.need_id = True
392
393    def load_error(self, message):
394        sys.stderr.write("An error occurred loading " + self.path + ":\n")
395        sys.stderr.write(message + "\nSnippets in this file will not be " \
396                "available, please correct or remove the file.\n")
397
398    def _add_snippet(self, element):
399        if not self.need_id or element.attrib.get('id'):
400            self.loading_elements.append(element)
401
402    def set_language(self, element):
403        self.language = element.attrib.get('language')
404
405        if self.language:
406            self.language = self.language.lower()
407
408    def _set_root(self, element):
409        self.set_language(element)
410
411    def _preprocess_element(self, element):
412        if not self.loaded:
413            if not element.tag == "snippets":
414                self.load_error("Root element should be `snippets' instead " \
415                        "of `%s'" % element.tag)
416                return False
417            else:
418                self._set_root(element)
419                self.loaded = True
420        elif element.tag != 'snippet' and not self.insnippet:
421            self.load_error("Element should be `snippet' instead of `%s'" \
422                    % element.tag)
423            return False
424        else:
425            self.insnippet = True
426
427        return True
428
429    def _process_element(self, element):
430        if element.tag == 'snippet':
431            self._add_snippet(element)
432            self.insnippet = False
433
434        return True
435
436    def ensure(self):
437        if not self.ok or self.loaded:
438            return
439
440        self.load()
441
442    def parse_xml(self, readsize=16384):
443        if not self.path:
444            return
445
446        elements = []
447
448        builder = SnippetsTreeBuilder( \
449                lambda node: elements.append((node, True)), \
450                lambda node: elements.append((node, False)))
451
452        parser = et.XMLParser(target=builder)
453        self.insnippet = False
454
455        try:
456            f = open(self.path, "r", encoding='utf-8')
457        except IOError:
458            self.ok = False
459            return
460
461        while True:
462            try:
463                data = f.read(readsize)
464            except IOError:
465                self.ok = False
466                break
467
468            if not data:
469                break
470
471            try:
472                parser.feed(data)
473            except Exception:
474                self.ok = False
475                break
476
477            for element in elements:
478                yield element
479
480            del elements[:]
481
482        f.close()
483
484    def load(self):
485        if not self.ok:
486            return
487
488        helper.snippets_debug("Loading library (" + str(self.language) + "): " + \
489                self.path)
490
491        self.loaded = False
492        self.ok = False
493        self.loading_elements = []
494
495        for element in self.parse_xml():
496            if element[1]:
497                if not self._preprocess_element(element[0]):
498                    del self.loading_elements[:]
499                    return
500            else:
501                if not self._process_element(element[0]):
502                    del self.loading_elements[:]
503                    return
504
505        for element in self.loading_elements:
506            Library().add_snippet(self, element)
507
508        del self.loading_elements[:]
509        self.ok = True
510
511    # This function will get the language for a file by just inspecting the
512    # root element of the file. This is provided so that a cache can be built
513    # for which file contains which language.
514    # It returns the name of the language
515    def ensure_language(self):
516        if not self.loaded:
517            self.ok = False
518
519            for element in self.parse_xml(256):
520                if element[1]:
521                    if element[0].tag == 'snippets':
522                        self.set_language(element[0])
523                        self.ok = True
524
525                    break
526
527    def unload(self):
528        helper.snippets_debug("Unloading library (" + str(self.language) + "): " + \
529                self.path)
530        self.language = None
531        self.loaded = False
532        self.ok = True
533
534class SnippetsUserFile(SnippetsSystemFile):
535    def __init__(self, path=None):
536        SnippetsSystemFile.__init__(self, path)
537        self.tainted = False
538        self.need_id = False
539
540    def _set_root(self, element):
541        SnippetsSystemFile._set_root(self, element)
542        self.root = element
543
544    def add_prop(self, node, tag, data):
545        if data[tag]:
546            prop = et.SubElement(node, tag)
547            prop.text = data[tag]
548
549            return prop
550        else:
551            return None
552
553    def new_snippet(self, properties=None):
554        if (not self.ok) or self.root is None:
555            return None
556
557        element = et.SubElement(self.root, 'snippet')
558
559        if properties:
560            for prop in properties:
561                sub = et.SubElement(element, prop)
562                sub.text = properties[prop]
563
564        self.tainted = True
565
566        return Library().add_snippet(self, element)
567
568    def set_language(self, element):
569        SnippetsSystemFile.set_language(self, element)
570
571        filename = os.path.basename(self.path).lower()
572
573        if not self.language and filename == "global.xml":
574            self.modifier = True
575        elif self.language and filename == self.language + ".xml":
576            self.modifier = True
577        else:
578            self.modifier = False
579
580    def create_root(self, language):
581        if self.loaded:
582            helper.snippets_debug('Not creating root, already loaded')
583            return
584
585        if language:
586            root = et.Element('snippets', {'language': language})
587            self.path = os.path.join(Library().userdir, language.lower() + '.xml')
588        else:
589            root = et.Element('snippets')
590            self.path = os.path.join(Library().userdir, 'global.xml')
591
592        self._set_root(root)
593        self.loaded = True
594        self.ok = True
595        self.tainted = True
596        self.save()
597
598    def remove(self, element):
599        try:
600            self.root.remove(element)
601            self.tainted = True
602        except:
603            return
604
605        try:
606            self.root[0]
607        except:
608            # No more elements, this library is useless now
609            Library().remove_library(self)
610
611    def save(self):
612        if not self.ok or self.root is None or not self.tainted:
613            return
614
615        path = os.path.dirname(self.path)
616
617        try:
618            if not os.path.isdir(path):
619                os.makedirs(path, 0o755)
620        except OSError:
621            # TODO: this is bad...
622            sys.stderr.write("Error in making dirs\n")
623
624        try:
625            helper.write_xml(self.root, self.path, ('text', 'accelerator'))
626            self.tainted = False
627        except IOError:
628            # Couldn't save, what to do
629            sys.stderr.write("Could not save user snippets file to " + \
630                    self.path + "\n")
631
632    def unload(self):
633        SnippetsSystemFile.unload(self)
634        self.root = None
635
636class Singleton(object):
637    _instance = None
638
639    def __new__(cls, *args, **kwargs):
640        if not cls._instance:
641            cls._instance = super(Singleton, cls).__new__(
642                     cls, *args, **kwargs)
643            cls._instance.__init_once__()
644
645        return cls._instance
646
647class Library(Singleton):
648    def __init_once__(self):
649        self._accelerator_activated_cb = []
650        self.loaded = False
651        self.check_buffer = Gtk.TextBuffer()
652
653    def set_dirs(self, userdir, systemdirs):
654        self.userdir = userdir
655        self.systemdirs = systemdirs
656
657        self.libraries = {}
658        self.containers = {}
659        self.overridden = {}
660        self.loaded_ids = []
661
662        self.loaded = False
663
664    def add_accelerator_callback(self, cb):
665        self._accelerator_activated_cb.append(cb)
666
667    def remove_accelerator_callback(self, cb):
668        self._accelerator_activated_cb.remove(cb)
669
670    def accelerator_activated(self, group, obj, keyval, mod):
671        ret = False
672
673        for cb in self._accelerator_activated_cb:
674            ret = cb(group, obj, keyval, mod)
675
676            if ret:
677                break
678
679        return ret
680
681    def add_snippet(self, library, element):
682        container = self.container(library.language)
683        overrided = self.overrided(library, element)
684
685        if overrided:
686            overrided.set_library(library)
687            helper.snippets_debug('Snippet is overriden: ' + overrided['description'])
688            return None
689
690        snippet = SnippetData(element, library)
691
692        if snippet.id in self.loaded_ids:
693            helper.snippets_debug('Not added snippet ' + str(library.language) + \
694                    '::' + snippet['description'] + ' (duplicate)')
695            return None
696
697        snippet = container.append(snippet)
698        helper.snippets_debug('Added snippet ' + str(library.language) + '::' + \
699                snippet['description'])
700
701        if snippet and snippet.override:
702            self.add_override(snippet)
703
704        if snippet.id:
705            self.loaded_ids.append(snippet.id)
706
707        return snippet
708
709    def container(self, language):
710        language = self.normalize_language(language)
711
712        if not language in self.containers:
713            self.containers[language] = LanguageContainer(language)
714
715        return self.containers[language]
716
717    def get_user_library(self, language):
718        target = None
719
720        if language in self.libraries:
721            for library in self.libraries[language]:
722                if isinstance(library, SnippetsUserFile) and library.modifier:
723                    target = library
724                elif not isinstance(library, SnippetsUserFile):
725                    break
726
727        if not target:
728            # Create a new user file then
729            helper.snippets_debug('Creating a new user file for language ' + \
730                    str(language))
731            target = SnippetsUserFile()
732            target.create_root(language)
733            self.add_library(target)
734
735        return target
736
737    def new_snippet(self, language, properties=None):
738        language = self.normalize_language(language)
739        library = self.get_user_library(language)
740
741        return library.new_snippet(properties)
742
743    def revert_snippet(self, snippet):
744        # This will revert the snippet to the one it overrides
745        if not snippet.can_modify() or not snippet.override in self.overridden:
746            # It can't be reverted, shouldn't happen, but oh..
747            return
748
749        # The snippet in self.overriden only contains the property contents and
750        # the library it belongs to
751        revertto = self.overridden[snippet.override]
752        del self.overridden[snippet.override]
753
754        if revertto:
755            snippet.revert(revertto)
756
757            if revertto.id:
758                self.loaded_ids.append(revertto.id)
759
760    def remove_snippet(self, snippet):
761        if not snippet.can_modify() or snippet.is_override():
762            return
763
764        # Remove from the library
765        userlib = snippet.library()
766        userlib.remove(snippet.node)
767
768        # Remove from the container
769        container = self.containers[userlib.language]
770        container.remove(snippet)
771
772    def overrided(self, library, element):
773        id = NamespacedId(library.language, element.attrib.get('id')).id
774
775        if id in self.overridden:
776            snippet = SnippetData(element, None)
777            snippet.set_node(None)
778
779            self.overridden[id] = snippet
780            return snippet
781        else:
782            return None
783
784    def add_override(self, snippet):
785        helper.snippets_debug('Add override:', snippet.override)
786        if not snippet.override in self.overridden:
787            self.overridden[snippet.override] = None
788
789    def add_library(self, library):
790        library.ensure_language()
791
792        if not library.ok:
793            helper.snippets_debug('Library in wrong format, ignoring')
794            return False
795
796        helper.snippets_debug('Adding library (' + str(library.language) + '): ' + \
797                library.path)
798
799        if library.language in self.libraries:
800            # Make sure all the user files are before the system files
801            if isinstance(library, SnippetsUserFile):
802                self.libraries[library.language].insert(0, library)
803            else:
804                self.libraries[library.language].append(library)
805        else:
806            self.libraries[library.language] = [library]
807
808        return True
809
810    def remove_library(self, library):
811        if not library.ok:
812            return
813
814        if library.path and os.path.isfile(library.path):
815            os.unlink(library.path)
816
817        try:
818            self.libraries[library.language].remove(library)
819        except KeyError:
820            True
821
822        container = self.containers[library.language]
823
824        for snippet in list(container.snippets):
825            if snippet.library() == library:
826                container.remove(snippet)
827
828    def add_user_library(self, path):
829        library = SnippetsUserFile(path)
830        return self.add_library(library)
831
832    def add_system_library(self, path):
833        library = SnippetsSystemFile(path)
834        return self.add_library(library)
835
836    def find_libraries(self, path, searched, addcb):
837        helper.snippets_debug("Finding in: " + path)
838
839        if not os.path.isdir(path):
840            return searched
841
842        files = os.listdir(path)
843        searched.append(path)
844
845        for f in files:
846            f = os.path.realpath(os.path.join(path, f))
847
848            # Determine what language this file provides snippets for
849            if os.path.isfile(f):
850                addcb(f)
851
852        return searched
853
854    def normalize_language(self, language):
855        if language:
856            return language.lower()
857
858        return language
859
860    def remove_container(self, language):
861        for snippet in self.containers[language].snippets:
862            if snippet.id in self.loaded_ids:
863                self.loaded_ids.remove(snippet.id)
864
865            if snippet.override in self.overridden:
866                del self.overridden[snippet.override]
867
868        del self.containers[language]
869
870    def get_accel_group(self, language):
871        language = self.normalize_language(language)
872        container = self.container(language)
873
874        self.ensure(language)
875        return container.accel_group
876
877    def save(self, language):
878        language = self.normalize_language(language)
879
880        if language in self.libraries:
881            for library in self.libraries[language]:
882                if isinstance(library, SnippetsUserFile):
883                    library.save()
884                else:
885                    break
886
887    def ref(self, language):
888        language = self.normalize_language(language)
889
890        helper.snippets_debug('Ref:', language)
891        self.container(language).ref()
892
893    def unref(self, language):
894        language = self.normalize_language(language)
895
896        helper.snippets_debug('Unref:', language)
897
898        if language in self.containers:
899            if not self.containers[language].unref() and \
900                    language in self.libraries:
901
902                for library in self.libraries[language]:
903                    library.unload()
904
905                self.remove_container(language)
906
907    def ensure(self, language):
908        self.ensure_files()
909        language = self.normalize_language(language)
910
911        # Ensure language as well as the global snippets (None)
912        for lang in (None, language):
913            if lang in self.libraries:
914                # Ensure the container exists
915                self.container(lang)
916
917                for library in self.libraries[lang]:
918                    library.ensure()
919
920    def ensure_files(self):
921        if self.loaded:
922            return
923
924        searched = []
925        searched = self.find_libraries(self.userdir, searched, \
926                self.add_user_library)
927
928        for d in self.systemdirs:
929            searched = self.find_libraries(d, searched, \
930                    self.add_system_library)
931
932        self.loaded = True
933
934    def valid_accelerator(self, keyval, mod):
935        mod &= Gtk.accelerator_get_default_mod_mask()
936
937        return (mod and (Gdk.keyval_to_unicode(keyval) or \
938                keyval in range(Gdk.KEY_F1, Gdk.KEY_F12 + 1)))
939
940    def valid_tab_trigger(self, trigger):
941        if not trigger:
942            return True
943
944        return helper.is_tab_trigger(trigger)
945
946    # Snippet getters
947    # ===============
948    def _from_prop(self, prop, value, language=None):
949        self.ensure_files()
950
951        result = []
952        language = self.normalize_language(language)
953
954        if not language in self.containers:
955            return []
956
957        self.ensure(language)
958        result = self.containers[language].from_prop(prop, value)
959
960        if len(result) == 0 and language and None in self.containers:
961            result = self.containers[None].from_prop(prop, value)
962
963        return result
964
965    # Get snippets for a given language
966    def get_snippets(self, language=None):
967        self.ensure_files()
968        language = self.normalize_language(language)
969
970        if not language in self.libraries:
971            return []
972
973        self.ensure(language)
974
975        return list(self.containers[language].snippets)
976
977    # Get snippets for a given accelerator
978    def from_accelerator(self, accelerator, language=None):
979        return self._from_prop('accelerator', accelerator, language)
980
981    # Get snippets for a given tag
982    def from_tag(self, tag, language=None):
983        return self._from_prop('tag', tag, language)
984
985    # Get snippets for a given drop target
986    def from_drop_target(self, drop_target, language=None):
987        return self._from_prop('drop-targets', drop_target, language)
988
989# ex:ts=4:et:
990