Index: trac/util.py
===================================================================
--- trac/util.py	(revision 2905)
+++ trac/util.py	(working copy)
@@ -603,3 +603,110 @@
         rearranged += itoa64[v & 0x3f]; v >>= 6
 
     return magic + salt + '$' + rearranged
+
+
+class Ranges(object):
+    """
+    Holds information about ranges parsed from a string
+    
+    >>> x = Ranges("1,2,9-15")
+    >>> 1 in x
+    True
+    >>> 5 in x
+    False
+    >>> 10 in x
+    True
+    >>> 16 in x
+    False
+    >>> [i for i in range(20) if i in x]
+    [1, 2, 9, 10, 11, 12, 13, 14, 15]
+    
+    Also supports iteration, which makes that last example a bit simpler:
+    
+    >>> list(x)
+    [1, 2, 9, 10, 11, 12, 13, 14, 15]
+    
+    Note that it automatically reduces the list and short-circuits when the
+    desired ranges are a relatively small portion of the entire set:
+    
+    >>> x = Ranges("99")
+    >>> 1 in x #really fast
+    False
+    >>> x = Ranges("1, 2, 1-2, 2") #reduces this to 1-2
+    >>> x.pairs
+    [(1, 2)]
+    >>> x = Ranges("1-9,2-4") #handle ranges that completely overlap
+    >>> list(x)
+    [1, 2, 3, 4, 5, 6, 7, 8, 9]
+    
+    Empty ranges are ok, and ranges can be constructed in pieces, if you
+    so choose:
+    
+    >>> x = Ranges()
+    >>> x.appendrange("1, 2, 3")
+    >>> x.appendrange("5-9")
+    >>> x.appendrange("2-3") #reduce'd away
+    >>> list(x)
+    [1, 2, 3, 5, 6, 7, 8, 9]
+    
+    """
+    def __init__(self, r=None):
+        self.pairs = []
+        self.appendrange(r)
+
+    def appendrange(self, r):
+        """
+        Add a range (from a string or None) to the current one
+        """
+        if not r: return
+        p = self.pairs
+        for x in r.split(","):
+            try:
+                a, b = map(int, x.split("-", 1))
+            except ValueError:
+                a, b = int(x), int(x)
+            p.append((a, b))
+        self._reduce()
+
+    def _reduce(self):
+        """
+        Come up with the minimal representation of the ranges
+        """
+        d = [] #list of indices to delete
+        p = self.pairs
+        p.sort()
+        for i in range(len(p) - 1):
+            if p[i+1][0] <= p[i][1]: #this item overlaps with the next
+	        #make the first one include the second
+                p[i] = (p[i][0], max(p[i][1], p[i+1][1]))
+	        d.append(i+1) #delete the second on a later pass
+        d.reverse()
+        for i in d:
+            del p[i]
+        self.a = p[0][0] #min value
+        self.b = p[-1][1] #max value
+
+    def __iter__(self):
+        """
+        This is another way I came up with to do it.  Is it faster?
+        
+        from itertools import chain
+        return chain(*[xrange(a, b+1) for a, b in self.pairs])
+        """
+        for a, b in self.pairs:
+            for i in range(a, b+1):
+                yield i
+
+    def __contains__(self, x):
+        if self.a <= x <= self.b: #short-circuit if outside the possible range
+            for a, b in self.pairs:
+                if a <= x <= b:
+                    return True
+                if b > x: #short-circuit if we've gone too far
+                    break
+        return False
+
+if __name__ == "__main__":
+    from doctest import testmod
+    testmod()
+
Index: trac/mimeview/api.py
===================================================================
--- trac/mimeview/api.py	(revision 2905)
+++ trac/mimeview/api.py	(working copy)
@@ -24,7 +24,7 @@
     from StringIO import StringIO
 
 from trac.core import *
-from trac.util import escape, to_utf8, Markup
+from trac.util import escape, to_utf8, Markup, Ranges
 
 
 __all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview']
@@ -244,7 +244,8 @@
                 elif isinstance(result, (str, unicode)):
                     return Markup(result)
                 elif annotations:
-                    return Markup(self._annotate(result, annotations))
+                    m = req.args.get('mark')
+                    return Markup(self._annotate(result, annotations, m and Ranges(m)))
                 else:
                     buf = StringIO()
                     buf.write('<div class="code"><pre>')
@@ -256,7 +257,7 @@
                 self.log.warning('HTML preview using %s failed (%s)'
                                  % (renderer, e), exc_info=True)
 
-    def _annotate(self, lines, annotations):
+    def _annotate(self, lines, annotations, marks=None):
         buf = StringIO()
         buf.write('<table class="code"><thead><tr>')
         annotators = []
@@ -283,7 +284,10 @@
             for annotator in annotators:
                 cells.append(annotator.annotate_line(num + 1, line))
             cells.append('<td>%s</td>\n' % space_re.sub(htmlify, line))
-            buf.write('<tr>' + '\n'.join(cells) + '</tr>')
+            if marks and num+1 in marks:
+                buf.write('<tr class="%s">' % ('hilite',) + '\n'.join(cells) + '</tr>')
+            else:
+                buf.write('<tr>' + '\n'.join(cells) + '</tr>')
         else:
             if num < 0:
                 return ''
Index: htdocs/css/code.css
===================================================================
--- htdocs/css/code.css	(revision 2905)
+++ htdocs/css/code.css	(working copy)
@@ -55,7 +55,12 @@
  padding: 1px 2px;
  vertical-align: top;
 }
-
+table.code tbody tr.hilite th {
+ background: #ccf;
+}
+table.code tbody tr.hilite td {
+ background: #ddf;
+}
 .image-file { background: #eee; padding: .3em }
 .image-file img { background: url(../imggrid.png) }

