Index: trac/versioncontrol/web_ui/browser.py
===================================================================
--- trac/versioncontrol/web_ui/browser.py	(revision 2911)
+++ trac/versioncontrol/web_ui/browser.py	(working copy)
@@ -244,13 +244,14 @@
         match = IMG_RE.search(path)
         if formatter.flavor != 'oneliner' and match:
             return '<img src="%s" alt="%s" />' % \
-                   (formatter.href.file(path, format='raw'), label)
-        path, rev, line = get_path_rev_line(path)
+                   (formatter.href.browser(path, format='raw'), label)
+        path, rev, line, marks = get_path_rev_marks(path)
         if line is not None:
             anchor = '#L%d' % line
         else:
             anchor = ''
         label = urllib.unquote(label)
         return '<a class="source" href="%s%s">%s</a>' \
-               % (util.escape(formatter.href.browser(path, rev=rev)), anchor,
-                  label)
+               % (util.escape(formatter.href.browser(path, rev=rev,
+                                                     mark=marks)),
+                  anchor, label)
Index: trac/versioncontrol/web_ui/util.py
===================================================================
--- trac/versioncontrol/web_ui/util.py	(revision 2911)
+++ trac/versioncontrol/web_ui/util.py	(working copy)
@@ -23,7 +23,7 @@
                       TracError, Markup, rss_title
 from trac.wiki import wiki_to_html, wiki_to_oneliner
 
-__all__ = ['get_changes', 'get_path_links', 'get_path_rev_line',
+__all__ = ['get_changes', 'get_path_links', 'get_path_rev_marks',
            'get_existing_node']
 
 def get_changes(env, repos, revs, full=None, req=None, format=None):
@@ -71,19 +71,20 @@
         })
     return links
 
-rev_re = re.compile(r"([^@#]*)[@#]([^#]+)(?:#L(\d+))?")
+rev_re = re.compile(r"([^@#]*)[@#]([^#]+)(?:#L(?:\(((?:\d[,-]?)+)\))?(\d+))?")
 
-def get_path_rev_line(path):
+def get_path_rev_marks(path):
     rev = None
     line = None
+    marks = None
     match = rev_re.search(path)
     if match:
         path = match.group(1)
         rev = match.group(2)
-        if match.group(3):
-            line = int(match.group(3))
+        marks = match.group(3)
+        line = int(match.group(4))
     path = urllib.unquote(path)
-    return path, rev, line
+    return path, rev, line, marks
 
 def get_existing_node(env, repos, path, rev):
     try: 
Index: trac/versioncontrol/web_ui/log.py
===================================================================
--- trac/versioncontrol/web_ui/log.py	(revision 2911)
+++ trac/versioncontrol/web_ui/log.py	(working copy)
@@ -210,7 +210,7 @@
         yield ('log', self._format_link)
 
     def _format_link(self, formatter, ns, path, label):
-        path, rev, line = get_path_rev_line(path)
+        path, rev, line, marks = get_path_rev_marks(path)
         stop_rev = None
         if rev and ':' in rev:
             stop_rev, rev = rev.split(':', 1)
Index: trac/util.py
===================================================================
--- trac/util.py	(revision 2911)
+++ 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 2911)
+++ 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 2911)
+++ 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) }
 

