chiark / gitweb /
Initial commit
authorRoss Younger <onyx-commits@impropriety.org.uk>
Thu, 28 Feb 2013 09:09:00 +0000 (22:09 +1300)
committerRoss Younger <onyx-commits@impropriety.org.uk>
Thu, 28 Feb 2013 09:09:00 +0000 (22:09 +1300)
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
TagsSearch.py [new file with mode: 0644]
find_definition.py [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..021454b
--- /dev/null
@@ -0,0 +1,3 @@
+*.pyc
+*~
+*.swp
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..ecf6487
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2013 Ross Younger
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..07a07ca
--- /dev/null
+++ b/README.md
@@ -0,0 +1,108 @@
+find-definition
+===============
+
+This is a script I lashed together to open up a new editor directly
+at the definition of a type, structure, function or anything else that
+ctags reports.
+
+I use it with vim.  With other editors you either need to have ctags
+support (contributions welcome!) or be prepared to use vi as an external
+source browser.
+
+
+Usage
+-----
+
+`$ find_definition.py fooBar`
+
+... and vim opens up in read-only mode, right at the definition of fooBar.
+(I keep a symlink to the main script on my PATH.)
+If the definition isn't found in the tags file, the script apologises.
+
+But, of course, this only works because I already have a tags file in
+place. Read on...
+
+
+Limitations
+-----------
+
+In the words of A.A. Milne, this script is a bear of little brain.
+It takes you only to the first match for the given tag.
+
+If there are multiple matches in the tags file, you need to drive
+your source browser to walk them.
+
+In vim:
+
+1. Put the cursor on the keyword
+2. Ctrl-] to do a tags search
+3. Use command :tn for next, :tp for previous.
+
+Users of other editors are welcome to contribute instructions!
+
+There is no tab-completion at present; tag matching is whole-string only.
+Contributions welcome (though I might just do it myself one day).
+
+My tags file at work is 78M but the searching is blink-of-an-eye fast.
+
+
+Setting up ctags
+----------------
+
+Install the exuberant-ctags package, if you don't already have it.
+
+`$ sudo apt-get install exuberant-ctags`
+
+You need to run ctags regularly to keep the tags file up to date.
+
+I have set up a personal cron job on my workstation to do this, here's the crontab line:
+
+    0 6 * * * /home/younger/bin/update-ctags
+
+The `update-ctags` script looks like this (redacted; edit to suit):
+
+    #!/bin/sh
+       
+    cd /home/younger/Work/Mainline
+    ctags -R --c++-kinds=+p --fields=+iaS --extra=+q SourceDir* AutoGenDir
+
+
+Configuration
+-------------
+
+By default, the script reads `~/Work/Mainline/tags` and uses my `ViewInvocation` class (which opens vim in read-only mode).
+
+You can change this behaviour by creating a config file `~/.find_definition`:
+
+    [DEFAULT]
+    tagsfile=/some/where/tags
+    invocation=MyClass
+
+`invocation` is the name of a class.
+If it's not within the main script then the script will attempt to import
+the named class (so you might want to set your `PYTHONPATH` suitably)
+and then construct it with no parameters.
+
+
+Invocation Classes
+------------------
+
+`ViewInvocation` is the default. It opens up the source in `view` (which is a read-only vim), in the current shell.
+
+`GViewInvocation` opens up the source in `gview` which spawns a new graphical vim, read-only mode.
+
+Contributions welcome!
+
+
+Writing your own Invocation class
+---------------------------------
+
+It's probably easiest to crib from one of the existing invocation classes.
+You are given the filename and the ex-command (vim-style regex search)
+that will take you to the tag.
+
+If your editor can't cope with regexp-style ex-commands, you might care to look
+into driving ctags with `--excmd=number` to have it use line numbers
+instead; see the ctags man page for a discussion of the advantages and
+disadvantages.
+
diff --git a/TagsSearch.py b/TagsSearch.py
new file mode 100644 (file)
index 0000000..b36d1ea
--- /dev/null
@@ -0,0 +1,94 @@
+import re
+
+class TagsResponse:
+   def __init__(self, filename, pattern):
+      self.filename = filename
+      self.pattern = pattern
+   def __str__(self):
+      return '[filename=%s pattern=%s]' % (self.filename, self.pattern)
+
+def quote_pattern(pat):
+   pat = re.sub('~', '\~', pat)
+   pat = re.sub('\*', '\\\*', pat)
+   return pat
+
+class CTag:
+   def __init__(self, line):
+      self.fields = line.split('\t')
+   def to_response(self):
+      return TagsResponse(self.filename(), self.pattern())
+   def tag(self):
+      return self.fields[0]
+   def filename(self):
+      return self.fields[1]
+   def pattern(self):
+      return quote_pattern(self.fields[2])
+
+class AbstractTagsSearcher:
+   '''
+   Tags searching, using an unspecified algorithm.
+   '''
+   def __init__(self, tagsfile):
+      self.tagsfile = tagsfile
+
+   def find(self, tag):
+      '''
+      Searches for a tag. Returns a TagsResponse, or None if not found.
+      NOTE: Only returns the first match it finds.
+      '''
+      raise 'Abstract interface!'
+
+class LinearTagsSearcher(AbstractTagsSearcher):
+   ''' Crungey old lsearch. '''
+   def find(self,tag):
+      f = open(self.tagsfile, 'r')
+      needle = tag + '\t'
+      for line in f:
+         if line.startswith(needle):
+            return CTag(line).to_response()
+      return None
+
+class BinaryTagsSearcher(AbstractTagsSearcher):
+   ''' bsearch is faster, particularly on large files '''
+   def find(self,needle):
+      f = open(self.tagsfile, 'r')
+      f.seek(0,2)
+      size = f.tell()
+
+      TOP = 0
+      BOTTOM = size
+      while True:
+         MID = (TOP+BOTTOM)/2
+         #print "Top %d Mid %d Bottom %d"%(TOP,MID,BOTTOM)
+         f.seek(MID,0)
+         f.readline() # throw away the partial line
+         MID = f.tell()
+         if MID>=BOTTOM:
+            # Endgame... naive binary search fails to take account of line lengths, so use TOP as a false pivot
+            MID = TOP
+            f.seek(TOP,0)
+            f.readline()
+            MID=f.tell()
+         if MID>=BOTTOM:
+            return None # No?
+         line = f.readline()
+         ctag = CTag(line)
+         tag = ctag.tag()
+         #print "..-> %s"%tag
+
+         if tag == needle:
+            return ctag.to_response() # Jackpot!
+         elif tag < needle:
+            TOP = f.tell()-1 # end of the rejected mid-record
+            #print "<<<"
+         else: # needle > tag
+            BOTTOM = MID # beginning of the rejected mid-record
+            #print ">>>"
+
+         if TOP>=BOTTOM:
+            return None # Simple termination
+
+class TagsSearcherFactory:
+   def get(self, tagsfile):
+      return BinaryTagsSearcher(tagsfile)
+
diff --git a/find_definition.py b/find_definition.py
new file mode 100755 (executable)
index 0000000..97d1d23
--- /dev/null
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+
+import sys, os, subprocess, ConfigParser
+
+from TagsSearch import *
+
+CONFIG = os.path.expanduser('~/.find_definition')
+CONFIG_SECTION = 'DEFAULT'
+KEY_TAGS = 'tagsfile'
+KEY_INVOCATION = 'invocation'
+
+class AbstractInvocation:
+   def invoke(self, filename, expattern):
+      raise TypeError,'Abstract interface!'
+
+class ViewInvocation(AbstractInvocation):
+   ''' view FILENAME -c PATTERN - give it control '''
+   def __init__(self):
+      self.CMD='view'
+      self.waitFor=True
+   def invoke(self, filename, expattern):
+      args = [self.CMD, filename, '-c', expattern, '-c', 'redraw']
+      # -c redraw => avoids that pesky "Press Enter to continue"
+      proc = subprocess.Popen(args, executable=self.CMD)
+      # let it inherit from parent
+      if self.waitFor:
+         proc.wait()
+         if proc.returncode != 0:
+            raise subprocess.CalledProcessError(proc.returncode)
+
+class GViewInvocation(ViewInvocation):
+   ''' gview FILENAME -c PATTERN - launch it and leave it '''
+   def __init__(self):
+      self.CMD='gview'
+      self.waitFor=False
+
+if __name__ == '__main__':
+   helpRequested = False
+   if len(sys.argv) > 1 and sys.argv[1]=='--help':
+      helpRequested = True
+   if len(sys.argv) != 2 or helpRequested:
+      print "Usage: %s TermToSearchFor"%sys.argv[0]
+      if helpRequested: sys.exit(0)
+      sys.exit(1)
+
+   config = ConfigParser.SafeConfigParser({
+      KEY_TAGS : os.path.expanduser('~/Work/Mainline/tags'),
+      KEY_INVOCATION : 'ViewInvocation',
+      })
+   if os.path.exists(CONFIG):
+      config.read(CONFIG)
+
+   needle = sys.argv[1]
+
+   invokeme = config.get(CONFIG_SECTION, KEY_INVOCATION)
+   if invokeme in globals().keys():
+      invocation = globals()[invokeme]()
+   else:
+      # Not in scope, so we'll try the equivalent of:
+      #     import CLASS
+      #     invocation = CLASS()
+      module = __import__(invokeme)
+      try:
+         invclass = getattr(module,invokeme) # raises if not found
+      except AttributeError,e:
+         raise AttributeError,('Source module %s does not contain a class %s? (from %s)'%(invokeme,invokeme, e.args))
+      try:
+         invocation = invclass()
+      except TypeError,t:
+         raise TypeError,('Class %s does not have a no-parameter constructor? (from %s)'%(invokeme, t.args))
+
+   tag = TagsSearcherFactory().get(config.get(CONFIG_SECTION, KEY_TAGS)).find(needle)
+   if tag is None:
+      print "Not found, sorry"
+      sys.exit(1)
+   else:
+      invocation.invoke(tag.filename, tag.pattern)
+