# HG changeset patch # User Daniel Dourvaris # Date 2016-10-12 15:55:18 # Node ID e7837355f12b7a2c784b76c03d754e1dfce06377 # Parent 6ce2682d6da25d69babad405479641951e3f5d05 annotations: replace annotated source code viewer with renderer to allow soft wrapping of lines and viewing of annotation message diff --git a/rhodecode/controllers/files.py b/rhodecode/controllers/files.py --- a/rhodecode/controllers/files.py +++ b/rhodecode/controllers/files.py @@ -36,6 +36,8 @@ from webob.exc import HTTPNotFound, HTTP from rhodecode.controllers.utils import parse_path_ref from rhodecode.lib import diffs, helpers as h, caches from rhodecode.lib.compat import OrderedDict +from rhodecode.lib.codeblocks import ( + filenode_as_lines_tokens, filenode_as_annotated_lines_tokens) from rhodecode.lib.utils import jsonify, action_logger from rhodecode.lib.utils2 import ( convert_line_endings, detect_mode, safe_str, str2bool) @@ -221,9 +223,15 @@ class FilesController(BaseRepoController c.file_author = True c.file_tree = '' if c.file.is_file(): - c.renderer = ( - c.renderer and h.renderer_from_filename(c.file.path)) c.file_last_commit = c.file.last_commit + if c.annotate: # annotation has precedence over renderer + c.annotated_lines = filenode_as_annotated_lines_tokens( + c.file) + else: + c.renderer = ( + c.renderer and h.renderer_from_filename(c.file.path)) + if not c.renderer: + c.lines = filenode_as_lines_tokens(c.file) c.on_branch_head = self._is_valid_head( commit_id, c.rhodecode_repo) diff --git a/rhodecode/lib/annotate.py b/rhodecode/lib/annotate.py deleted file mode 100644 --- a/rhodecode/lib/annotate.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2011-2016 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - - -""" -Anontation library for usage in rhodecode, previously part of vcs -""" - -import StringIO - -from pygments import highlight -from pygments.formatters import HtmlFormatter - -from rhodecode.lib.vcs.exceptions import VCSError -from rhodecode.lib.vcs.nodes import FileNode - - -def annotate_highlight( - filenode, annotate_from_commit_func=None, - order=None, headers=None, **options): - """ - Returns html portion containing annotated table with 3 columns: line - numbers, commit information and pygmentized line of code. - - :param filenode: FileNode object - :param annotate_from_commit_func: function taking commit and - returning single annotate cell; needs break line at the end - :param order: ordered sequence of ``ls`` (line numbers column), - ``annotate`` (annotate column), ``code`` (code column); Default is - ``['ls', 'annotate', 'code']`` - :param headers: dictionary with headers (keys are whats in ``order`` - parameter) - """ - from rhodecode.lib.helpers import get_lexer_for_filenode - options['linenos'] = True - formatter = AnnotateHtmlFormatter( - filenode=filenode, order=order, headers=headers, - annotate_from_commit_func=annotate_from_commit_func, **options) - lexer = get_lexer_for_filenode(filenode) - highlighted = highlight(filenode.content, lexer, formatter) - return highlighted - - -class AnnotateHtmlFormatter(HtmlFormatter): - - def __init__( - self, filenode, annotate_from_commit_func=None, - order=None, **options): - """ - If ``annotate_from_commit_func`` is passed, it should be a function - which returns string from the given commit. For example, we may pass - following function as ``annotate_from_commit_func``:: - - def commit_to_anchor(commit): - return '%s\n' %\ - (commit.id, commit.id) - - :param annotate_from_commit_func: see above - :param order: (default: ``['ls', 'annotate', 'code']``); order of - columns; - :param options: standard pygment's HtmlFormatter options, there is - extra option tough, ``headers``. For instance we can pass:: - - formatter = AnnotateHtmlFormatter(filenode, headers={ - 'ls': '#', - 'annotate': 'Annotate', - 'code': 'Code', - }) - - """ - super(AnnotateHtmlFormatter, self).__init__(**options) - self.annotate_from_commit_func = annotate_from_commit_func - self.order = order or ('ls', 'annotate', 'code') - headers = options.pop('headers', None) - if headers and not ( - 'ls' in headers and 'annotate' in headers and 'code' in headers): - raise ValueError( - "If headers option dict is specified it must " - "all 'ls', 'annotate' and 'code' keys") - self.headers = headers - if isinstance(filenode, FileNode): - self.filenode = filenode - else: - raise VCSError( - "This formatter expect FileNode parameter, not %r" % - type(filenode)) - - def annotate_from_commit(self, commit): - """ - Returns full html line for single commit per annotated line. - """ - if self.annotate_from_commit_func: - return self.annotate_from_commit_func(commit) - else: - return commit.id + '\n' - - def _wrap_tablelinenos(self, inner): - dummyoutfile = StringIO.StringIO() - lncount = 0 - for t, line in inner: - if t: - lncount += 1 - dummyoutfile.write(line) - - fl = self.linenostart - mw = len(str(lncount + fl - 1)) - sp = self.linenospecial - st = self.linenostep - la = self.lineanchors - aln = self.anchorlinenos - if sp: - lines = [] - - for i in range(fl, fl + lncount): - if i % st == 0: - if i % sp == 0: - if aln: - lines.append('' - '%*d' % - (la, i, mw, i)) - else: - lines.append('' - '%*d' % (mw, i)) - else: - if aln: - lines.append('' - '%*d' % (la, i, mw, i)) - else: - lines.append('%*d' % (mw, i)) - else: - lines.append('') - ls = '\n'.join(lines) - else: - lines = [] - for i in range(fl, fl + lncount): - if i % st == 0: - if aln: - lines.append('%*d' \ - % (la, i, mw, i)) - else: - lines.append('%*d' % (mw, i)) - else: - lines.append('') - ls = '\n'.join(lines) - - cached = {} - annotate = [] - for el in self.filenode.annotate: - commit_id = el[1] - if commit_id in cached: - result = cached[commit_id] - else: - commit = el[2]() - result = self.annotate_from_commit(commit) - cached[commit_id] = result - annotate.append(result) - - annotate = ''.join(annotate) - - # in case you wonder about the seemingly redundant
here: - # since the content in the other cell also is wrapped in a div, - # some browsers in some configurations seem to mess up the formatting. - ''' - yield 0, ('' % self.cssclass + - '' + - '
' +
-                  ls + '
') - yield 0, dummyoutfile.getvalue() - yield 0, '
' - - ''' - headers_row = [] - if self.headers: - headers_row = [''] - for key in self.order: - td = ''.join(('', self.headers[key], '')) - headers_row.append(td) - headers_row.append('') - - body_row_start = [''] - for key in self.order: - if key == 'ls': - body_row_start.append( - '
' +
-                    ls + '
') - elif key == 'annotate': - body_row_start.append( - '
' +
-                    annotate + '
') - elif key == 'code': - body_row_start.append('') - yield 0, ('' % self.cssclass + - ''.join(headers_row) + - ''.join(body_row_start) - ) - yield 0, dummyoutfile.getvalue() - yield 0, '
' diff --git a/rhodecode/lib/codeblocks.py b/rhodecode/lib/codeblocks.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/codeblocks.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +from itertools import groupby + +from pygments import lex +# PYGMENTS_TOKEN_TYPES is used in a hot loop keep attribute lookups to a minimum +from pygments.token import STANDARD_TYPES as PYGMENTS_TOKEN_TYPES + +from rhodecode.lib.helpers import get_lexer_for_filenode + +def tokenize_file(content, lexer): + """ + Use pygments to tokenize some content based on a lexer + ensuring all original new lines and whitespace is preserved + """ + + lexer.stripall = False + lexer.stripnl = False + lexer.ensurenl = False + return lex(content, lexer) + + +def pygment_token_class(token_type): + """ Convert a pygments token type to html class name """ + + fname = PYGMENTS_TOKEN_TYPES.get(token_type) + if fname: + return fname + + aname = '' + while fname is None: + aname = '-' + token_type[-1] + aname + token_type = token_type.parent + fname = PYGMENTS_TOKEN_TYPES.get(token_type) + + return fname + aname + + +def tokens_as_lines(tokens, split_string=u'\n'): + """ + Take a list of (TokenType, text) tuples and split them by a string + + eg. [(TEXT, 'some\ntext')] => [(TEXT, 'some'), (TEXT, 'text')] + """ + + buffer = [] + for token_type, token_text in tokens: + parts = token_text.split(split_string) + for part in parts[:-1]: + buffer.append((token_type, part)) + yield buffer + buffer = [] + + buffer.append((token_type, parts[-1])) + + if buffer: + yield buffer + + +def filenode_as_lines_tokens(filenode): + """ + Return a generator of lines with pygment tokens for a filenode eg: + + [ + (1, line1_tokens_list), + (2, line1_tokens_list]), + ] + """ + + return enumerate( + tokens_as_lines( + tokenize_file( + filenode.content, get_lexer_for_filenode(filenode) + ) + ), + 1) + + +def filenode_as_annotated_lines_tokens(filenode): + """ + Take a file node and return a list of annotations => lines, if no annotation + is found, it will be None. + + eg: + + [ + (annotation1, [ + (1, line1_tokens_list), + (2, line2_tokens_list), + ]), + (annotation2, [ + (3, line1_tokens_list), + ]), + (None, [ + (4, line1_tokens_list), + ]), + (annotation1, [ + (5, line1_tokens_list), + (6, line2_tokens_list), + ]) + ] + """ + + + # cache commit_getter lookups + commit_cache = {} + def _get_annotation(commit_id, commit_getter): + if commit_id not in commit_cache: + commit_cache[commit_id] = commit_getter() + return commit_cache[commit_id] + + annotation_lookup = { + line_no: _get_annotation(commit_id, commit_getter) + for line_no, commit_id, commit_getter, line_content + in filenode.annotate + } + + annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens) + for line_no, tokens + in filenode_as_lines_tokens(filenode)) + + grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0]) + + for annotation, group in grouped_annotations_lines: + yield ( + annotation, [(line_no, tokens) + for (_, line_no, tokens) in group] + ) diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -67,7 +67,6 @@ from webhelpers.html.tags import _set_in convert_boolean_attrs, NotGiven, _make_safe_id_component from webhelpers2.number import format_byte_size -from rhodecode.lib.annotate import annotate_highlight from rhodecode.lib.action_parser import action_parser from rhodecode.lib.ext_json import json from rhodecode.lib.utils import repo_name_slug, get_custom_lexer @@ -125,17 +124,18 @@ def asset(path, ver=None): 'rhodecode:public/{}'.format(path), _query=query) -def html_escape(text, html_escape_table=None): +default_html_escape_table = { + ord('&'): u'&', + ord('<'): u'<', + ord('>'): u'>', + ord('"'): u'"', + ord("'"): u''', +} + + +def html_escape(text, html_escape_table=default_html_escape_table): """Produce entities within text.""" - if not html_escape_table: - html_escape_table = { - "&": "&", - '"': """, - "'": "'", - ">": ">", - "<": "<", - } - return "".join(html_escape_table.get(c, c) for c in text) + return text.translate(html_escape_table) def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None): @@ -500,6 +500,86 @@ def get_matching_line_offsets(lines, ter return matching_lines +def hsv_to_rgb(h, s, v): + """ Convert hsv color values to rgb """ + + if s == 0.0: + return v, v, v + i = int(h * 6.0) # XXX assume int() truncates! + f = (h * 6.0) - i + p = v * (1.0 - s) + q = v * (1.0 - s * f) + t = v * (1.0 - s * (1.0 - f)) + i = i % 6 + if i == 0: + return v, t, p + if i == 1: + return q, v, p + if i == 2: + return p, v, t + if i == 3: + return p, q, v + if i == 4: + return t, p, v + if i == 5: + return v, p, q + + +def unique_color_generator(n=10000, saturation=0.10, lightness=0.95): + """ + Generator for getting n of evenly distributed colors using + hsv color and golden ratio. It always return same order of colors + + :param n: number of colors to generate + :param saturation: saturation of returned colors + :param lightness: lightness of returned colors + :returns: RGB tuple + """ + + golden_ratio = 0.618033988749895 + h = 0.22717784590367374 + + for _ in xrange(n): + h += golden_ratio + h %= 1 + HSV_tuple = [h, saturation, lightness] + RGB_tuple = hsv_to_rgb(*HSV_tuple) + yield map(lambda x: str(int(x * 256)), RGB_tuple) + + +def color_hasher(n=10000, saturation=0.10, lightness=0.95): + """ + Returns a function which when called with an argument returns a unique + color for that argument, eg. + + :param n: number of colors to generate + :param saturation: saturation of returned colors + :param lightness: lightness of returned colors + :returns: css RGB string + + >>> color_hash = color_hasher() + >>> color_hash('hello') + 'rgb(34, 12, 59)' + >>> color_hash('hello') + 'rgb(34, 12, 59)' + >>> color_hash('other') + 'rgb(90, 224, 159)' + """ + + color_dict = {} + cgenerator = unique_color_generator( + saturation=saturation, lightness=lightness) + + def get_color_string(thing): + if thing in color_dict: + col = color_dict[thing] + else: + col = color_dict[thing] = cgenerator.next() + return "rgb(%s)" % (', '.join(col)) + + return get_color_string + + def get_lexer_safe(mimetype=None, filepath=None): """ Tries to return a relevant pygments lexer using mimetype/filepath name, @@ -536,92 +616,6 @@ def pygmentize(filenode, **kwargs): CodeHtmlFormatter(**kwargs))) -def pygmentize_annotation(repo_name, filenode, **kwargs): - """ - pygmentize function for annotation - - :param filenode: - """ - - color_dict = {} - - def gen_color(n=10000): - """generator for getting n of evenly distributed colors using - hsv color and golden ratio. It always return same order of colors - - :returns: RGB tuple - """ - - def hsv_to_rgb(h, s, v): - if s == 0.0: - return v, v, v - i = int(h * 6.0) # XXX assume int() truncates! - f = (h * 6.0) - i - p = v * (1.0 - s) - q = v * (1.0 - s * f) - t = v * (1.0 - s * (1.0 - f)) - i = i % 6 - if i == 0: - return v, t, p - if i == 1: - return q, v, p - if i == 2: - return p, v, t - if i == 3: - return p, q, v - if i == 4: - return t, p, v - if i == 5: - return v, p, q - - golden_ratio = 0.618033988749895 - h = 0.22717784590367374 - - for _ in xrange(n): - h += golden_ratio - h %= 1 - HSV_tuple = [h, 0.95, 0.95] - RGB_tuple = hsv_to_rgb(*HSV_tuple) - yield map(lambda x: str(int(x * 256)), RGB_tuple) - - cgenerator = gen_color() - - def get_color_string(commit_id): - if commit_id in color_dict: - col = color_dict[commit_id] - else: - col = color_dict[commit_id] = cgenerator.next() - return "color: rgb(%s)! important;" % (', '.join(col)) - - def url_func(repo_name): - - def _url_func(commit): - author = commit.author - date = commit.date - message = tooltip(commit.message) - - tooltip_html = ("
Author:" - " %s
Date: %s
Message:" - " %s
") - - tooltip_html = tooltip_html % (author, date, message) - lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id) - uri = link_to( - lnk_format, - url('changeset_home', repo_name=repo_name, - revision=commit.raw_id), - style=get_color_string(commit.raw_id), - class_='tooltip', - title=tooltip_html - ) - - uri += '\n' - return uri - return _url_func - - return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs)) - - def is_following_repo(repo_name, user_id): from rhodecode.model.scm import ScmModel return ScmModel().is_following_repo(repo_name, user_id) diff --git a/rhodecode/public/css/bootstrap-variables.less b/rhodecode/public/css/bootstrap-variables.less --- a/rhodecode/public/css/bootstrap-variables.less +++ b/rhodecode/public/css/bootstrap-variables.less @@ -45,7 +45,8 @@ @font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; @font-family-serif: Georgia, "Times New Roman", Times, serif; //** Default monospace fonts for ``, ``, and `
`.
-@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
+@font-family-monospace:   Consolas, "Liberation Mono", Menlo, Monaco, Courier, monospace;
+
 @font-family-base:        @font-family-sans-serif;
 
 @font-size-base:          14px;
diff --git a/rhodecode/public/css/code-block.less b/rhodecode/public/css/code-block.less
--- a/rhodecode/public/css/code-block.less
+++ b/rhodecode/public/css/code-block.less
@@ -506,20 +506,19 @@ div.codeblock {
 
         th,
         td {
-            padding: .5em !important;
-            border: @border-thickness solid @border-default-color !important;
+            padding: .5em;
+            border: @border-thickness solid @border-default-color;
         }
     }
 
     table {
-        width: 0 !important;
-        border: 0px !important;
+        border: 0px;
         margin: 0;
         letter-spacing: normal;
-    
-    
+
+
         td {
-            border: 0px !important;
+            border: 0px;
             vertical-align: top;
         }
     }
@@ -569,7 +568,7 @@ div.annotatediv { margin-left: 2px; marg
 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
 
 .code { display: block; border:0px !important; }
-.code-highlight,
+.code-highlight, /* TODO: dan: merge codehilite into code-highlight */
 .codehilite {
     .hll { background-color: #ffffcc }
     .c { color: #408080; font-style: italic } /* Comment */
@@ -641,3 +640,110 @@ pre.literal-block, .codehilite pre{
     .border-radius(@border-radius);
     background-color: @grey7;
 }
+
+
+/* START NEW CODE BLOCK CSS */
+
+table.cb {
+    width: 100%;
+    border-collapse: collapse;
+    margin-bottom: 10px;
+
+    * {
+        box-sizing: border-box;
+    }
+
+    /* intentionally general selector since .cb-line-selected must override it
+       and they both use !important since the td itself may have a random color
+       generated by annotation blocks. TLDR: if you change it, make sure
+       annotated block selection and line selection in file view still work */
+    .cb-line-fresh .cb-content {
+        background: white !important;
+    }
+
+    tr.cb-annotate {
+        border-top: 1px solid #eee;
+
+        &+ .cb-line {
+            border-top: 1px solid #eee;
+        }
+
+        &:first-child {
+            border-top: none;
+            &+ .cb-line {
+                border-top: none;
+            }
+        }
+    }
+
+    td {
+        vertical-align: top;
+        padding: 2px 10px;
+
+        &.cb-content {
+            white-space: pre-wrap;
+            font-family: @font-family-monospace;
+            font-size: 12.35px;
+
+            span {
+                word-break: break-word;
+            }
+        }
+
+        &.cb-lineno {
+            padding: 0;
+            height: 1px; /* this allows the  link to fill to 100% height of the td */
+            width: 50px;
+            color: rgba(0, 0, 0, 0.3);
+            text-align: right;
+            border-right: 1px solid #eee;
+            font-family: @font-family-monospace;
+
+            a::before {
+                content: attr(data-line-no);
+            }
+            &.cb-line-selected {
+                background: @comment-highlight-color !important;
+            }
+
+            a {
+                display: block;
+                height: 100%;
+                color: rgba(0, 0, 0, 0.3);
+                padding: 0 10px; /* vertical padding is 0 so that height: 100% works */
+                line-height: 18px; /* use this instead of vertical padding */
+            }
+        }
+
+        &.cb-content {
+            &.cb-line-selected {
+                background: @comment-highlight-color !important;
+            }
+        }
+
+        &.cb-annotate-info {
+            width: 320px;
+            min-width: 320px;
+            max-width: 320px;
+            padding: 5px 2px;
+            font-size: 13px;
+
+            strong.cb-annotate-message {
+                padding: 5px 0;
+                white-space: pre-line;
+                display: inline-block;
+            }
+            .rc-user {
+                float: none;
+                padding: 0 6px 0 17px;
+                min-width: auto;
+                min-height: auto;
+            }
+        }
+
+        &.cb-annotate-revision {
+            cursor: pointer;
+            text-align: right;
+        }
+    }
+}
diff --git a/rhodecode/public/js/src/rhodecode.js b/rhodecode/public/js/src/rhodecode.js
--- a/rhodecode/public/js/src/rhodecode.js
+++ b/rhodecode/public/js/src/rhodecode.js
@@ -266,7 +266,35 @@ function offsetScroll(element, offset){
             }
         });
 
-    $('.compare_view_files').on(
+    $('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
+                    when new diffs are integrated */
+        'click', '.cb-lineno a', function(event) {
+
+            if ($(this).attr('data-line-no') !== ""){
+                $('.cb-line-selected').removeClass('cb-line-selected');
+                var td = $(this).parent();
+                td.addClass('cb-line-selected'); // line number td
+                td.next().addClass('cb-line-selected'); // line content td
+
+                // Replace URL without jumping to it if browser supports.
+                // Default otherwise
+                if (history.pushState) {
+                    var new_location = location.href.rstrip('#');
+                    if (location.hash) {
+                        new_location = new_location.replace(location.hash, "");
+                    }
+
+                    // Make new anchor url
+                    new_location = new_location + $(this).attr('href');
+                    history.pushState(true, document.title, new_location);
+
+                    return false;
+                }
+            }
+        });
+
+    $('.compare_view_files').on( /* TODO: replace this with .cb function above
+                                    when new diffs are integrated */
         'click', 'tr.line .lineno a',function(event) {
             if ($(this).text() != ""){
                 $('tr.line').removeClass('selected');
@@ -365,10 +393,11 @@ function offsetScroll(element, offset){
     // Select the line that comes from the url anchor
     // At the time of development, Chrome didn't seem to support jquery's :target
     // element, so I had to scroll manually
-    if (location.hash) {
+
+    if (location.hash) { /* TODO: dan: remove this and replace with code block
+                            below when new diffs are ready */
         var result = splitDelimitedHash(location.hash);
         var loc  = result.loc;
-        var remainder = result.remainder;
         if (loc.length > 1){
             var lineno = $(loc+'.lineno');
             if (lineno.length > 0){
@@ -378,11 +407,70 @@ function offsetScroll(element, offset){
                 tr[0].scrollIntoView();
 
                 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
-                    tr:tr,
-                    remainder:remainder});
+                    tr: tr,
+                    remainder: result.remainder});
             }
         }
     }
 
+    if (location.hash) { /* TODO: dan: use this to replace the code block above
+                            when new diffs are ready */
+        var result = splitDelimitedHash(location.hash);
+        var loc  = result.loc;
+        if (loc.length > 1) {
+            var page_highlights = loc.substring(
+                loc.indexOf('#') + 1).split('L');
+
+            if (page_highlights.length > 1) {
+                var highlight_ranges = page_highlights[1].split(",");
+                var h_lines = [];
+                for (var pos in highlight_ranges) {
+                    var _range = highlight_ranges[pos].split('-');
+                    if (_range.length === 2) {
+                        var start = parseInt(_range[0]);
+                        var end = parseInt(_range[1]);
+                        if (start < end) {
+                            for (var i = start; i <= end; i++) {
+                                h_lines.push(i);
+                            }
+                        }
+                    }
+                    else {
+                        h_lines.push(parseInt(highlight_ranges[pos]));
+                    }
+                }
+                for (pos in h_lines) {
+                    var line_td = $('td.cb-lineno#L' + h_lines[pos]);
+                    if (line_td.length) {
+                        line_td.addClass('cb-line-selected'); // line number td
+                        line_td.next().addClass('cb-line-selected'); // line content
+                    }
+                }
+                var first_line_td = $('td.cb-lineno#L' + h_lines[0]);
+                if (first_line_td.length) {
+                    var elOffset = first_line_td.offset().top;
+                    var elHeight = first_line_td.height();
+                    var windowHeight = $(window).height();
+                    var offset;
+
+                    if (elHeight < windowHeight) {
+                        offset = elOffset - ((windowHeight / 4) - (elHeight / 2));
+                    }
+                    else {
+                        offset = elOffset;
+                    }
+                    $(function() { // let browser scroll to hash first, then
+                                   // scroll the line to the middle of page
+                        setTimeout(function() {
+                            $('html, body').animate({ scrollTop: offset });
+                          }, 100);
+                    });
+                    $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
+                        lineno: first_line_td,
+                        remainder: result.remainder});
+                }
+            }
+        }
+    }
     collapsableContent();
 });
diff --git a/rhodecode/templates/codeblocks/source.html b/rhodecode/templates/codeblocks/source.html
new file mode 100644
--- /dev/null
+++ b/rhodecode/templates/codeblocks/source.html
@@ -0,0 +1,70 @@
+<%def name="render_line(line_num, tokens,
+                        annotation=None,
+                        bgcolor=None)">
+    <%
+    # avoid module lookups for performance
+    from rhodecode.lib.codeblocks import pygment_token_class
+    from rhodecode.lib.helpers import html_escape
+    %>
+    
+    
+      
+    
+    ${
+      ''.join(
+       '%s' %
+        (pygment_token_class(token_type), html_escape(token_text))
+        for token_type, token_text in tokens) + '\n' | n
+    }
+    ## this ugly list comp is necessary for performance
+  
+
+
+<%def name="render_annotation_lines(annotation, lines, color_hasher)">
+  <%
+  rowspan = len(lines) + 1 # span the line's  and annotation 
+  %>
+  %if not annotation:
+  
+    
+    
+  
+  %else:
+  
+    
+      ${h.gravatar_with_user(annotation.author, 16) | n}
+      ${
+          h.truncate(annotation.message, len(lines) * 30)
+      }
+    
+    
+    
+      r${annotation.revision}
+    
+    
+  
+  %endif
+
+  %for line_num, tokens in lines:
+    ${render_line(line_num, tokens,
+      bgcolor=color_hasher(annotation and annotation.raw_id or ''),
+      annotation=annotation,
+      )}
+  %endfor
+
diff --git a/rhodecode/templates/files/files.html b/rhodecode/templates/files/files.html
--- a/rhodecode/templates/files/files.html
+++ b/rhodecode/templates/files/files.html
@@ -147,43 +147,6 @@
             if (source_page) {
                 // variants for with source code, not tree view
 
-                if (location.href.indexOf('#') != -1) {
-                    page_highlights = location.href.substring(location.href.indexOf('#') + 1).split('L');
-                    if (page_highlights.length == 2) {
-                        highlight_ranges = page_highlights[1].split(",");
-
-                        var h_lines = [];
-                        for (pos in highlight_ranges) {
-                            var _range = highlight_ranges[pos].split('-');
-                            if (_range.length == 2) {
-                                var start = parseInt(_range[0]);
-                                var end = parseInt(_range[1]);
-                                if (start < end) {
-                                    for (var i = start; i <= end; i++) {
-                                        h_lines.push(i);
-                                    }
-                                }
-                            }
-                            else {
-                                h_lines.push(parseInt(highlight_ranges[pos]));
-                            }
-                        }
-
-                        for (pos in h_lines) {
-                            // @comment-highlight-color
-                            $('#L' + h_lines[pos]).css('background-color', '#ffd887');
-                        }
-
-                        var _first_line = $('#L' + h_lines[0]).get(0);
-                        if (_first_line) {
-                            var line = $('#L' + h_lines[0]);
-                            if (line.length > 0){
-                                offsetScroll(line, 70);
-                            }
-                        }
-                    }
-                }
-
                 // select code link event
                 $("#hlcode").mouseup(getSelectionLink);
 
diff --git a/rhodecode/templates/files/files_source.html b/rhodecode/templates/files/files_source.html
--- a/rhodecode/templates/files/files_source.html
+++ b/rhodecode/templates/files/files_source.html
@@ -1,3 +1,4 @@
+<%namespace name="sourceblock" file="/codeblocks/source.html"/>
 
 
@@ -51,12 +52,22 @@
%else: % if c.file.size < c.cut_off_limit: - %if c.annotate: - ${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")} - %elif c.renderer: + %if c.renderer and not c.annotate: ${h.render(c.file.content, renderer=c.renderer)} %else: - ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")} + + %if c.annotate: + <% color_hasher = h.color_hasher() %> + %for annotation, lines in c.annotated_lines: + ${sourceblock.render_annotation_lines(annotation, lines, color_hasher)} + %endfor + %else: + %for line_num, tokens in c.lines: + ${sourceblock.render_line(line_num, tokens)} + %endfor + %endif +
+
%endif %else: ${_('File is too big to display')} ${h.link_to(_('Show as raw'), diff --git a/rhodecode/tests/functional/test_files.py b/rhodecode/tests/functional/test_files.py --- a/rhodecode/tests/functional/test_files.py +++ b/rhodecode/tests/functional/test_files.py @@ -81,7 +81,7 @@ def _commit_change( return commit - + @pytest.mark.usefixtures("app") class TestFilesController: @@ -270,9 +270,9 @@ class TestFilesController: annotate=True)) expected_revisions = { - 'hg': 'r356:25213a5fbb04', - 'git': 'r345:c994f0de03b2', - 'svn': 'r208:209', + 'hg': 'r356', + 'git': 'r345', + 'svn': 'r208', } response.mustcontain(expected_revisions[backend.alias])