diff --git a/application.py b/application.py index da3ba38..a7b2a87 100755 --- a/application.py +++ b/application.py @@ -152,7 +152,7 @@ class Application(object): 'method', 'method.svn', 'method.cvs', 'method.search', 'method.buffers', 'method.move', 'method.shell', 'method.introspect', 'method.help', 'method.numbers', - 'method.spell', 'method.hg', 'method.utf8', + 'method.spell', 'method.hg', 'method.utf8', 'method.tags', ) for name in names: exec("import %s" % name) @@ -231,6 +231,15 @@ class Application(object): self.registers = {} self.arg_history = {'default': []} + # this is used to maintain state about various things (ctags, version + # control, etc) which is shared across several different buffers. + # this is so it will be correctly inherited by new buffers, and also + # so that when buffers are closed this state won't be lost. + self.state = { + 'tags': {}, + 'vc': {}, + } + # initialize tab handlers completer.set_completer('path', completer.FileCompleter(self)) completer.set_completer('buffer', completer.BufferCompleter(self)) diff --git a/default.py b/default.py index 22826da..378c0ca 100644 --- a/default.py +++ b/default.py @@ -15,6 +15,18 @@ def last_buffer(w): def current_buffer(w): return w.buffer.name() +def current_word(w): + return w.get_word() or '' + +def current_token(w): + if w.mode.name not in w.buffer.highlights: + return '' + token = w.get_token() + if token: + return token.string + else: + return '' + def last_replace_before(w): a = w.application if a.config.get('use_last_replace') and a.last_replace_before: diff --git a/etags.py b/etags.py new file mode 100644 index 0000000..0bbc19d --- /dev/null +++ b/etags.py @@ -0,0 +1,186 @@ +""" +Etags parser + +:author: Dan Williams +""" + +import os +import re +from stat import ST_MTIME +from subprocess import Popen, PIPE, STDOUT + + +class EtagsRunError(Exception): pass + + +class TagManager(object): + lang = None + prune = ('SCCS', 'RCS', 'CVS', '.svn', '.hg', '.git', '.bzr') + exts = set() + + def __init__(self, base='.'): + self.etags = None + self.base = base + self.path = os.path.join(self.base, 'TAGS') + self.update() + + def has(self, name): + return name in self.etags.tag_map + + def get(self, name): + return self.etags.tag_map.get(name, []) + + def update(self): + if self.is_outdated(): + self.run() + self.etags = Etags(self.path) + self.etags.parse() + + def run(self): + lf = '--language-force=%s' % self.lang + args = ['ctags', '-e', '-f', self.path, lf, '-L-'] + pipe = Popen(args, stdin=PIPE, stdout=PIPE, stderr=STDOUT) + indata = '\n'.join(self.get_paths()) + '\n' + outdata = pipe.communicate(indata) + if pipe.returncode != 0: + raise EtagsRunError(outdata) + + def get_paths(self): + return list(self._walk(mtime=0)) + + def is_outdated(self): + if not os.path.exists(self.path): + return True + mtime = os.stat(self.path)[ST_MTIME] + itr = self._walk(mtime) + try: + itr.next() + return True + except StopIteration: + return False + + def _walk(self, mtime=0): + paths = [] + for root, dirs, files in os.walk(self.base): + for d in dirs: + if d in self.prune: + dirs.remove(d) + for f in files: + path = os.path.join(root, f) + if not self._match(path): + continue + elif os.stat(path)[ST_MTIME] < mtime: + continue + else: + yield os.path.join(root, f) + raise StopIteration + + def _match(self, path): + _, ext = os.path.splitext(path) + return ext in self.exts + +class Etags(object): + def __init__(self, fname=None): + self.fname = fname + self.rawdata = None + self.record_list = [] + self.tag_map = {} + + def _load(self): + fd = file(self.fname, 'r') + self.rawdata = fd.read() + fd.close() + + def lookup(self, tag): + return self.tag_map[tag] + + def __getitem__(self, tag): + return self.lookup(tag) + + def parse(self, fname=None): + """ + Parser is based on the little info found in + Wikipedia: http://en.wikipedia.org/wiki/Ctags + """ + if fname: + self.fname = fname + self._load() + i = 0 + data_len = len(self.rawdata) + data = self.rawdata + while i < data_len: + if ord(data[i]) == 0xc: + i = self._parse_block(data, i+2) + + def _add_record(self, record): + self.record_list.append(record) + name = record.name + if name is None: + return + self.tag_map.setdefault(name, []) + self.tag_map[name].append(record) + + def _parse_block(self, data, i): + n = data[i:].find('\n') + i + l = data[i:n] + try: + filename, size = l.split(',') + except ValueError: + print i + raise + size = int(size) + subblock = data[n+1:n+size+1] + # ... + for lineitem in subblock.split('\n'): + if len(lineitem) == 0: + continue + record = self._parse_record(lineitem, filename) + self._add_record(record) + return n+size+1 + + def _parse_record(self, lineitem, filename): + try: + defn, rest = lineitem.split(chr(0x7f)) + except ValueError: + print lineitem + raise + name = None + if chr(0x01) in rest: + name, rest = rest.split(chr(0x01)) + else: + txt = defn.strip() + sp = re.split('[ ,;*()\t&=]', txt) + sp = [x for x in sp if x != ''] + if len(sp): + name = sp[-1] + + tokens = rest.split(',') + line = int(tokens[0]) + byte = int(tokens[1]) + + record = EtagRecord(path=filename, defn=defn, name=name, line=line, byte=byte) + return record + +class EtagRecord(object): + def __init__(self, **kwargs): + self.path = None + self.defn = None + self.name = None + self.line = -1 + self.byte = None + self.__dict__.update(kwargs) + + def __repr__(self): + return "%s [%s:%d]" % (self.name, self.path, self.line) + + + +if __name__ == '__main__': + import sys + from pprint import pprint + etags = Etags(sys.argv[1]) + etags.parse() + if len(sys.argv) > 2: + print etags[sys.argv[2]] + else: + pprint(etags.record_list) diff --git a/method/tags.py b/method/tags.py new file mode 100644 index 0000000..7cbb33b --- /dev/null +++ b/method/tags.py @@ -0,0 +1,91 @@ +import os.path +from method import Method, arg +import completer +from etags import TagManager +import default + +class FindTag(Method): + args = [arg('tag', p='Tag name: ', dv=default.current_token, ld=True, h='Search for a tag')] + def _execute(self, w, **vargs): + tag = vargs['tag'] + b = w.buffer + a = w.application + + InitTags().execute(w) + base = b.settings[w.mode.name].get('tag-base') + m = a.state['tags'][base] + + records = m.get(tag) + if not records: + w.set_error('tag %r was not found' % tag) + return + + if len(records) == 1: + b = a.open_path(records[0].path) + a.switch_buffer(b) + b.windows[0].goto_line(records[0].line) + w.set_error('found one record for tag %r' % tag) + return + + cwd = os.getcwd() + if not cwd.endswith('/'): + cwd += '/' + + tpls = [(r.path.replace(cwd, ''), r.line, r.defn) for r in records] + + data = '\n'.join(['%s:%d:%s' % tpl for tpl in tpls]) + '\n' + a.data_buffer("*Tag-Records*", data, switch_to=True, modename='error') + w.set_error('found %d records for tag %r' % (len(records), tag)) + + +class InitTags(Method): + manager_cls = None + def _save_manager(self, w, base): + m = w.mode.tagcls(base) + w.application.state['tags'][base] = m + w.buffer.settings[w.mode.name]['tag-base'] = base + ntag, nrec = len(m.etags.tag_map), len(m.etags.record_list) + w.set_error('%s: loaded %d names (%d records)' % (m.path, ntag, nrec)) + + def _execute(self, w, **vargs): + b = w.buffer + a = w.application + a.state.setdefault('tags', {}) + + if not b.path: + raise Exception('Buffer %r has no path' % b.name()) + + t = b.settings['C'].get('tag-base') + if t and t in a.state['tags']: + m = a.state['tags'][t] + if m.is_outdated(): + m.update() + ntag, nrec = len(m.etags.tag_map), len(m.etags.record_list) + fmt = '%s: updated %d names (%d records)' + w.set_error(fmt % (m.path, ntag, nrec)) + else: + w.set_error('%s: is up-to-date' % m.path) + return + + base = b.path + while 'tag-base' not in b.settings['C']: + base, tail = os.path.split(base) + if not tail: + break + + if base in a.state['tags']: + return self._save_manager(w, base) + elif os.path.exists(os.path.join(base, 'TAGS')): + return self._save_manager(w, base) + + if 'tag-base' not in b.settings['C']: + self._old_window = w + self._prompt = "Enter source directory: " + c = completer.get_completer('path') + d = os.path.dirname(w.buffer.path) + a.open_mini_buffer(self._prompt, self._cb, tabber=c, startvalue=d) + + def _cb(self, v): + w = self._old_window + w.application.close_mini_buffer() + self._save_manager(w, v) diff --git a/mode/__init__.py b/mode/__init__.py index 17d11c1..544c525 100644 --- a/mode/__init__.py +++ b/mode/__init__.py @@ -261,9 +261,9 @@ class Fundamental(Handler): self.add_bindings('insert-multiline-text', ('C-c m',)) self.add_bindings('increment', ('M-+', 'M-=')) self.add_bindings('decrement', ('M--',)) - self.add_bindings('uppercase-word', ('M-u',)) self.add_bindings('lowercase-word', ('M-l',)) + self.add_bindings('find-tag', ('M-.',)) # used for all word operations if not self.word_letters: diff --git a/mode/c.py b/mode/c.py index fd767f7..8343b7d 100644 --- a/mode/c.py +++ b/mode/c.py @@ -1,10 +1,14 @@ import os.path from subprocess import Popen, PIPE, STDOUT +from method import Method, arg from method.shell import Exec +from method.tags import InitTags from mode import Fundamental import tab +import completer from lex import Grammar, PatternRule, RegionRule, PatternMatchRule, OverridePatternRule from mode.python import StringGrammar2 +from etags import TagManager class CommentGrammar(Grammar): rules = [ @@ -148,6 +152,11 @@ class CTabber2(tab.StackTabber2): return t.fqisa('spaces', 'eol', 'c.comment', 'c.comment.start', 'c.comment.data', 'c.comment.null', 'c.comment.end') +class CTagManager(TagManager): + lang = 'C' + exts = set(('.c', '.h')) + + class CCheckSyntax(Exec): '''Build this C program (using the mode's make cmd)''' show_success = False @@ -207,11 +216,13 @@ class C(Fundamental): name = 'C' extensions = ['.c', '.h', '.cpp'] tabbercls = CTabber2 + tagcls = CTagManager grammar = CGrammar opentokens = ('delimiter',) opentags = {'(': ')', '[': ']', '{': '}'} closetokens = ('delimiter',) closetags = {')': '(', ']': '[', '}': '{'} + #actions = [CCheckSyntax, CMake, CInitTags] actions = [CCheckSyntax, CMake] format = "%(flag)s %(bname)s (%(mname)s) %(indent)s %(cursor)s %(perc)s [%(func)s] %(vc-info)s" commentc = '//'