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 <http://www.gnu.org/licenses/>. -# -# 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 '<a href="/commits/%s/">%s</a>\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('<a href="#%s-%d" class="special">' - '%*d</a>' % - (la, i, mw, i)) - else: - lines.append('<span class="special">' - '%*d</span>' % (mw, i)) - else: - if aln: - lines.append('<a href="#%s-%d">' - '%*d</a>' % (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('<a href="#%s-%d">%*d</a>' \ - % (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 <div> 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, ('<table class="%stable">' % self.cssclass + - '<tr><td class="linenos"><div class="linenodiv"><pre>' + - ls + '</pre></div></td>' + - '<td class="code">') - yield 0, dummyoutfile.getvalue() - yield 0, '</td></tr></table>' - - ''' - headers_row = [] - if self.headers: - headers_row = ['<tr class="annotate-header">'] - for key in self.order: - td = ''.join(('<td>', self.headers[key], '</td>')) - headers_row.append(td) - headers_row.append('</tr>') - - body_row_start = ['<tr>'] - for key in self.order: - if key == 'ls': - body_row_start.append( - '<td class="linenos"><div class="linenodiv"><pre>' + - ls + '</pre></div></td>') - elif key == 'annotate': - body_row_start.append( - '<td class="annotate"><div class="annotatediv"><pre>' + - annotate + '</pre></div></td>') - elif key == 'code': - body_row_start.append('<td class="code">') - yield 0, ('<table class="%stable">' % self.cssclass + - ''.join(headers_row) + - ''.join(body_row_start) - ) - yield 0, dummyoutfile.getvalue() - yield 0, '</td></tr></table>' 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 <http://www.gnu.org/licenses/>. +# +# 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 = ("<div style='font-size:0.8em'><b>Author:</b>" - " %s<br/><b>Date:</b> %s</b><br/><b>Message:" - "</b> %s<br/></div>") - - 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 `<code>`, `<kbd>`, and `<pre>`. -@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 <a> 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 + %> + <tr class="cb-line cb-line-fresh" + %if annotation: + data-revision="${annotation.revision}" + %endif + > + <td class="cb-lineno" id="L${line_num}"> + <a data-line-no="${line_num}" href="#L${line_num}"></a> + </td> + <td class="cb-content cb-content-fresh" + %if bgcolor: + style="background: ${bgcolor}" + %endif + >${ + ''.join( + '<span class="%s">%s</span>' % + (pygment_token_class(token_type), html_escape(token_text)) + for token_type, token_text in tokens) + '\n' | n + }</td> + ## this ugly list comp is necessary for performance + </tr> +</%def> + +<%def name="render_annotation_lines(annotation, lines, color_hasher)"> + <% + rowspan = len(lines) + 1 # span the line's <tr> and annotation <tr> + %> + %if not annotation: + <tr class="cb-annotate"> + <td class="cb-annotate-message" rowspan="${rowspan}"></td> + <td class="cb-annotate-revision" rowspan="${rowspan}"></td> + </tr> + %else: + <tr class="cb-annotate"> + <td class="cb-annotate-info tooltip" + rowspan="${rowspan}" + title="Author: ${annotation.author | entity}<br>Date: ${annotation.date}<br>Message: ${annotation.message | entity}" + > + ${h.gravatar_with_user(annotation.author, 16) | n} + <strong class="cb-annotate-message">${ + h.truncate(annotation.message, len(lines) * 30) + }</strong> + </td> + <td + class="cb-annotate-revision" + rowspan="${rowspan}" + data-revision="${annotation.revision}" + onclick="$('[data-revision=${annotation.revision}]').toggleClass('cb-line-fresh')" + style="background: ${color_hasher(annotation.raw_id)}"> + <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=annotation.raw_id)}"> + r${annotation.revision} + </a> + </td> + </tr> + %endif + + %for line_num, tokens in lines: + ${render_line(line_num, tokens, + bgcolor=color_hasher(annotation and annotation.raw_id or ''), + annotation=annotation, + )} + %endfor +</%def> 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"/> <div id="codeblock" class="codeblock"> <div class="codeblock-header"> @@ -51,12 +52,22 @@ </div> %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")} + <table class="cb codehilite"> + %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 + </table> + </div> %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])