##// END OF EJS Templates
annotations: replace annotated source code viewer with renderer...
dan -
r986:e7837355 default
parent child Browse files
Show More
@@ -0,0 +1,147 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 from itertools import groupby
23
24 from pygments import lex
25 # PYGMENTS_TOKEN_TYPES is used in a hot loop keep attribute lookups to a minimum
26 from pygments.token import STANDARD_TYPES as PYGMENTS_TOKEN_TYPES
27
28 from rhodecode.lib.helpers import get_lexer_for_filenode
29
30 def tokenize_file(content, lexer):
31 """
32 Use pygments to tokenize some content based on a lexer
33 ensuring all original new lines and whitespace is preserved
34 """
35
36 lexer.stripall = False
37 lexer.stripnl = False
38 lexer.ensurenl = False
39 return lex(content, lexer)
40
41
42 def pygment_token_class(token_type):
43 """ Convert a pygments token type to html class name """
44
45 fname = PYGMENTS_TOKEN_TYPES.get(token_type)
46 if fname:
47 return fname
48
49 aname = ''
50 while fname is None:
51 aname = '-' + token_type[-1] + aname
52 token_type = token_type.parent
53 fname = PYGMENTS_TOKEN_TYPES.get(token_type)
54
55 return fname + aname
56
57
58 def tokens_as_lines(tokens, split_string=u'\n'):
59 """
60 Take a list of (TokenType, text) tuples and split them by a string
61
62 eg. [(TEXT, 'some\ntext')] => [(TEXT, 'some'), (TEXT, 'text')]
63 """
64
65 buffer = []
66 for token_type, token_text in tokens:
67 parts = token_text.split(split_string)
68 for part in parts[:-1]:
69 buffer.append((token_type, part))
70 yield buffer
71 buffer = []
72
73 buffer.append((token_type, parts[-1]))
74
75 if buffer:
76 yield buffer
77
78
79 def filenode_as_lines_tokens(filenode):
80 """
81 Return a generator of lines with pygment tokens for a filenode eg:
82
83 [
84 (1, line1_tokens_list),
85 (2, line1_tokens_list]),
86 ]
87 """
88
89 return enumerate(
90 tokens_as_lines(
91 tokenize_file(
92 filenode.content, get_lexer_for_filenode(filenode)
93 )
94 ),
95 1)
96
97
98 def filenode_as_annotated_lines_tokens(filenode):
99 """
100 Take a file node and return a list of annotations => lines, if no annotation
101 is found, it will be None.
102
103 eg:
104
105 [
106 (annotation1, [
107 (1, line1_tokens_list),
108 (2, line2_tokens_list),
109 ]),
110 (annotation2, [
111 (3, line1_tokens_list),
112 ]),
113 (None, [
114 (4, line1_tokens_list),
115 ]),
116 (annotation1, [
117 (5, line1_tokens_list),
118 (6, line2_tokens_list),
119 ])
120 ]
121 """
122
123
124 # cache commit_getter lookups
125 commit_cache = {}
126 def _get_annotation(commit_id, commit_getter):
127 if commit_id not in commit_cache:
128 commit_cache[commit_id] = commit_getter()
129 return commit_cache[commit_id]
130
131 annotation_lookup = {
132 line_no: _get_annotation(commit_id, commit_getter)
133 for line_no, commit_id, commit_getter, line_content
134 in filenode.annotate
135 }
136
137 annotations_lines = ((annotation_lookup.get(line_no), line_no, tokens)
138 for line_no, tokens
139 in filenode_as_lines_tokens(filenode))
140
141 grouped_annotations_lines = groupby(annotations_lines, lambda x: x[0])
142
143 for annotation, group in grouped_annotations_lines:
144 yield (
145 annotation, [(line_no, tokens)
146 for (_, line_no, tokens) in group]
147 )
@@ -0,0 +1,70 b''
1 <%def name="render_line(line_num, tokens,
2 annotation=None,
3 bgcolor=None)">
4 <%
5 # avoid module lookups for performance
6 from rhodecode.lib.codeblocks import pygment_token_class
7 from rhodecode.lib.helpers import html_escape
8 %>
9 <tr class="cb-line cb-line-fresh"
10 %if annotation:
11 data-revision="${annotation.revision}"
12 %endif
13 >
14 <td class="cb-lineno" id="L${line_num}">
15 <a data-line-no="${line_num}" href="#L${line_num}"></a>
16 </td>
17 <td class="cb-content cb-content-fresh"
18 %if bgcolor:
19 style="background: ${bgcolor}"
20 %endif
21 >${
22 ''.join(
23 '<span class="%s">%s</span>' %
24 (pygment_token_class(token_type), html_escape(token_text))
25 for token_type, token_text in tokens) + '\n' | n
26 }</td>
27 ## this ugly list comp is necessary for performance
28 </tr>
29 </%def>
30
31 <%def name="render_annotation_lines(annotation, lines, color_hasher)">
32 <%
33 rowspan = len(lines) + 1 # span the line's <tr> and annotation <tr>
34 %>
35 %if not annotation:
36 <tr class="cb-annotate">
37 <td class="cb-annotate-message" rowspan="${rowspan}"></td>
38 <td class="cb-annotate-revision" rowspan="${rowspan}"></td>
39 </tr>
40 %else:
41 <tr class="cb-annotate">
42 <td class="cb-annotate-info tooltip"
43 rowspan="${rowspan}"
44 title="Author: ${annotation.author | entity}<br>Date: ${annotation.date}<br>Message: ${annotation.message | entity}"
45 >
46 ${h.gravatar_with_user(annotation.author, 16) | n}
47 <strong class="cb-annotate-message">${
48 h.truncate(annotation.message, len(lines) * 30)
49 }</strong>
50 </td>
51 <td
52 class="cb-annotate-revision"
53 rowspan="${rowspan}"
54 data-revision="${annotation.revision}"
55 onclick="$('[data-revision=${annotation.revision}]').toggleClass('cb-line-fresh')"
56 style="background: ${color_hasher(annotation.raw_id)}">
57 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=annotation.raw_id)}">
58 r${annotation.revision}
59 </a>
60 </td>
61 </tr>
62 %endif
63
64 %for line_num, tokens in lines:
65 ${render_line(line_num, tokens,
66 bgcolor=color_hasher(annotation and annotation.raw_id or ''),
67 annotation=annotation,
68 )}
69 %endfor
70 </%def>
@@ -36,6 +36,8 b' from webob.exc import HTTPNotFound, HTTP'
36 from rhodecode.controllers.utils import parse_path_ref
36 from rhodecode.controllers.utils import parse_path_ref
37 from rhodecode.lib import diffs, helpers as h, caches
37 from rhodecode.lib import diffs, helpers as h, caches
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.codeblocks import (
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
39 from rhodecode.lib.utils import jsonify, action_logger
41 from rhodecode.lib.utils import jsonify, action_logger
40 from rhodecode.lib.utils2 import (
42 from rhodecode.lib.utils2 import (
41 convert_line_endings, detect_mode, safe_str, str2bool)
43 convert_line_endings, detect_mode, safe_str, str2bool)
@@ -221,9 +223,15 b' class FilesController(BaseRepoController'
221 c.file_author = True
223 c.file_author = True
222 c.file_tree = ''
224 c.file_tree = ''
223 if c.file.is_file():
225 if c.file.is_file():
224 c.renderer = (
225 c.renderer and h.renderer_from_filename(c.file.path))
226 c.file_last_commit = c.file.last_commit
226 c.file_last_commit = c.file.last_commit
227 if c.annotate: # annotation has precedence over renderer
228 c.annotated_lines = filenode_as_annotated_lines_tokens(
229 c.file)
230 else:
231 c.renderer = (
232 c.renderer and h.renderer_from_filename(c.file.path))
233 if not c.renderer:
234 c.lines = filenode_as_lines_tokens(c.file)
227
235
228 c.on_branch_head = self._is_valid_head(
236 c.on_branch_head = self._is_valid_head(
229 commit_id, c.rhodecode_repo)
237 commit_id, c.rhodecode_repo)
@@ -67,7 +67,6 b' from webhelpers.html.tags import _set_in'
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
68 from webhelpers2.number import format_byte_size
68 from webhelpers2.number import format_byte_size
69
69
70 from rhodecode.lib.annotate import annotate_highlight
71 from rhodecode.lib.action_parser import action_parser
70 from rhodecode.lib.action_parser import action_parser
72 from rhodecode.lib.ext_json import json
71 from rhodecode.lib.ext_json import json
73 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
72 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
@@ -125,17 +124,18 b' def asset(path, ver=None):'
125 'rhodecode:public/{}'.format(path), _query=query)
124 'rhodecode:public/{}'.format(path), _query=query)
126
125
127
126
128 def html_escape(text, html_escape_table=None):
127 default_html_escape_table = {
128 ord('&'): u'&amp;',
129 ord('<'): u'&lt;',
130 ord('>'): u'&gt;',
131 ord('"'): u'&quot;',
132 ord("'"): u'&#39;',
133 }
134
135
136 def html_escape(text, html_escape_table=default_html_escape_table):
129 """Produce entities within text."""
137 """Produce entities within text."""
130 if not html_escape_table:
138 return text.translate(html_escape_table)
131 html_escape_table = {
132 "&": "&amp;",
133 '"': "&quot;",
134 "'": "&apos;",
135 ">": "&gt;",
136 "<": "&lt;",
137 }
138 return "".join(html_escape_table.get(c, c) for c in text)
139
139
140
140
141 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
141 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
@@ -500,6 +500,86 b' def get_matching_line_offsets(lines, ter'
500 return matching_lines
500 return matching_lines
501
501
502
502
503 def hsv_to_rgb(h, s, v):
504 """ Convert hsv color values to rgb """
505
506 if s == 0.0:
507 return v, v, v
508 i = int(h * 6.0) # XXX assume int() truncates!
509 f = (h * 6.0) - i
510 p = v * (1.0 - s)
511 q = v * (1.0 - s * f)
512 t = v * (1.0 - s * (1.0 - f))
513 i = i % 6
514 if i == 0:
515 return v, t, p
516 if i == 1:
517 return q, v, p
518 if i == 2:
519 return p, v, t
520 if i == 3:
521 return p, q, v
522 if i == 4:
523 return t, p, v
524 if i == 5:
525 return v, p, q
526
527
528 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
529 """
530 Generator for getting n of evenly distributed colors using
531 hsv color and golden ratio. It always return same order of colors
532
533 :param n: number of colors to generate
534 :param saturation: saturation of returned colors
535 :param lightness: lightness of returned colors
536 :returns: RGB tuple
537 """
538
539 golden_ratio = 0.618033988749895
540 h = 0.22717784590367374
541
542 for _ in xrange(n):
543 h += golden_ratio
544 h %= 1
545 HSV_tuple = [h, saturation, lightness]
546 RGB_tuple = hsv_to_rgb(*HSV_tuple)
547 yield map(lambda x: str(int(x * 256)), RGB_tuple)
548
549
550 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
551 """
552 Returns a function which when called with an argument returns a unique
553 color for that argument, eg.
554
555 :param n: number of colors to generate
556 :param saturation: saturation of returned colors
557 :param lightness: lightness of returned colors
558 :returns: css RGB string
559
560 >>> color_hash = color_hasher()
561 >>> color_hash('hello')
562 'rgb(34, 12, 59)'
563 >>> color_hash('hello')
564 'rgb(34, 12, 59)'
565 >>> color_hash('other')
566 'rgb(90, 224, 159)'
567 """
568
569 color_dict = {}
570 cgenerator = unique_color_generator(
571 saturation=saturation, lightness=lightness)
572
573 def get_color_string(thing):
574 if thing in color_dict:
575 col = color_dict[thing]
576 else:
577 col = color_dict[thing] = cgenerator.next()
578 return "rgb(%s)" % (', '.join(col))
579
580 return get_color_string
581
582
503 def get_lexer_safe(mimetype=None, filepath=None):
583 def get_lexer_safe(mimetype=None, filepath=None):
504 """
584 """
505 Tries to return a relevant pygments lexer using mimetype/filepath name,
585 Tries to return a relevant pygments lexer using mimetype/filepath name,
@@ -536,92 +616,6 b' def pygmentize(filenode, **kwargs):'
536 CodeHtmlFormatter(**kwargs)))
616 CodeHtmlFormatter(**kwargs)))
537
617
538
618
539 def pygmentize_annotation(repo_name, filenode, **kwargs):
540 """
541 pygmentize function for annotation
542
543 :param filenode:
544 """
545
546 color_dict = {}
547
548 def gen_color(n=10000):
549 """generator for getting n of evenly distributed colors using
550 hsv color and golden ratio. It always return same order of colors
551
552 :returns: RGB tuple
553 """
554
555 def hsv_to_rgb(h, s, v):
556 if s == 0.0:
557 return v, v, v
558 i = int(h * 6.0) # XXX assume int() truncates!
559 f = (h * 6.0) - i
560 p = v * (1.0 - s)
561 q = v * (1.0 - s * f)
562 t = v * (1.0 - s * (1.0 - f))
563 i = i % 6
564 if i == 0:
565 return v, t, p
566 if i == 1:
567 return q, v, p
568 if i == 2:
569 return p, v, t
570 if i == 3:
571 return p, q, v
572 if i == 4:
573 return t, p, v
574 if i == 5:
575 return v, p, q
576
577 golden_ratio = 0.618033988749895
578 h = 0.22717784590367374
579
580 for _ in xrange(n):
581 h += golden_ratio
582 h %= 1
583 HSV_tuple = [h, 0.95, 0.95]
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
586
587 cgenerator = gen_color()
588
589 def get_color_string(commit_id):
590 if commit_id in color_dict:
591 col = color_dict[commit_id]
592 else:
593 col = color_dict[commit_id] = cgenerator.next()
594 return "color: rgb(%s)! important;" % (', '.join(col))
595
596 def url_func(repo_name):
597
598 def _url_func(commit):
599 author = commit.author
600 date = commit.date
601 message = tooltip(commit.message)
602
603 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
604 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
605 "</b> %s<br/></div>")
606
607 tooltip_html = tooltip_html % (author, date, message)
608 lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
609 uri = link_to(
610 lnk_format,
611 url('changeset_home', repo_name=repo_name,
612 revision=commit.raw_id),
613 style=get_color_string(commit.raw_id),
614 class_='tooltip',
615 title=tooltip_html
616 )
617
618 uri += '\n'
619 return uri
620 return _url_func
621
622 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
623
624
625 def is_following_repo(repo_name, user_id):
619 def is_following_repo(repo_name, user_id):
626 from rhodecode.model.scm import ScmModel
620 from rhodecode.model.scm import ScmModel
627 return ScmModel().is_following_repo(repo_name, user_id)
621 return ScmModel().is_following_repo(repo_name, user_id)
@@ -45,7 +45,8 b''
45 @font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
45 @font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;
46 @font-family-serif: Georgia, "Times New Roman", Times, serif;
46 @font-family-serif: Georgia, "Times New Roman", Times, serif;
47 //** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
47 //** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
48 @font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
48 @font-family-monospace: Consolas, "Liberation Mono", Menlo, Monaco, Courier, monospace;
49
49 @font-family-base: @font-family-sans-serif;
50 @font-family-base: @font-family-sans-serif;
50
51
51 @font-size-base: 14px;
52 @font-size-base: 14px;
@@ -506,20 +506,19 b' div.codeblock {'
506
506
507 th,
507 th,
508 td {
508 td {
509 padding: .5em !important;
509 padding: .5em;
510 border: @border-thickness solid @border-default-color !important;
510 border: @border-thickness solid @border-default-color;
511 }
511 }
512 }
512 }
513
513
514 table {
514 table {
515 width: 0 !important;
515 border: 0px;
516 border: 0px !important;
517 margin: 0;
516 margin: 0;
518 letter-spacing: normal;
517 letter-spacing: normal;
519
518
520
519
521 td {
520 td {
522 border: 0px !important;
521 border: 0px;
523 vertical-align: top;
522 vertical-align: top;
524 }
523 }
525 }
524 }
@@ -569,7 +568,7 b' div.annotatediv { margin-left: 2px; marg'
569 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
570
569
571 .code { display: block; border:0px !important; }
570 .code { display: block; border:0px !important; }
572 .code-highlight,
571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
573 .codehilite {
572 .codehilite {
574 .hll { background-color: #ffffcc }
573 .hll { background-color: #ffffcc }
575 .c { color: #408080; font-style: italic } /* Comment */
574 .c { color: #408080; font-style: italic } /* Comment */
@@ -641,3 +640,110 b' pre.literal-block, .codehilite pre{'
641 .border-radius(@border-radius);
640 .border-radius(@border-radius);
642 background-color: @grey7;
641 background-color: @grey7;
643 }
642 }
643
644
645 /* START NEW CODE BLOCK CSS */
646
647 table.cb {
648 width: 100%;
649 border-collapse: collapse;
650 margin-bottom: 10px;
651
652 * {
653 box-sizing: border-box;
654 }
655
656 /* intentionally general selector since .cb-line-selected must override it
657 and they both use !important since the td itself may have a random color
658 generated by annotation blocks. TLDR: if you change it, make sure
659 annotated block selection and line selection in file view still work */
660 .cb-line-fresh .cb-content {
661 background: white !important;
662 }
663
664 tr.cb-annotate {
665 border-top: 1px solid #eee;
666
667 &+ .cb-line {
668 border-top: 1px solid #eee;
669 }
670
671 &:first-child {
672 border-top: none;
673 &+ .cb-line {
674 border-top: none;
675 }
676 }
677 }
678
679 td {
680 vertical-align: top;
681 padding: 2px 10px;
682
683 &.cb-content {
684 white-space: pre-wrap;
685 font-family: @font-family-monospace;
686 font-size: 12.35px;
687
688 span {
689 word-break: break-word;
690 }
691 }
692
693 &.cb-lineno {
694 padding: 0;
695 height: 1px; /* this allows the <a> link to fill to 100% height of the td */
696 width: 50px;
697 color: rgba(0, 0, 0, 0.3);
698 text-align: right;
699 border-right: 1px solid #eee;
700 font-family: @font-family-monospace;
701
702 a::before {
703 content: attr(data-line-no);
704 }
705 &.cb-line-selected {
706 background: @comment-highlight-color !important;
707 }
708
709 a {
710 display: block;
711 height: 100%;
712 color: rgba(0, 0, 0, 0.3);
713 padding: 0 10px; /* vertical padding is 0 so that height: 100% works */
714 line-height: 18px; /* use this instead of vertical padding */
715 }
716 }
717
718 &.cb-content {
719 &.cb-line-selected {
720 background: @comment-highlight-color !important;
721 }
722 }
723
724 &.cb-annotate-info {
725 width: 320px;
726 min-width: 320px;
727 max-width: 320px;
728 padding: 5px 2px;
729 font-size: 13px;
730
731 strong.cb-annotate-message {
732 padding: 5px 0;
733 white-space: pre-line;
734 display: inline-block;
735 }
736 .rc-user {
737 float: none;
738 padding: 0 6px 0 17px;
739 min-width: auto;
740 min-height: auto;
741 }
742 }
743
744 &.cb-annotate-revision {
745 cursor: pointer;
746 text-align: right;
747 }
748 }
749 }
@@ -266,7 +266,35 b' function offsetScroll(element, offset){'
266 }
266 }
267 });
267 });
268
268
269 $('.compare_view_files').on(
269 $('body').on( /* TODO: replace the $('.compare_view_files').on('click') below
270 when new diffs are integrated */
271 'click', '.cb-lineno a', function(event) {
272
273 if ($(this).attr('data-line-no') !== ""){
274 $('.cb-line-selected').removeClass('cb-line-selected');
275 var td = $(this).parent();
276 td.addClass('cb-line-selected'); // line number td
277 td.next().addClass('cb-line-selected'); // line content td
278
279 // Replace URL without jumping to it if browser supports.
280 // Default otherwise
281 if (history.pushState) {
282 var new_location = location.href.rstrip('#');
283 if (location.hash) {
284 new_location = new_location.replace(location.hash, "");
285 }
286
287 // Make new anchor url
288 new_location = new_location + $(this).attr('href');
289 history.pushState(true, document.title, new_location);
290
291 return false;
292 }
293 }
294 });
295
296 $('.compare_view_files').on( /* TODO: replace this with .cb function above
297 when new diffs are integrated */
270 'click', 'tr.line .lineno a',function(event) {
298 'click', 'tr.line .lineno a',function(event) {
271 if ($(this).text() != ""){
299 if ($(this).text() != ""){
272 $('tr.line').removeClass('selected');
300 $('tr.line').removeClass('selected');
@@ -365,10 +393,11 b' function offsetScroll(element, offset){'
365 // Select the line that comes from the url anchor
393 // Select the line that comes from the url anchor
366 // At the time of development, Chrome didn't seem to support jquery's :target
394 // At the time of development, Chrome didn't seem to support jquery's :target
367 // element, so I had to scroll manually
395 // element, so I had to scroll manually
368 if (location.hash) {
396
397 if (location.hash) { /* TODO: dan: remove this and replace with code block
398 below when new diffs are ready */
369 var result = splitDelimitedHash(location.hash);
399 var result = splitDelimitedHash(location.hash);
370 var loc = result.loc;
400 var loc = result.loc;
371 var remainder = result.remainder;
372 if (loc.length > 1){
401 if (loc.length > 1){
373 var lineno = $(loc+'.lineno');
402 var lineno = $(loc+'.lineno');
374 if (lineno.length > 0){
403 if (lineno.length > 0){
@@ -378,11 +407,70 b' function offsetScroll(element, offset){'
378 tr[0].scrollIntoView();
407 tr[0].scrollIntoView();
379
408
380 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
409 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
381 tr:tr,
410 tr: tr,
382 remainder:remainder});
411 remainder: result.remainder});
383 }
412 }
384 }
413 }
385 }
414 }
386
415
416 if (location.hash) { /* TODO: dan: use this to replace the code block above
417 when new diffs are ready */
418 var result = splitDelimitedHash(location.hash);
419 var loc = result.loc;
420 if (loc.length > 1) {
421 var page_highlights = loc.substring(
422 loc.indexOf('#') + 1).split('L');
423
424 if (page_highlights.length > 1) {
425 var highlight_ranges = page_highlights[1].split(",");
426 var h_lines = [];
427 for (var pos in highlight_ranges) {
428 var _range = highlight_ranges[pos].split('-');
429 if (_range.length === 2) {
430 var start = parseInt(_range[0]);
431 var end = parseInt(_range[1]);
432 if (start < end) {
433 for (var i = start; i <= end; i++) {
434 h_lines.push(i);
435 }
436 }
437 }
438 else {
439 h_lines.push(parseInt(highlight_ranges[pos]));
440 }
441 }
442 for (pos in h_lines) {
443 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
444 if (line_td.length) {
445 line_td.addClass('cb-line-selected'); // line number td
446 line_td.next().addClass('cb-line-selected'); // line content
447 }
448 }
449 var first_line_td = $('td.cb-lineno#L' + h_lines[0]);
450 if (first_line_td.length) {
451 var elOffset = first_line_td.offset().top;
452 var elHeight = first_line_td.height();
453 var windowHeight = $(window).height();
454 var offset;
455
456 if (elHeight < windowHeight) {
457 offset = elOffset - ((windowHeight / 4) - (elHeight / 2));
458 }
459 else {
460 offset = elOffset;
461 }
462 $(function() { // let browser scroll to hash first, then
463 // scroll the line to the middle of page
464 setTimeout(function() {
465 $('html, body').animate({ scrollTop: offset });
466 }, 100);
467 });
468 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
469 lineno: first_line_td,
470 remainder: result.remainder});
471 }
472 }
473 }
474 }
387 collapsableContent();
475 collapsableContent();
388 });
476 });
@@ -147,43 +147,6 b''
147 if (source_page) {
147 if (source_page) {
148 // variants for with source code, not tree view
148 // variants for with source code, not tree view
149
149
150 if (location.href.indexOf('#') != -1) {
151 page_highlights = location.href.substring(location.href.indexOf('#') + 1).split('L');
152 if (page_highlights.length == 2) {
153 highlight_ranges = page_highlights[1].split(",");
154
155 var h_lines = [];
156 for (pos in highlight_ranges) {
157 var _range = highlight_ranges[pos].split('-');
158 if (_range.length == 2) {
159 var start = parseInt(_range[0]);
160 var end = parseInt(_range[1]);
161 if (start < end) {
162 for (var i = start; i <= end; i++) {
163 h_lines.push(i);
164 }
165 }
166 }
167 else {
168 h_lines.push(parseInt(highlight_ranges[pos]));
169 }
170 }
171
172 for (pos in h_lines) {
173 // @comment-highlight-color
174 $('#L' + h_lines[pos]).css('background-color', '#ffd887');
175 }
176
177 var _first_line = $('#L' + h_lines[0]).get(0);
178 if (_first_line) {
179 var line = $('#L' + h_lines[0]);
180 if (line.length > 0){
181 offsetScroll(line, 70);
182 }
183 }
184 }
185 }
186
187 // select code link event
150 // select code link event
188 $("#hlcode").mouseup(getSelectionLink);
151 $("#hlcode").mouseup(getSelectionLink);
189
152
@@ -1,3 +1,4 b''
1 <%namespace name="sourceblock" file="/codeblocks/source.html"/>
1
2
2 <div id="codeblock" class="codeblock">
3 <div id="codeblock" class="codeblock">
3 <div class="codeblock-header">
4 <div class="codeblock-header">
@@ -51,12 +52,22 b''
51 </div>
52 </div>
52 %else:
53 %else:
53 % if c.file.size < c.cut_off_limit:
54 % if c.file.size < c.cut_off_limit:
54 %if c.annotate:
55 %if c.renderer and not c.annotate:
55 ${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
56 %elif c.renderer:
57 ${h.render(c.file.content, renderer=c.renderer)}
56 ${h.render(c.file.content, renderer=c.renderer)}
58 %else:
57 %else:
59 ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
58 <table class="cb codehilite">
59 %if c.annotate:
60 <% color_hasher = h.color_hasher() %>
61 %for annotation, lines in c.annotated_lines:
62 ${sourceblock.render_annotation_lines(annotation, lines, color_hasher)}
63 %endfor
64 %else:
65 %for line_num, tokens in c.lines:
66 ${sourceblock.render_line(line_num, tokens)}
67 %endfor
68 %endif
69 </table>
70 </div>
60 %endif
71 %endif
61 %else:
72 %else:
62 ${_('File is too big to display')} ${h.link_to(_('Show as raw'),
73 ${_('File is too big to display')} ${h.link_to(_('Show as raw'),
@@ -81,7 +81,7 b' def _commit_change('
81 return commit
81 return commit
82
82
83
83
84
84
85 @pytest.mark.usefixtures("app")
85 @pytest.mark.usefixtures("app")
86 class TestFilesController:
86 class TestFilesController:
87
87
@@ -270,9 +270,9 b' class TestFilesController:'
270 annotate=True))
270 annotate=True))
271
271
272 expected_revisions = {
272 expected_revisions = {
273 'hg': 'r356:25213a5fbb04',
273 'hg': 'r356',
274 'git': 'r345:c994f0de03b2',
274 'git': 'r345',
275 'svn': 'r208:209',
275 'svn': 'r208',
276 }
276 }
277 response.mustcontain(expected_revisions[backend.alias])
277 response.mustcontain(expected_revisions[backend.alias])
278
278
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now